java基礎(十一) 列舉型別

程式設計師歐陽思海發表於2018-04-20

列舉型別Enum的簡介

1.什麼是列舉型別

列舉型別: 就是由一組具有名的值的有限集合組成新的型別。(即新的類)。

好像還是不懂,別急,我們們先來看一下 為什麼要引入列舉型別

在沒有引入列舉型別前,當我們想要維護一組 常量集合時,我們是這樣做的,看下面的例子:

class FavouriteColor_class{
    public static final  int RED   = 1;
    public static final  int BLACK = 3;
    public static final  int GREEN = 2;
    public static final  int BLUE  = 4;
    public static final  int WHITE = 5;
    public static final  int BROWN = 6;
}
複製程式碼

當我們有列舉型別後,便可以簡寫成:

//列舉型別
public enum FavouriteColor {
    //列舉成員
    RED,GREEN,BLACK,BLUE,WHITE,BROWN
}
複製程式碼

是不是很簡單,很清晰。這樣就可以省掉大量重複的程式碼,使得程式碼更加易於維護。

現在有點明白列舉型別的定義了吧!在說的再仔細一點,就是 **使用關鍵字enum來用 一組由常量組成的有限集合 來建立一個新的class類 。**至於新的class型別,請繼續往下看。

二、 深入分析列舉的特性與實現原理

上面僅僅簡單地介紹了列舉型別的最簡單的用法,下面我們將逐步深入,掌握列舉型別的複雜的用法,以及其原理。

1. 列舉成員

上面的列舉類FavouriteColor裡面的成員便都是列舉成員,換句話說,列舉成員 就是列舉類中,沒有任何型別修飾,只有變數名,也不能賦值的成員。

到這裡還是對列舉成員很疑惑,我們先將上面的例子進行反編譯一下:

public final class FavouriteColor extends Enum {

    public static final FavouriteColor RED;
    public static final FavouriteColor GREEN;
    public static final FavouriteColor BLACK;
    public static final FavouriteColor BLUE;
    public static final FavouriteColor WHITE;
    public static final FavouriteColor BROWN;
}
複製程式碼

從反編譯的結果可以看出,列舉成員都被處理成 public static final 的靜態列舉常量。即上面例子的列舉成員都是 列舉類FavouriteColor 的例項。

2. 為列舉型別新增方法、構造器、非列舉的成員

列舉型別在新增方法、構造器、非列舉成員時,與普通類是沒有多大的區別,除了以下幾個限制:

  • 列舉成員必須是最先宣告,且只能用一行宣告(相互間以逗號隔開,分號結束宣告)。
  • 構造器的訪問許可權只能是private(可以不寫,預設強制是private),不能是public、protected。
public enum FavouriteColor {
   //列舉成員
    RED, GREEN(2), BLACK(3), BLUE, WHITE, BROWN;// 必須要有分號

    // 非列舉型別的成員
    private int colorValue;
    public int aa;
    // 靜態常量也可以
    public static final int cc = 2;

    //無參構造器
    private FavouriteColor() {

    }

       //有參構造器
    FavouriteColor(int colorValue) {
         this.colorValue = colorValue;
    }

    //方法
    public void print() {
        System.out.println(cc);
    }
}
複製程式碼

可以看出,我們其實是可以使用Eunm型別做很多事情,雖然,我們一般只使用普通的列舉型別。

仔細看一下所有的列舉成員,我們會發現GREEN(2), BLACK(3) 這兩個列舉成員有點奇怪!其實也很簡答,前面說了,列舉成員其實就是列舉型別的例項,所以,GREEN(2), BLACK(3) 就是指明瞭用帶參構造器,並傳入引數,即可以理解成 FavouriteColor GREEN = new FavouriteColor(2)。其他幾個列舉型別則表示使用無參構造器來建立物件。( 事實上,編譯器會重新建立每個構造器,為每個構造器多加兩個引數)。

3. 包含抽象方法的列舉型別

