爲什麼要用枚舉實現Singleton--java學習筆記

爲什麼要用枚舉實現Singleton–java學習筆記


  • 理由一:無需再考慮可序列化的情況

  《effective java》第77條:對於實例控制,枚舉類型優先於readResolve

  說到readResolve,有的人可能會不甚清楚其作用,簡單來說,readResolve的作用是這樣的:readResolve特性允許你用readObject創建的實例代替另一個實例。
  對於一個正在被反序列化的對象,如果他的類定義了一個readResolve方法,並具備正確的聲明,那麼在反序列化之後,新建對象上的readResolve方法就會被調用。然後,該方法返回的對象引用將會被返回,取代新建的對象。

  《effective java》中 第三條提到,如果在這個類的聲明中加上了‘inplments Serializable’的字樣,它就不再是一個Singleton。無論該類使用了默認的序列化形式,還是自定義的序列化形式,都沒有關係;也跟它是否提供了顯示的readObject方法無關。任何一個readObject方法,不管是顯示的還是默認的,它都會返回一個新建的實例。這個新建的實例不同於該類初始化時創建的實例。這也是爲什麼要提供readResolve方法的原因。

《effective java》第3條中:
  To make a singleton class that is implemented using either of the previous approaches serializable (Chapter 11), it is not sufficient merely to add implements Serializable to its declaration. To maintain the singleton guarantee, you have to declare all instance fields transient and provide a readResolve method (Item 77). Otherwise, each time a serialized instance is deserialized, a new instance will be created, leading, in the case of our example, to spurious Elvis sightings. To prevent this, add this readResolve method to the Elvis class:
// readResolve method to preserve singleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
  由於中文版中概念稍有混淆,特意去查閱英文原文。大體上的意思如下:
  爲了使之前的方法實現的Singleton類可序列化,僅僅在聲明中加上”implements Serializable”是不夠的。爲了保持Singleton,你需要聲明所有實例字段爲transient並提供readResolve方法。否則,每次一個序列化的實例被反序列化時,都會創建一個新的實例。比如在我們的例子中,會導致假冒的Elvis情況。爲防止此種情況,要在Elvis類中加入下面這個readResolve方法:
看到這裏應該明白,對於一個需要序列化的Singleton來說,我們需要手動爲其添加readResolve方法,在某些情況下,這樣做會尤爲複雜。
  而用枚舉來實現Singleton則完全不必考慮,因爲jvm可以保證這一點。

  • 理由二:無需再考慮通過反射調用私有構造函數的情況

      《effective java》第三條中:享有特權的客戶端可以藉助AccessiableObject.setAccessible方法,通過反射機制調用私有構造器。如果需要抵禦這種攻擊,可以修改構造器,讓他在被要求第二次創建第二個實例的時候拋出異常。

       先看一個例子

     
      
    package singleton;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    class Singleton//最普通的一種Singleton實現
    {   
        private static Singleton INSTANCE=new Singleton();
        private Singleton() 
        {
            System.out.println("私有構造函數被調用");
        }
        public static Singleton getInstance()
        {
            return INSTANCE;
        }
    }
    enum EnumSingleton2//枚舉實現的Singleton
    {
        INSTANCE;
        private EnumSingleton2()
        {
            System.out.println("enum 私有構造函數被調用");
        }
    }
    public class SingletonTest
    {
        public static void main(String[] args) 
            throws ClassNotFoundException, IllegalAccessException, 
            IllegalArgumentException, InvocationTargetException, 
            NoSuchMethodException, SecurityException, InstantiationException
        {
            Class <?> cls1 = Class.forName("singleton.Singleton");
            Class <?> cls2 = Class.forName("singleton.EnumSingleton2");
            //通過反射調用Singleton的構造函數
            Constructor <?> c0=cls1.getDeclaredConstructor();   
            c0.setAccessible(true);
            Singleton s=(Singleton)c0.newInstance();   
            //通過反射調用EnumSingleton的構造函數
            Constructor <?> c1=cls2.getDeclaredConstructor();   
            c1.setAccessible(true);   
            EnumSingleton es=(EnumSingleton)c1.newInstance();   
        }
    }
      
    

    這段代碼的執行結果如下:

    私有構造函數被調用
    enum 私有構造函數被調用
    私有構造函數被調用
    Exception in thread “main” java.lang.NoSuchMethodException: singleton.EnumSingleton2.()
    at java.lang.Class.getConstructor0(Unknown Source)
    at java.lang.Class.getDeclaredConstructor(Unknown Source)
    at singleton.SingletonTest.main(SingletonTest.java:46)

    在普通的Singleton中,我們通過反射機制調用了其私有的構造函數,而在通過反射調用enmuSingleton的私有構造函數時,則直接拋出了異常。可見,在使用枚舉實現Singleton時,jvm會替我們完成防止通過反射調用私有構造函數的工作。

  • 理由三:枚舉實例創建是線程安全的,無需再考慮Double checked locking

    我們都知道,在延遲初始化的情況下,爲了保證線程安全,通常在實現Singleton的時候使用Double checked locking,而在枚舉的情況下,我們則可以完全不必考慮這些。
    關於Double checked locking實現Singleton詳見http://www.cnblogs.com/techyc/p/3529983.html
    並且此連接中有處細節需要糾正一下,用到Double checked locking時,必須要用volatile修飾單例的實例,否則將毫無意義。
    至於爲什麼要用volatile修飾,我不說了,嘻嘻。

  • 總結

    總結起來使用枚舉類型實現Singleton主要有以下三大優勢:

    1:無需再考慮可序列化的情況

    2:無需再考慮通過反射調用私有構造函數的情況

    3:枚舉實例創建是線程安全的

    最後,千言萬語一個字,使用枚舉實現單例情況會好的多,但不排除某些情況用特殊的方法實現單例也同樣很高效。


  • 參考資料http://www.javalobby.org/java/forums/t17491.html (可能需翻牆
    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章