1. 程式人生 > >【小家Java】深入理解Java列舉型別(enum)及7種常見的用法(含EnumMap和EnumSet)

【小家Java】深入理解Java列舉型別(enum)及7種常見的用法(含EnumMap和EnumSet)

相關閱讀

【小家java】java5新特性(簡述十大新特性) 重要一躍
【小家java】java6新特性(簡述十大新特性) 雞肋升級
【小家java】java7新特性(簡述八大新特性) 不溫不火
【小家java】java8新特性(簡述十大新特性) 飽受讚譽
【小家java】java9新特性(簡述十大新特性) 褒貶不一
【小家java】java10新特性(簡述十大新特性) 小步迭代
【小家java】java11新特性(簡述八大新特性) 首個重磅LTS版本


前文

這次當我入職一家新公司的時候,編寫程式碼發現,裡面還在大量的使用public static final…這種語句來神馬一些狀態常量。

很多時候,雖然都能暫時完成一樣的功能,但武功高低,一看便知。因此我加入之後,迅速全面引入列舉型別,並且指定列舉的使用規範、統一實現的介面。。。

什麼是列舉型別

列舉型別是Java 5中新增特性的一部分,它是一種特殊的資料型別,之所以特殊是因為它既是一種類(class)型別卻又比類型別多了些特殊的約束,但是這些約束的存在也造就了列舉型別的簡潔性、安全性以及便捷性。

列舉型別的定義

這段程式碼,是在enum沒引入之前:

public class DayDemo {
    public static final int MONDAY =1;
    public static
final int TUESDAY=2; public static final int WEDNESDAY=3; public static final int THURSDAY=4; public static final int FRIDAY=5; public static final int SATURDAY=6; public static final int SUNDAY=7; }

上述的常量定義常量的方式稱為int列舉模式,這樣的定義方式並沒有什麼錯,但它存在許多不足:

  1. 如在型別安全和使用方便性上並沒有多少好處
  2. 如果存在定義int值相同的變數,混淆的機率還是很大的,編譯器也不會提出任何警告
  3. 操作上,比如我要拿到所有的列舉值,或者根據列舉值拿到具體的名字等都非常的不方便
    因此這種方式在枚舉出現後並不提倡,現在我們利用列舉型別來重新定義上述的常量,同時也感受一把列舉定義的方式,如下定義週一到週日的常量
//列舉型別,使用關鍵字enum
public enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

相當簡潔,使用起來也是異常的方便(下面會著重講解它的用法)。

列舉的真身

需要看真身,首先我們得看看編譯後的.class檔案。

/**
 * @author [email protected]
 * @description
 * @date 2018-11-03 16:49
 */
public enum DayEnum {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

檢視.class檔案如下:
在這裡插入圖片描述
我們發現它和普通的class檔案一樣,還是會生成一個同名的.class檔案。現在我們反編譯看看.class檔案的內容:

public final class DayEnum extends Enum<DayEnum> {
    
    //編譯器為我們新增的靜態的values()方法
    public static DayEnum[] values() {
        return (DayEnum[])$VALUES.clone();
    }
    
    //編譯器為我們新增的靜態的valueOf()方法,注意間接呼叫了Enum也類的valueOf方法
    public static DayEnum valueOf(String s) {
        return (DayEnum)Enum.valueOf(com/fsx/run/enums/DayEnum, s);
    }
    
    //私有建構函式 只能由編譯器來呼叫
    private DayEnum(String enumName, int index) {
        super(enumName, index);
    }
    //前面定義的7種列舉例項
    public static final DayEnum MONDAY;
    public static final DayEnum TUESDAY;
    public static final DayEnum WEDNESDAY;
    public static final DayEnum THURSDAY;
    public static final DayEnum FRIDAY;
    public static final DayEnum SATURDAY;
    public static final DayEnum SUNDAY;
    
    //裝載所有例項的一個數組
    private static final DayEnum $VALUES[];

    //通過靜態程式碼快例項這些多例
    static {
        MONDAY = new DayEnum("MONDAY", 0);
        TUESDAY = new DayEnum("TUESDAY", 1);
        WEDNESDAY = new DayEnum("WEDNESDAY", 2);
        THURSDAY = new DayEnum("THURSDAY", 3);
        FRIDAY = new DayEnum("FRIDAY", 4);
        SATURDAY = new DayEnum("SATURDAY", 5);
        SUNDAY = new DayEnum("SUNDAY", 6);
        
        //都裝載進去
        $VALUES = (new DayEnum[] {
                MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        });
    }
}

看到了enum型別編譯後的真身,很多結果都一目瞭然了有木有。

我們注意到,DayEnum類是final型別的,將無法被繼承。而且該類繼承自java.lang.Enum類(它是一個抽象類,所有的enum型別的類都是它的子類,提供很多方法和定義)

這裡提醒大家一點,Enum類內部會有一個建構函式,該建構函式只能有編譯器呼叫,我們是無法手動操作的

列舉的Class物件

需求:我們需要一次性獲取到所有的列舉值物件:

