為什么要用枚舉實現單例模式(避免反射、序列化問題)


1 引言

        相信如果能看到我這篇博客的小伙伴,肯定都看過Joshua Bloch大神說過的這句話:“單元素的枚舉類型已經成為實現Singleton的最佳方法”。其實,第一次讀到這句話,我連其中說的單元素指什么都不知道,尷尬。后來,網上看了搜索了好幾篇文章,發現基本上都是轉載自相同的一篇文章,而我的困惑是“為什么要用枚舉類型實現單例模式呢”,文章中都說的很籠統,於是決定自己結合Joshua Bloch的《effective java》寫一篇總結下,給后來的同學做個參考。

2 什么是單例模式

        關於什么是單例模式的定義,我之前的一篇文章(最簡單的設計模式--單例模式)中有寫過,主要是講惡漢懶漢、線程安全方面得問題,我就不再重復了,只是做下單例模式的總結。之前文章中實現單例模式三個主要特點:1、構造方法私有化;2、實例化的變量引用私有化;3、獲取實例的方法共有。

        如果不使用枚舉,大家采用的一般都是“雙重檢查加鎖”這種方式,如下,對單例模式還不了解的同學希望先大致看下這種思路,接下來的3.1和3.2都是針對這種實現方式進行探討,了解過單例模式的同學可以跳過直接看3.1的內容

 1 public class Singleton {
 2     private volatile static Singleton uniqueInstance;
 3     private Singleton() {}
 4     public static Singleton getInstance() {
 5         if (uniqueInstance == null) {
 6             synchronized (Singleton.class){
 7                 if(uniqueInstance == null){//進入區域后,再檢查一次,如果仍是null,才創建實例
 8                     uniqueInstance = new Singleton();
 9                 }
10             }
11         }
12         return uniqueInstance;
13     }
14 }

3 為什么要用枚舉單例

3.1 私有化構造器並不保險

        《effective java》中只簡單的提了幾句話:“享有特權的客戶端可以借助AccessibleObject.setAccessible方法,通過反射機制調用私有構造器。如果需要低於這種攻擊,可以修改構造器,讓它在被要求創建第二個實例的時候拋出異常。”下面我以代碼來演示一下,大家就能明白:

 1  public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
 2         Singleton s=Singleton.getInstance();
 3         Singleton sUsual=Singleton.getInstance();
 4         Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
 5         constructor.setAccessible(true);
 6         Singleton sReflection=constructor.newInstance();
 7         System.out.println(s+"\n"+sUsual+"\n"+sReflection);
 8         System.out.println("正常情況下,實例化兩個實例是否相同:"+(s==sUsual));
 9         System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:"+(s==sReflection));
10     }

輸出為:

com.lxp.pattern.singleton.Singleton@1540e19d
com.lxp.pattern.singleton.Singleton@1540e19d
com.lxp.pattern.singleton.Singleton@677327b6
正常情況下,實例化兩個實例是否相同:true
通過反射攻擊單例模式情況下,實例化兩個實例是否相同:false

既然存在反射可以攻擊的問題,就需要按照Joshua Bloch做說的,加個異常處理。這里我就不演示了,等會講到枚舉我再演示。

3.2 序列化問題

大家先看下面這個代碼:

 1 public class SerSingleton implements Serializable {
 2     private volatile static SerSingleton uniqueInstance;
 3     private  String content;
 4     public String getContent() {
 5         return content;
 6     }
 7 
 8     public void setContent(String content) {
 9         this.content = content;
10     }
11     private SerSingleton() {
12     }
13 
14     public static SerSingleton getInstance() {
15         if (uniqueInstance == null) {
16             synchronized (SerSingleton.class) {
17                 if (uniqueInstance == null) {
18                     uniqueInstance = new SerSingleton();
19                 }
20             }
21         }
22         return uniqueInstance;
23     }
24 
25     
26     public static void main(String[] args) throws IOException, ClassNotFoundException {
27         SerSingleton s = SerSingleton.getInstance();
28         s.setContent("單例序列化");
29         System.out.println("序列化前讀取其中的內容:"+s.getContent());
30         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
31         oos.writeObject(s);
32         oos.flush();
33         oos.close();
34 
35         FileInputStream fis = new FileInputStream("SerSingleton.obj");
36         ObjectInputStream ois = new ObjectInputStream(fis);
37         SerSingleton s1 = (SerSingleton)ois.readObject();
38         ois.close();
39         System.out.println(s+"\n"+s1);
40         System.out.println("序列化后讀取其中的內容:"+s1.getContent());
41         System.out.println("序列化前后兩個是否同一個:"+(s==s1));
42     }
43     
44 }