列舉型別也是允許包含抽象方法的(除了幾個小限制外,列舉類幾乎與普通類一樣),那麼包含抽象方法的列舉型別的列舉成員是怎麼樣的,編譯器又是怎麼處理的?

我們知道,上面的例子 FavouriteColor 類經過反編譯後得到的類是一個繼承了Enum的final類:

public final class FavouriteColor extends Enum 
複製程式碼

那麼包含抽象方法的列舉型別是不是也是被編譯器處理成 final類,如果是這樣,那有怎麼被子類繼承呢? 還是處理成 abstract 類呢?

我們看個包含抽象方法的列舉類的例子,Fruit 類中有三種水果,希望能為每種水果輸出對應的資訊:

public enum Frutit {

    APPLE {
        @Override
        public void printFruitInfo() {
           System.out.println("This is apple");
        }
    },BANANA {
        @Override
        public void printFruitInfo() {
            System.out.println("This is apple");
        }
    },WATERMELON {
        @Override
        public void printFruitInfo() {
            System.out.println("This is apple");
        }
    };
    //抽象方法
    public abstract void printFruitInfo();
    
    public static void main(String[] arg) {
        Frutit.APPLE.printFruitInfo();
    }
}
複製程式碼

執行結果:

This is apple

對於上面的列舉成員的形式也很容易理解,因為列舉成員是一個列舉型別的例項,上面的這種形式就是一種匿名內部類的形式,即每個列舉成員的建立可以理解成:

    BANANA = new Frutit("BANANA", 1) {//此構造器是編譯器生成的,下面會說

        public void printFruitInfo() {//匿名內部類的抽象方法實現。

            System.out.println("This is apple");
        }
    };
複製程式碼

事實上,編譯器確實就是這樣處理的,即上面的例子中,建立了三個匿名內部類,同時也會多建立三個class檔案.

最後,我們反編譯一下fruit類,看fruit類的定義:

public abstract class Frutit extends Enum
複製程式碼

Fruit類被處理成抽象類,所以可以說,列舉型別經過編譯器的處理,含抽象方法的將被處理成抽象類,否則處理成final類

4. 列舉型別的父類 -- Enum

每一個列舉型別都繼承了Enum,所以是很有必要來了解一下Enum;

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

//列舉成員的名稱
private final String name;
//列舉成員的順序,是按照定義的順序,從0開始
private final int ordinal;

//構造方法
protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }


 public final int ordinal() {//返回列舉常量的序數
        return ordinal;
    }
 }

 public final String name() {//返回此列舉常量的名稱,在其列舉宣告中對其進行宣告。
        return name;
    }

 public final boolean equals(Object other) {
        return this==other;//比較地址
    }

public final int hashCode() {
        return super.hashCode();
 }

public final int compareTo(E o) {//返回列舉常量的序數
    //是按照次序 ordinal來比較的
}

 public static <T extends Enum<T>> T valueOf(Class<T> enumType,  String name) { }

 public String toString() {
        return name;
    }
複製程式碼

以上都是一些可能會用到的方法,我們從上面可以發現兩個有趣的地方:

  • Enum類實現了 Serializable 介面,也就是說可以列舉型別可以進行序列化。
  • Enum的幾乎所有方法都是final方法,也就是說,列舉型別只能重寫toString()方法,其他方法不能重寫,連hashcode()、equal()等方法也不行。

5. 真正掌握列舉型別的原理

上面說了這麼多,都是片面地、簡單地理解了列舉型別,但還沒有完全掌握列舉型別的本質,有了上面的基礎,我們將如魚得水。

想要真正理解列舉型別的本質,就得了解編譯器是如何處理列舉型別的,也就是老辦法 -- 反編譯。這次看一個完整的反編譯程式碼,先看一個例子:

public enum Fruit {

    APPLE ,BANANA ,WATERMELON ;
    
    private int value;
    
    private Fruit() {//預設構造器
       this.value = 0;   
    } 
    
