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

1 引言

        相信若是能看到我這篇博客的小夥伴,確定都看過Joshua Bloch大神說過的這句話:「單元素的枚舉類型已經成爲實現Singleton的最佳方法」。其實,第一次讀到這句話,我連其中說的單元素指什麼都不知道,尷尬。後來,網上看了搜索了好幾篇文章,發現基本上都是轉載自相同的一篇文章,而個人困惑是「爲何要用枚舉類型實現單例模式呢」,文章中都說的很籠統,因而決定本身結合Joshua Bloch的《effective java》寫一篇總結下,給後來的同窗作個參考。html

2 什麼是單例模式

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

        若是不使用枚舉,你們採用的通常都是「雙重檢查加鎖」這種方式,以下,對單例模式還不瞭解的同窗但願先大體看下這種思路,接下來的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作說的,加個異常處理。這裏我就不演示了,等會講到枚舉我再演示。app

3.2 序列化問題

你們先看下面這個代碼:ide

 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 }

先猜猜看輸出結果:post

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

        能夠看出,序列化先後兩個對象並不想等。爲何會出現這種問題呢?這個講起來,又能夠寫一篇博客了,簡單來講「任何一個readObject方法,不論是顯式的仍是默認的,它都會返回一個新建的實例,這個新建的實例不一樣於該類初始化時建立的實例」固然,這個問題也是能夠解決的,想詳細瞭解的同窗能夠翻看《effective java》第77條:對於實例控制,枚舉類型優於readResolve。ui

3.3 枚舉類詳解

3.3.1 枚舉單例定義

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

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

參考:

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

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

相關文章
相關標籤/搜索