先猜猜看輸出結果:

序列化前讀取其中的內容:單例序列化
com.lxp.pattern.singleton.SerSingleton@135fbaa4
com.lxp.pattern.singleton.SerSingleton@58372a00
序列化后讀取其中的內容:單例序列化
序列化前后兩個是否同一個:false

        可以看出,序列化前后兩個對象並不想等。為什么會出現這種問題呢?這個講起來,又可以寫一篇博客了,簡單來說“任何一個readObject方法,不管是顯式的還是默認的,它都會返回一個新建的實例,這個新建的實例不同於該類初始化時創建的實例”當然,這個問題也是可以解決的,想詳細了解的同學可以翻看《effective java》第77條:對於實例控制,枚舉類型優於readResolve。

3.3 枚舉類詳解

3.3.1 枚舉單例定義

咱們先來看一下枚舉類型單例:

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

怎么樣,是不是覺得好簡單,只有這么點代碼,其實也沒這么簡單啦,編譯后相當於:

1 public final class  EnumSingleton extends Enum< EnumSingleton> {
2         public static final  EnumSingleton  ENUMSINGLETON;
3         public static  EnumSingleton[] values();
4         public static  EnumSingleton valueOf(String s);
5         static {};
6 }

 

咱們先來驗證下會不會避免上述的兩個問題,先看下枚舉單例的優點,然后再來講原理。

3.3.2 避免反射攻擊

 1 public enum  EnumSingleton {
 2     INSTANCE;
 3     public EnumSingleton getInstance(){
 4         return INSTANCE;
 5     }
 6 
 7     public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
 8         EnumSingleton singleton1=EnumSingleton.INSTANCE;
 9         EnumSingleton singleton2=EnumSingleton.INSTANCE;
10         System.out.println("正常情況下,實例化兩個實例是否相同:"+(singleton1==singleton2));
11         Constructor<EnumSingleton> constructor= null;
12         constructor = EnumSingleton.class.getDeclaredConstructor();
13         constructor.setAccessible(true);
14         EnumSingleton singleton3= null;
15         singleton3 = constructor.newInstance();
16         System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
17         System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:"+(singleton1==singleton3));
18     }
19 }

結果就報異常了:

 1 Exception in thread "main" java.lang.NoSuchMethodException: com.lxp.pattern.singleton.EnumSingleton.<init>()
 2     at java.lang.Class.getConstructor0(Class.java:3082)
 3     at java.lang.Class.getDeclaredConstructor(Class.java:2178)
 4     at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:20)
 5     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 6     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
 7     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 8     at java.lang.reflect.Method.invoke(Method.java:498)
 9     at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
10 正常情況下,實例化兩個實例是否相同:true

然后debug模式,可以發現是因為EnumSingleton.class.getDeclaredConstructors()獲取所有構造器,會發現並沒有我們所設置的無參構造器,只有一個參數為(String.class,int.class)構造器,然后看下Enum源碼就明白,這兩個參數是name和ordial兩個屬性:

 1 public abstract class Enum<E extends Enum<E>>
 2             implements Comparable<E>, Serializable {
 3         private final String name;
 4         public final String name() {
 5             return name;
 6         }
 7         private final int ordinal;
 8         public final int ordinal() {
 9             return ordinal;
10         }
11         protected Enum(String name, int ordinal) {
12             this.name = name;
13             this.ordinal = ordinal;
14         }
15         //余下省略

        枚舉Enum是個抽象類,其實一旦一個類聲明為枚舉,實際上就是繼承了Enum,所以會有(String.class,int.class)的構造器。既然是可以獲取到父類Enum的構造器,那你也許會說剛才我的反射是因為自身的類沒有無參構造方法才導致的異常,並不能說單例枚舉避免了反射攻擊。好的,那我們就使用父類Enum的構造器,看看是什么情況:

  

 1 public enum  EnumSingleton {
 2     INSTANCE;
 3     public EnumSingleton getInstance(){
 4         return INSTANCE;
 5     }
 6 
 7     public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
 8         EnumSingleton singleton1=EnumSingleton.INSTANCE;
 9         EnumSingleton singleton2=EnumSingleton.INSTANCE;
10         System.out.println("正常情況下,實例化兩個實例是否相同:"+(singleton1==singleton2));
11         Constructor<EnumSingleton> constructor= null;
12 //        constructor = EnumSingleton.class.getDeclaredConstructor();
13         constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父類的構造器
14         constructor.setAccessible(true);
15         EnumSingleton singleton3= null;
16         //singleton3 = constructor.newInstance();
17         singleton3 = constructor.newInstance("testInstance",66);
18         System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
19         System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:"+(singleton1==singleton3));
20     }
21 }

