枚舉作為一個常規的語言概念,一直到Java5才誕生不得不說有點奇怪,以至於到現在為止很多程序員仍然更喜歡用static final的形式去命名常量而不使用,一般情況下,Java程序員用這種方式去實現枚舉:
class EnumByClass{ public static final int RED=0; public static final int GREEN=1; public static final int BLUE=2; }
這種方式實現的枚舉也叫int枚舉模式,盡管很常用,但是由int實現的枚舉很難保證安全性,即當調用不在枚舉范圍內的數值時,需要額外的維護。另外 ,也不利於查看log和測試。
此時,我們需要開始使用Java的枚舉類型,例如上面的int枚舉模式類如果用enum實現,那么代碼如下:
enum Color{ RED,GREEN,BLUE; }
上述是將枚舉作為常量集合的簡單用法,實際上,枚舉更多的還是用於switch,也是在這時才能發現枚舉相對於int枚舉模式的好處,這里面舉一個用enum實現switch的例子:
enum Color{ RED,GREEN,BLUE; } public class Hello { public static void main(String[] args){ Color color=Color.RED; int counter=10; while (counter-->0){ switch (color){ case RED: System.out.println("Red"); color=Color.BLUE; break; case BLUE: System.out.println("Blue"); color=Color.GREEN; break; case GREEN: System.out.println("Green"); color=Color.RED; break; } } } }
如果我們用int枚舉模式的話,誠然可以用一些類似++,——的語法糖,但是也要更多的考慮到安全性的問題。
如果僅此而已,那么枚舉也沒什么單獨拿出來寫博客的價值。
我個人對enum感興趣主要是因為之前在介紹Singleton時有一個非常經驗的枚舉實現的單例,代碼如下:
enum SingletonDemo{ INSTANCE; public void otherMethods(){ System.out.println("Something"); } }
簡簡單單的一點代碼就實現了一個線程安全的單例,與其說是寫法鬼斧神工,不如說是恰如其分地應用了enum的性質。
在用enum實現Singleton時我曾介紹過三個特性,自由序列化,線程安全,保證單例。這里我們就要探討一下why的問題。
首先,我們都知道enum是由class實現的,換言之,enum可以實現很多class的內容,包括可以有member和member function,這也是我們可以用enum作為一個類來實現單例的基礎。另外,由於enum是通過繼承了Enum類實現的,enum結構不能夠作為子類繼承其他類,但是可以用來實現接口。此外,enum類也不能夠被繼承,在反編譯中,我們會發現該類是final的。
其次,enum有且僅有private的構造器,防止外部的額外構造,這恰好和單例模式吻合,也為保證單例性做了一個鋪墊。這里展開說下這個private構造器,如果我們不去手寫構造器,則會有一個默認的空參構造器,我們也可以通過給枚舉變量參量來實現類的初始化。這里舉一個例子。
enum Color{ RED(1),GREEN(2),BLUE(3); private int code; Color(int code){ this.code=code; } public int getCode(){ return code; } }
需要注意的是,private修飾符對於構造器是可以省略的,但這不代表構造器的權限是默認權限。
目前我們對enum的結構和特性有了初步的了解,接下來探究一下原理層次的特性。
想要了解enum是如何工作的,就要對其進行反編譯。
反編譯后就會發現,使用枚舉其實和使用靜態類內部加載方法原理類似。枚舉會被編譯成如下形式:
public final class T extends Enum{
...
}
其中,Enum是Java提供給編譯器的一個用於繼承的類。枚舉量的實現其實是public static final T 類型的未初始化變量,之后,會在靜態代碼中對枚舉量進行初始化。所以,如果用枚舉去實現一個單例,這樣的加載時間其實有點類似於餓漢模式,並沒有起到lazy-loading的作用。
對於序列化和反序列化,因為每一個枚舉類型和枚舉變量在JVM中都是唯一的,即Java在序列化和反序列化枚舉時做了特殊的規定,枚舉的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被編譯器禁用的,因此也不存在實現序列化接口后調用readObject會破壞單例的問題。
對於線程安全方面,類似於普通的餓漢模式,通過在第一次調用時的靜態初始化創建的對象是線程安全的。
因此,選擇枚舉作為Singleton的實現方式,相對於其他方式尤其是類似的餓漢模式主要有以下優點:
1. 代碼簡單
2. 自由序列化
至於lazy-loading,考慮到一般情況不存在調用單例類又不需要實例化單例的情況,所以即便不能做到很好的lazy-loading,也並不是大問題。換言之,除了枚舉這種方案,餓漢模式也在單例設計中廣泛的被應用。例如,Hibernate默認的單例,獲取sessionFactory用的HibernateUtil類建立方式如下:
public class HibernateUtil { private static final SessionFactory ourSessionFactory; static { try { Configuration configuration = new Configuration(); configuration.configure(); ourSessionFactory = configuration.buildSessionFactory(); } catch (Throwable ex) { throw new ExceptionInInitializerError(ex); } } public static Session getSession() throws HibernateException { return ourSessionFactory.openSession(); } }
這是一個典型的餓漢模式,考慮到這個單例只有一個方法即getSession,顯然這種模式本身就是最優的且簡潔的。這里面由於SessionFactory的創建並不是用系統默認的方式,如果想要用enum去實現反而麻煩且無必要。不過至少說明這樣做也許需要一個解決自由序列化的問題。