    public static void main(String[] args) {
        DayEnum[] values = DayEnum.values();
        DayEnum[] enumConstants = DayEnum.class.getEnumConstants();
        //判斷某個class是否為列舉型別
        System.out.println(MONDAY.getClass().isEnum()); //true
    }

enum中定義抽象方法

與常規抽象類一樣,enum類允許我們為其定義抽象方法,然後使每個列舉例項都實現該方法,以便產生不同的行為方式,注意abstract關鍵字對於列舉類來說並不是必須的如下:

public enum EnumDemo3 {

    FIRST{
        @Override
        public String getInfo() {
            return "FIRST TIME";
        }
    },
    SECOND{
        @Override
        public String getInfo() {
            return "SECOND TIME";
        }
    }

    ;

    /**
     * 定義抽象方法
     * @return
     */
    public abstract String getInfo();

    //測試
    public static void main(String[] args){
        System.out.println("F:"+EnumDemo3.FIRST.getInfo());
        System.out.println("S:"+EnumDemo3.SECOND.getInfo());
        /**
         輸出結果:
         F:FIRST TIME
         S:SECOND TIME
         */
    }
}

常用的7種使用方式

用法一:常量(也是最為常用的使用場景)

在JDK1.5 之前,我們定義常量都是: public static final… 。現在好了,有了列舉,可以把相關的常量分組到一個列舉型別裡,而且列舉提供了比常量更多的方法。

public enum Color {  
  // RED, GREEN, BLANK, YELLOW //若後續沒有程式碼了,此;可以省略。否則不行
  RED, GREEN, BLANK, YELLOW;  
} 
用法二:switch

JDK1.6之前的switch語句只支援int,char,enum型別,使用列舉,能讓我們的程式碼可讀性更強。

enum Signal {  
    GREEN, YELLOW, RED  
}  
public class TrafficLight {  
    Signal color = Signal.RED;  
    public void change() {  
        switch (color) {  
        case RED:  
            color = Signal.GREEN;  
            break;  
        case YELLOW:  
            color = Signal.RED;  
            break;  
        case GREEN:  
            color = Signal.YELLOW;  
            break;  
        }  
    }  
}  
用法三:向列舉中新增新方法

如果打算自定義自己的方法,那麼必須在enum例項序列的最後新增一個分號。而且 Java 要求必須先定義 enum 例項。

public enum Color {  
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);  
    // 成員變數  
    private String name;  
    private int index;  
    
    // 構造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    
    // 普通靜態方法方法  可通過列舉類直接呼叫
    public static String getName(int index) {  
        for (Color c : Color.values()) {  
            if (c.getIndex() == index) {  
                return c.name;  
            }  
        }  
        return null;  
    }  
    // get方法(set方法是沒有必要給出的,因為並不希望改變值)  
    public String getName() {  
        return name;  
    }  
    public int getIndex() {  
        return index;  
    }  
}  
用法四:覆蓋列舉的方法

下面給出一個toString()方法覆蓋的例子。

public enum Color {  
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);  
    // 成員變數  
    private String name;  
    private int index;  
    // 構造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    //覆蓋方法  
    @Override  
    public String toString() {  
        return this.index+"_"+this.name;  
    }  
}  
用法五:實現介面(規範、統一控制非常有效)

所有的列舉都繼承自java.lang.Enum類。由於Java 不支援多繼承,所以列舉物件不能再繼承其他類。

public interface Behaviour {  
    void print();  
    String getInfo();  
}  
public enum Color implements Behaviour{  
    RED("紅色", 1), GREEN("綠色", 2), BLANK("白色", 3), YELLO("黃色", 4);  
    // 成員變數  
    private String name;  
    private int index;  
    // 構造方法  
    private Color(String name, int index) {  
        this.name = name;  
        this.index = index;  
    }  
    
   //介面方法  
    @Override  
    public String getInfo() {  
        return this.name;  
    }  
    //介面方法  
    @Override  
    public void print() {  
        System.out.println(this.index+":"+this.name);  
    }  
}  
用法六:使用介面組織列舉

這個思想是很好的。如果你的一個模組需要有多個列舉,建議可以放在介面內,來統一組織。這樣方便管理,也方便做一些多型的使用

public interface Food {  
    enum Coffee implements Food{  
        BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO  
    }  
    enum Dessert implements Food{  
        FRUIT, CAKE, GELATO  
    }  
}  
用法七:關於列舉集合的使用(EnumSet和EnumMap)

java.util.EnumSet和java.util.EnumMap是兩個列舉集合。EnumSet保證集合中的元素不重複;EnumMap中的 key是enum型別,而value則可以是任意型別