    private Fruit(int value) {//帶引數的構造器
        this.value = value;
    }
}
複製程式碼

反編譯的結果:

public final class Fruit extends Enum {
   
   //3個列舉成員例項
    public static final Fruit APPLE;
    public static final Fruit BANANA;
    public static final Fruit WATERMELON;
    private int value;//普通變數
    private static final Fruit ENUM$VALUES[];//儲存列舉常量的列舉陣列

    static {//靜態域,初始化列舉常量,列舉陣列
        APPLE = new Fruit("APPLE", 0);
        BANANA = new Fruit("BANANA", 1);
        WATERMELON = new Fruit("WATERMELON", 2);
        ENUM$VALUES = (new Fruit[]{APPLE, BANANA, WATERMELON});
    }

    private Fruit(String s, int i) {//編譯器改造了預設構造器
        super(s, i);
        value = 0;
    }

    private Fruit(String s, int i, int value) {//編譯器改造了帶引數的構造器
        super(s, i);
        this.value = value;
    }

    public static Fruit[] values() {//編譯器新增了靜態方法values()
        Fruit afruit[];
        int i;
        Fruit afruit1[];
        System.arraycopy(afruit = ENUM$VALUES, 0, afruit1 = new Fruit[i = afruit.length], 0, i);
        return afruit1;
    }

    public static Fruit valueOf(String s) {//編譯器新增了靜態方法valueOf()
        return (Fruit) Enum.valueOf(Test_2018_1_16 / Fruit, s);
    }
}
複製程式碼

從反編譯的結果可以看出,編譯器為我們建立出來的列舉類做了很多工作:

- 對列舉成員的處理   編譯器對所有的列舉成員處理成public static final的列舉常量,並在靜態域中進行初始化。 - 構造器   編譯器重新定義了構造器,不僅為每個構造器都增加了兩個引數,還新增父類了的構造方法呼叫。 - 新增了兩個類方法    編譯器為列舉類新增了 values()valueOf()values()方法返回一個列舉型別的陣列,可用於遍歷列舉型別。valueOf()方法也是新增的,而且是過載了父類的valueOf()方法

注意了: 正因為列舉型別的真正構造器是再編譯時才生成的,所以我們沒法建立列舉型別的例項,以及繼承擴充套件列舉型別(即使是被處理成abstract類)。列舉型別的例項只能由編譯器來處理建立

三、 列舉型別的使用

1. switch

    Fruit fruit = Fruit.APPLE;

    switch (fruit) {
    case APPLE:
        System.out.println("APPLE");
        break;
    case BANANA:
        System.out.println("BANANA");
        break;
    case WATERMELON:
        System.out.println("WATERMELON");
        break;
    }
複製程式碼

2. 實現介面

實現介面就不多說了。列舉型別繼承了Enum類,所以不能再繼承其他類,但可以實現介面。

3. 使用介面組織列舉

前面說了,列舉型別是無法被子類繼承擴充套件的,這就造成無法滿足以下兩種情況的需求:

  • 希望擴充套件原來的列舉型別中的元素;
  • 希望使用子類對列舉型別中的元素進行分組;

看一個例子:對食物進行分類,大類是 Food,Food下面有好幾種食物類別,類別上才是具體的食物;

public interface Food {
    
    enum Appetizer implements Food {
        SALAD, SOUP, SPRING_ROLLS
    }

    enum Coffee implements Food {
        BLACK_COFFEE, DECAF_COFFEE, ESPERSSO, TEA;
    }

    enum Dessert implements Food {
        FRUIT, GELATO, TIRAMISU;
    }   
}
複製程式碼

介面Food作為一個大類,3種列舉型別做為介面的子類;Food管理著這些列舉型別。對於列舉而言,實現介面是使其子類化的唯一辦法,所以巢狀在Food中的每個列舉類都實現了Food介面。從而“所有這東西都是某種型別的Food”。

Food food = Food.Coffee.ESPERSSO;//ESPERSSO不僅是coffee,也屬於大類Food,達到分類的效果
複製程式碼

