java基礎(十一) 枚舉類型
枚舉類型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類
二、 深入分析枚舉的特性與實現原理
??上面僅僅簡單地介紹了枚舉類型的最簡單的用法,下面我們將逐步深入,掌握枚舉類型的復雜的用法,以及其原理。
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方法
參考文獻
- Java枚舉enum以及應用:枚舉實現單例模式
java基礎(十一) 枚舉類型