EnumMap基本用法

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> 
			implements java.io.Serializable, Cloneable

先思考這樣一個問題,現在我們有一堆size大小相同而顏色不同的資料,需要統計出每種顏色的數量是多少以便將資料錄入倉庫,定義如下列舉用於表示顏色Color:

enum Color {
    GREEN,RED,BLUE,YELLOW
}

顯然這個如果用Map來做,估計誰都會做。但是本文采用更方便,更加高效的EnumMap來處理:

//方案2:使用EnumMap
			
		// 使用color作為key,總數Integer作為值即可方便統計
        Map<Color,Integer> enumMap=new EnumMap<>(Color.class);

        for (Clothes clothes:list){
            Color color=clothes.getColor();
            Integer count = enumMap.get(color);
            if(count!=null){
                enumMap.put(color,count+1);
            }else {
                enumMap.put(color,1);
            }
        }

        System.out.println(enumMap.toString());

EnumMap作為列舉的專屬的集合,我們沒有理由再去使用HashMap,畢竟EnumMap要求其Key必須為Enum型別,因而使用Color列舉例項作為key是最恰當不過了,也避免了獲取name的步驟。

更重要的是EnumMap效率更高,因為其內部是通過陣列實現的(具體原始碼分析,本文不做過多的分析)。注意EnumMap的key值不能為null,雖說是列舉專屬集合,但其操作與一般的Map差不多,概括性來說EnumMap是專門為列舉型別量身定做的Map實現,雖然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap會更加高效.

它只能接收同一列舉型別的例項作為鍵值且不能為null,由於列舉型別例項的數量相對固定並且有限,所以EnumMap使用陣列來存放與列舉型別對應的值,畢竟陣列是一段連續的記憶體空間,根據程式區域性性原理,效率會相當高。

它有三個建構函式:

//建立一個具有指定鍵型別的空列舉對映。
EnumMap(Class<K> keyType) 
//建立一個其鍵型別與指定列舉對映相同的列舉對映,最初包含相同的對映關係(如果有的話)。     
EnumMap(EnumMap<K,? extends V> m) 
//建立一個列舉對映,從指定對映對其初始化。
EnumMap(Map<K,? extends V> m)  

使用例項:

//使用第一種構造
Map<Color,Integer> enumMap=new EnumMap<>(Color.class);
//使用第二種構造
Map<Color,Integer> enumMap2=new EnumMap<>(enumMap);
//使用第三種構造
Map<Color,Integer> hashMap = new HashMap<>();
hashMap.put(Color.GREEN, 2);
hashMap.put(Color.BLUE, 3);
Map<Color, Integer> enumMap = new EnumMap<>(hashMap);

EnumSet用法

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
	    implements Cloneable, java.io.Serializable

EnumSet 中所有元素都必須是列舉型別。

與其他Set介面的實現類HashSet/TreeSet(內部都是用對應的HashMap/TreeMap實現的)不同的是,EnumSet在內部實現是位向量(稍後分析),它是一種極為高效的位運算操作。

由於直接儲存和操作都是bit,因此EnumSet空間和時間效能都十分可觀,足以媲美傳統上基於 int 的“位標誌”的運算,重要的是我們可像操作set集合一般來操作位運算,這樣使用程式碼更簡單易懂同時又具備型別安全的優勢。

建立EnumSet並不能使用new關鍵字,因為它是個抽象類,而應該使用其提供的靜態工廠方法,EnumSet的靜態工廠方法比較多,如下:

// 建立一個具有指定元素型別的空EnumSet。
EnumSet<E>  noneOf(Class<E> elementType)       
//建立一個指定元素型別幷包含所有列舉值的EnumSet
<E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
// 建立一個包括列舉值中指定範圍元素的EnumSet
<E extends Enum<E>> EnumSet<E> range(E from, E to)
// 初始集合包括指定集合的補集
<E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
// 建立一個包括引數中所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> of(E e)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
<E extends Enum<E>> EnumSet<E> of(E first, E... rest)
//建立一個包含引數容器中的所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
<E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)

使用示例:

enum Color {
    GREEN , RED , BLUE , BLACK , YELLOW
}

public static void main(String[] args){

        //空集合
        EnumSet<Color> enumSet= EnumSet.noneOf(Color.class);
        System.out.println("新增前:"+enumSet.toString());
        enumSet.add(Color.GREEN);
        enumSet.add(Color.RED);
        enumSet.add(Color.BLACK);
        enumSet.add(Color.BLUE);
        enumSet.add(Color.YELLOW);
        System.out.println("新增後:"+enumSet.toString());

        System.out.println("-----------------------------------");

        //使用allOf建立包含所有列舉型別的enumSet,其內部根據Class物件初始化了所有列舉例項
        EnumSet<Color> enumSet1= EnumSet.allOf(Color.class)