java 枚舉(enum) 全面解讀

簡介

枚舉是Java1.5引入的新特性,通過關鍵字enum來定義枚舉類。枚舉類是一種特殊類,它和普通類一樣可以使用構造器、定義成員變量和方法,也能實現一個或多個接口,但枚舉類不能繼承其他類.

原理分析

枚舉類型使用的最常用類型就是枚舉常量.下面通過一個簡單的Demo來說明枚舉的原理.

這樣只是能夠知道枚舉簡單的使用方法,不能看出枚舉的特點和枚舉的具體實現.

下面我們通過 jad工具來反編譯Color類, 通過jad -sjava Color.class反編譯出一份java文件.

從反編譯的類中,可以看出, 我們使用enum關鍵字編寫的類,在編譯階段編譯器會自動幫我們生成一份真正在jvm中運行的代碼.

該類繼承自 Enum類,public abstract class Enum>implements Comparable, Serializable.

Enum類接受一個繼承自Enum的泛型.(在反編譯java文件中沒有體現泛型是因為,泛型在階段就會被類型類型擦除,替換為具體的實現.).

從反編譯的Color類中可以看出,在enum關鍵字的類中,第一行 (準確的說是第一個分號前)定義的變量,都會生成一個 Color實例,且它是在靜態域中進行初始化的, 而靜態域在類加載階段的cinit中進行初始化,所以枚舉對象是線程安全的,由JVM來保證.

生成的枚舉類有 Color $VALUES[];成員變量,外部可以通過values()方法獲取當前枚舉類的所有實例對象.

Enum成員變量和方法分析


Enum成員變量


Enum成員變量和方法

Enum類實現了 Comparable接口,表明它是支持排序的,可以通過 Collections.sort 進行自動排序.實現了public final int compareTo(E o)接口,方法定義為final且其實現依賴的ordinal字段也是final類型,說明他只能根據ordinal排序,排序規則不可變.

ordinal: 表示枚舉的順序,從Color類中可以看出,它是從0開始按自然數順序增長,且其值是final類型,外部無法更改.對於 ordinal()方法,官方建議儘量不要使用它,它主要是提供給EnumMap,EnumSet使用的.

name: 表示枚舉類的名字,從Color類的構造函數可以看出,它的值就是我們定義的實例的名稱.

我們在例子中之所以能打印出實例名稱,是因為 它的toString()方法直接返回了name屬性.

equals(): 從其實現來看, 我們程序中使用 == 或者 equals來判斷兩個枚舉相等都是一樣的.

getDeclaringClass(): 方法返回枚舉聲明的Class對象

每一個枚舉類型極其定義的枚舉變量在JVM中都是唯一的

這句話的意思是枚舉類型它擁有的實例在編寫的時候,就已經確定下,不能通過其他手段進行創建,且枚舉變量在jvm有且只有一個對應的實例.

為了達到這個效果,它通過以下方法來確保.

1. 類加載時創建,保證線程安全

從Color類中可以看出, Color對象是在靜態域創建,由類加載時初始化,JVM保證線程安全,這樣就能確保Color對象不會因為併發同時請求而錯誤的創建多個實例.

2. 對序列化進行特殊處理,防止反序列化時創建新的對象

我們知道一旦實現了Serializable接口之後,反序列化時每次調用 readObject()方法返回的都是一個新創建出來的對象.

而枚舉則不同,在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是通過Enum的valueOf()方法來根據名字查找枚舉對象。同時,編譯器是不允許任何對這種序列化進行定製,因此禁用了writeObject

readObjectreadObjectNoDatawriteReplacereadResolve等方法。

1.私有構造函數, 無法正常的 new出對象

2.無法通過 clone()方法,克隆對象

3. 無法通過反射的方式創建枚舉對象

枚舉類型,在 JVM 層面禁止了通過反射構造枚舉實例的行為,如果嘗試通過反射創建,將會報Cannot reflectively create enum objects.

枚舉類的特點總結

枚舉實例必須在 enum關鍵字聲明的類中顯式的指定(首行開始的以第一個分號結束)除了1, 沒有任何方式(new,clone,反射,序列化)可以手動創建枚舉實例枚舉類不可被繼承枚舉類是線程安全的枚舉類型是類型安全的(typesafe)無法繼承其他類(已經默認繼承Enum)

枚舉的使用

枚舉常量

如上訴 Color枚舉類,就是典型的枚舉常量.

它可以在 switch語句中使用

枚舉類型是類型安全的,可以對傳入的值進行類型檢查:

如有個 handleColor(Color color)方法,那麼方法參數自動會對類型進行檢查,只能傳入 Color.WHITEColor.BLACK,如果使用 static final定義的常量則不具備 類型安全的特點.

枚舉與構造函數

枚舉類可以編寫自己的構造函數,但是不能聲明public,protected,為了是不讓外部創建實例對象,默認為private且只能為它.

枚舉與類

除了枚舉常量外, enum是一個完整的類,它也可以編寫自己的構造方法以及方法,甚至實現接口.

這裡需要注意,枚舉類不能繼承其他類,因為在編譯時它已經繼承了 Enum,java無法多繼承

枚舉與單例模式

單例模式網上有6-7中寫法,除了 枚舉方式外, 都有兩個致命的缺點, 不能完全保證單例在jvm中保持唯一性.

1. 反射創建單例對象

解決方案 : 在構造上述中判斷,當多於一個實例時,再調用構造函數,直接報錯.

2. 反序列化時創建對象

解決方案 : 使用readResolve()方法來避免此事發生.

這兩種缺點雖然都有方式解決,但是不免有些繁瑣.

枚舉類天生有這些特性.而且實現單例相當簡單.

所以,枚舉實現的單例,可以說是最完美和簡潔的單例了.推薦大家使用這種方式創建單例.

但是,枚舉類的裝載和初始化時會有時間和空間的成本. 它的實現比其他方式需要更多的內存空間,所以在Android這種受資源約束的設備中儘量避免使用枚舉單例,而選擇 雙重檢查鎖(DCL)和靜態內部類的方式實現單例.

枚舉與策略模式

特定的常量類型與主體中的方法或行為有關時,即當數據與行為之間有關聯時,可以考慮使用枚舉來實現策略模式.

如我們需要實現加減運算,就可以在枚舉類型中聲明一個 apply抽象方法,在特定於常量的方法(Constant-specific class body的Constant -specific method implementation)中,用具體實現抽象方法.

枚舉與Android

在舊版的Android開發者官網的指南 Managing Your App's Memory,新版中已經被移除.

有這麼一句話 :

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

使用枚舉常量比使用final static來實現常量定義,枚舉的內存消耗比後高不止兩倍. 你應該嚴格避免在Android上使用枚舉.

導致很多開發者把它當成了教條,禁止在Android上使用枚舉.

從反編譯的Color類中可以發現, 枚舉為每一個對象創建一個枚舉對象,枚舉對象裡面至少有 一個String類型(name),和一個int類型(ordinal)再加上對象頭部佔用的內存.(此處還忽略了$VALUS數組的創建消耗).

單個枚舉類型常量,比static final聲明的常量佔用的內存大的多.

因此,不建議在Android中使用枚舉常量,而更偏向於使用 static final來定義常量.

但是,枚舉常量中有類型安全檢查的功能,使用常規的實現,沒有這種功能.

這裡我們可以使用android提供的註解來實現類型檢查. @StringDef和@IntDef

具體可以參考這篇文章. Android Performance: Avoid using ENUM on Android

但是,一定不能使用枚舉嗎?

我覺得並不如此,當數據和行為有關聯時,或者說數據受到行為的控制時,可以考慮使用策略枚舉.