4. 使用列舉來實現單例模式

對於序列化和反序列化,因為每一個列舉型別和列舉變數在JVM中都是唯一的,即Java在序列化和反序列化列舉時做了特殊的規定,列舉的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被編譯器禁用的,因此,對於列舉單例,是不存在實現序列化介面後呼叫readObject會破壞單例的問題。所以,列舉單例是單利模式的最佳實現方式。

public enum  EnumSingletonDemo {

    SINGLETON;
    
    //其他方法、成員等
    public int otherMethod() {
        return 0;
    }
}
複製程式碼

單例的使用方式:

int a = EnumSingletonDemo.SINGLETON.otherMethod();
複製程式碼

四、EnumSet、EnumMap

此處只是簡單地介紹這兩個類的使用,並不深入分析其實現原理。

1、EnumSet

EnumSet是一個抽象類,繼承了AbstractSet類,其本質上就是一個Set**。只不過,Enumset是要與列舉型別一起使用的專用 Set 實現。列舉 set 中所有鍵都必須來自單個列舉型別,該列舉型別在建立 set 時顯式或隱式地指定**。

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
複製程式碼

儘管JDK沒有提供EnumSet的實現子類,但是EnumSet新增的方法都是static方法,而且這些方法都是用來建立一個EnumSet的物件。因此可以看做是一個對列舉中的元素進行操作的Set,而且效能也很高。看下面的例子:

public static void main(String[] args) {
    //建立物件,並指定EnumSet儲存的列舉型別
    EnumSet<FavouriteColor> set = EnumSet.allOf(FavouriteColor.class);
    //移除列舉元素
    set.remove(FavouriteColor.BLACK);
    set.remove(FavouriteColor.BLUE);
    for(FavouriteColor color : set) {//遍歷set
        System.out.println(color);
    }
}
複製程式碼

執行結果:

RED GREEN WHITE BROWN

EnumSet不支援同步訪問。實現執行緒安全的方式是:

Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));
複製程式碼

2. EnumMap

EnumMap是一個類,同樣也是與列舉型別鍵一起使用的專用 Map 實現。列舉對映中所有鍵都必須來自單個列舉型別,該列舉型別在建立對映時顯式或隱式地指定。列舉對映在內部表示為陣列。此表示形式非常緊湊且高效。

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
複製程式碼

簡單使用的例子:

public static void main(String[] args) {
        
    EnumMap< FavouriteColor,Integer> map = new EnumMap<>(FavouriteColor.class); 
    map.put(FavouriteColor.BLACK,1 );
    map.put(FavouriteColor.BLUE, 2);
    map.put(FavouriteColor.BROWN, 3);
         
    System.out.println(map.get(FavouriteColor.BLACK));
}
複製程式碼

同樣,防止意外的同步操作:

     Map<EnumKey, V> m
         = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));
複製程式碼

總結:

  • 列舉型別繼承於Enum類,所以只能用實現介面,不能再繼承其他類。
  • 列舉型別會編譯器處理成 抽象類(含抽象方法)或 final類。
  • 列舉成員都是public static final 的列舉例項常量。列舉成員必須是最先宣告,且只能宣告一行(逗號隔開,分號結束)。
  • 構造方法必須是 private,如果定義了有參的構造器,就要注意列舉成員的宣告。沒有定義構造方法時,編譯器為列舉類自動新增的是一個帶兩個引數的構造方法,並不是無參構造器。
  • 編譯器會為列舉類新增 values() 和 valueOf()兩個方法。
  • 沒有抽象方法的列舉類,被編譯器處理成 final 類。如果是包含抽象方法的列舉類則被處理成抽象abstract類。
  • Enum實現了Serializable介面,並且幾乎所有方法都是 final方法

出處:http://www.cnblogs.com/jinggod/p/8503281.html

文章有不當之處,歡迎指正,你也可以關注我的微信公眾號:好好學java,獲取優質資源。

相關文章