然后咱們看運行結果:

正常情況下,實例化兩個實例是否相同:true
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:25)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

        繼續報異常。之前是因為沒有無參構造器,這次拿到了父類的構造器了,只是在執行第17行(我沒有復制import等包,所以行號少於我自己運行的代碼)時候拋出異常,說是不能夠反射,我們看下Constructor類的newInstance方法源碼:

 1 @CallerSensitive
 2     public T newInstance(Object ... initargs)
 3         throws InstantiationException, IllegalAccessException,
 4                IllegalArgumentException, InvocationTargetException
 5     {
 6         if (!override) {
 7             if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
 8                 Class<?> caller = Reflection.getCallerClass();
 9                 checkAccess(caller, clazz, null, modifiers);
10             }
11         }
12         if ((clazz.getModifiers() & Modifier.ENUM) != 0)
13             throw new IllegalArgumentException("Cannot reflectively create enum objects");
14         ConstructorAccessor ca = constructorAccessor;   // read volatile
15         if (ca == null) {
16             ca = acquireConstructorAccessor();
17         }
18         @SuppressWarnings("unchecked")
19         T inst = (T) ca.newInstance(initargs);
20         return inst;
21     }

請看黃顏色標注的第12行源碼,說明反射在通過newInstance創建對象時,會檢查該類是否ENUM修飾,如果是則拋出異常,反射失敗。

3.3.3 避免序列化問題

 我按照3.2中方式來寫,作為對比,方面大家看的更清晰些:

 1 public enum  SerEnumSingleton implements Serializable {
 2     INSTANCE;
 3     private  String content;
 4     public String getContent() {
 5         return content;
 6     }
 7     public void setContent(String content) {
 8         this.content = content;
 9     }
10     private SerEnumSingleton() {
11     }
12 
13     public static void main(String[] args) throws IOException, ClassNotFoundException {
14         SerEnumSingleton s = SerEnumSingleton.INSTANCE;
15         s.setContent("枚舉單例序列化");
16         System.out.println("枚舉序列化前讀取其中的內容:"+s.getContent());
17         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
18         oos.writeObject(s);
19         oos.flush();
20         oos.close();
21 
22         FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
23         ObjectInputStream ois = new ObjectInputStream(fis);
24         SerEnumSingleton s1 = (SerEnumSingleton)ois.readObject();
25         ois.close();
26         System.out.println(s+"\n"+s1);
27         System.out.println("枚舉序列化后讀取其中的內容:"+s1.getContent());
28         System.out.println("枚舉序列化前后兩個是否同一個:"+(s==s1));
29     }
30 }

運行結果如下:

1 枚舉序列化前讀取其中的內容:枚舉單例序列化
2 INSTANCE
3 INSTANCE
4 枚舉序列化后讀取其中的內容:枚舉單例序列化
5 枚舉序列化前后兩個是否同一個:true

        枚舉類是JDK1.5才出現的,那之前的程序員面對反射攻擊和序列化問題是怎么解決的呢?其實就是像Enum源碼那樣解決的,只是現在可以用enum可以使我們代碼量變的極其簡潔了。至此,相信同學們應該能明白了為什么Joshua Bloch說的“單元素的枚舉類型已經成為實現Singleton的最佳方法”了吧,也算解決了我自己的困惑。既然能解決這些問題,還能使代碼量變的極其簡潔,那我們就有理由選枚舉單例模式了。對了,解決序列化問題,要先懂transient和readObject,鑒於我的主要目的不在於此,就不在此寫這兩個原理了。推薦一個小姐姐程序媛寫的transient博客,真是思路清晰,簡單易懂,見參考2

參考:

1、《Effective Java》(第2版):p14-15,p271-274

2、Java transient關鍵字使用小記:https://www.cnblogs.com/lanxuezaipiao/p/3369962.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM