Java设计模式——为什么要用枚举实现单例模式(避免反射、序列化问题)
1、序言
??相信如果能看到我這篇博客的小伙伴,肯定都看過Joshua Bloch大神說過的這句話:“單元素的枚舉類型已經成為實現Singleton的最佳方法”。其實,第一次讀到這句話,我連其中說的單元素指什么都不知道,尷尬。后來,網上看了搜索了好幾篇文章,發現基本上都是轉載自相同的一篇文章,而我的困惑是“為什么要用枚舉類型實現單例模式呢?”,文章中都說的很籠統,于是決定自己結合Joshua Bloch的《effective java》寫一篇總結下,給后來的同學做個參考。
2、什么是單例模式
??關于什么是單例模式的定義,我之前的一篇文章《Java設計模式——單例模式的七種寫法》中有寫過,主要是講餓漢懶漢、線程安全方面得問題,我就不再重復了,只是做下單例模式的總結。之前文章中實現單例模式三個主要特點:1、構造方法私有化;2、實例化的變量引用私有化;3、獲取實例的方法共有。
??如果不使用枚舉,大家采用的一般都是“雙重檢查加鎖”這種方式,如下,對單例模式還不了解的同學希望先大致看下這種思路,接下來的3.1和3.2都是針對這種實現方式進行探討,了解過單例模式的同學可以跳過直接看3.1的內容:
public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {}public static Singleton getInstance() {if (uniqueInstance == null) {synchronized (Singleton.class){if(uniqueInstance == null){//進入區域后,再檢查一次,如果仍是null,才創建實例uniqueInstance = new Singleton();}}}return uniqueInstance;} }3、為什么要用枚舉單例
3.1私有化構造器并不保險
??《effective java》中只簡單的提了幾句話:“享有特權的客戶端可以借助AccessibleObject.setAccessible方法,通過反射機制調用私有構造器。如果需要低于這種攻擊,可以修改構造器,讓它在被要求創建第二個實例的時候拋出異常。”下面我以代碼來演示一下,大家就能明白:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {Singleton s=Singleton.getInstance();Singleton sUsual=Singleton.getInstance();Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();constructor.setAccessible(true);Singleton sReflection=constructor.newInstance();System.out.println(s+"\n"+sUsual+"\n"+sReflection);System.out.println("正常情況下,實例化兩個實例是否相同:"+(s==sUsual));System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:"+(s==sReflection));}輸出為:
com.lxp.pattern.singleton.Singleton@1540e19d com.lxp.pattern.singleton.Singleton@1540e19d com.lxp.pattern.singleton.Singleton@677327b6 正常情況下,實例化兩個實例是否相同:true 通過反射攻擊單例模式情況下,實例化兩個實例是否相同:false??既然存在反射可以攻擊的問題,就需要按照Joshua Bloch做說的,加個異常處理。這里我就不演示了,等會講到枚舉我再演示。
3.2 序列化問題
大家先看下面這個代碼:
public class SerSingleton implements Serializable {private volatile static SerSingleton uniqueInstance;private String content;public String getContent() {return content;}public void setContent(String content) {this.content = content;}private SerSingleton() {}public static SerSingleton getInstance() {if (uniqueInstance == null) {synchronized (SerSingleton.class) {if (uniqueInstance == null) {uniqueInstance = new SerSingleton();}}}return uniqueInstance;}public static void main(String[] args) throws IOException, ClassNotFoundException {SerSingleton s = SerSingleton.getInstance();s.setContent("單例序列化");System.out.println("序列化前讀取其中的內容:"+s.getContent());ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));oos.writeObject(s);oos.flush();oos.close();FileInputStream fis = new FileInputStream("SerSingleton.obj");ObjectInputStream ois = new ObjectInputStream(fis);SerSingleton s1 = (SerSingleton)ois.readObject();ois.close();System.out.println(s+"\n"+s1);System.out.println("序列化后讀取其中的內容:"+s1.getContent());System.out.println("序列化前后兩個是否同一個:"+(s==s1));}}先猜猜看輸出結果:
序列化前讀取其中的內容:單例序列化 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;} }怎么樣,是不是覺得好簡單,只有這么點代碼,其實也沒這么簡單啦,編譯后相當于:
public final class EnumSingleton extends Enum< EnumSingleton> {public static final EnumSingleton ENUMSINGLETON;public static EnumSingleton[] values();public static EnumSingleton valueOf(String s);static {}; }咱們先來驗證下會不會避免上述的兩個問題,先看下枚舉單例的優點,然后再來講原理。
3.3.2避免反射攻擊
public enum EnumSingleton {INSTANCE;public EnumSingleton getInstance(){return INSTANCE;}public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {EnumSingleton singleton1=EnumSingleton.INSTANCE;EnumSingleton singleton2=EnumSingleton.INSTANCE;System.out.println("正常情況下,實例化兩個實例是否相同:"+(singleton1==singleton2));Constructor<EnumSingleton> constructor= null;constructor = EnumSingleton.class.getDeclaredConstructor();constructor.setAccessible(true);EnumSingleton singleton3= null;singleton3 = constructor.newInstance();System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:"+(singleton1==singleton3));} }結果就報異常了:
Exception in thread "main" java.lang.NoSuchMethodException: com.lxp.pattern.singleton.EnumSingleton.<init>()at java.lang.Class.getConstructor0(Class.java:3082)at java.lang.Class.getDeclaredConstructor(Class.java:2178)at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:20)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) 正常情況下,實例化兩個實例是否相同:true??然后debug模式,可以發現是因為EnumSingleton.class.getDeclaredConstructors()獲取所有構造器,會發現并沒有我們所設置的無參構造器,只有一個參數為(String.class,int.class)構造器,然后看下Enum源碼就明白,這兩個參數是name和ordial兩個屬性:
public abstract class Enum<E extends Enum<E>>implements Comparable<E>, Serializable {private final String name;public final String name() {return name;}private final int ordinal;public final int ordinal() {return ordinal;}protected Enum(String name, int ordinal) {this.name = name;this.ordinal = ordinal;}//余下省略??枚舉Enum是個抽象類,其實一旦一個類聲明為枚舉,實際上就是繼承了Enum,所以會有(String.class,int.class)的構造器。既然是可以獲取到父類Enum的構造器,那你也許會說剛才我的反射是因為自身的類沒有無參構造方法才導致的異常,并不能說單例枚舉避免了反射攻擊。好的,那我們就使用父類Enum的構造器,看看是什么情況:
public enum EnumSingleton {INSTANCE;public EnumSingleton getInstance(){return INSTANCE;}public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {EnumSingleton singleton1=EnumSingleton.INSTANCE;EnumSingleton singleton2=EnumSingleton.INSTANCE;System.out.println("正常情況下,實例化兩個實例是否相同:"+(singleton1==singleton2));Constructor<EnumSingleton> constructor= null; // constructor = EnumSingleton.class.getDeclaredConstructor();constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父類的構造器constructor.setAccessible(true);EnumSingleton singleton3= null;//singleton3 = constructor.newInstance();singleton3 = constructor.newInstance("testInstance",66);System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);System.out.println("通過反射攻擊單例模式情況下,實例化兩個實例是否相同:"+(singleton1==singleton3));} }然后咱們看運行結果:
正常情況下,實例化兩個實例是否相同:true Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objectsat 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方法源碼:
@CallerSensitivepublic T newInstance(Object ... initargs)throws InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException{if (!override) {if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {Class<?> caller = Reflection.getCallerClass();checkAccess(caller, clazz, null, modifiers);}}if ((clazz.getModifiers() & Modifier.ENUM) != 0)throw new IllegalArgumentException("Cannot reflectively create enum objects");ConstructorAccessor ca = constructorAccessor; // read volatileif (ca == null) {ca = acquireConstructorAccessor();}@SuppressWarnings("unchecked")T inst = (T) ca.newInstance(initargs);return inst;}請看的第12行源碼,說明反射在通過newInstance創建對象時,會檢查該類是否ENUM修飾,如果是則拋出異常,反射失敗。
3.3.3避免序列化問題
我按照3.2中方式來寫,作為對比,方面大家看的更清晰些:
public enum SerEnumSingleton implements Serializable {INSTANCE;private String content;public String getContent() {return content;}public void setContent(String content) {this.content = content;}private SerEnumSingleton() {}public static void main(String[] args) throws IOException, ClassNotFoundException {SerEnumSingleton s = SerEnumSingleton.INSTANCE;s.setContent("枚舉單例序列化");System.out.println("枚舉序列化前讀取其中的內容:"+s.getContent());ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));oos.writeObject(s);oos.flush();oos.close();FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");ObjectInputStream ois = new ObjectInputStream(fis);SerEnumSingleton s1 = (SerEnumSingleton)ois.readObject();ois.close();System.out.println(s+"\n"+s1);System.out.println("枚舉序列化后讀取其中的內容:"+s1.getContent());System.out.println("枚舉序列化前后兩個是否同一個:"+(s==s1));} }運行結果如下:
枚舉序列化前讀取其中的內容:枚舉單例序列化 INSTANCE INSTANCE 枚舉序列化后讀取其中的內容:枚舉單例序列化 枚舉序列化前后兩個是否同一個:true??枚舉類是JDK1.5才出現的,那之前的程序員面對反射攻擊和序列化問題是怎么解決的呢?其實就是像Enum源碼那樣解決的,只是現在可以用enum可以使我們代碼量變的極其簡潔了。至此,相信同學們應該能明白了為什么Joshua Bloch說的“單元素的枚舉類型已經成為實現Singleton的最佳方法”了吧,也算解決了我自己的困惑。既然能解決這些問題,還能使代碼量變的極其簡潔,那我們就有理由選枚舉單例模式了。對了,解決序列化問題,要先懂transient和readObject,鑒于我的主要目的不在于此,就不在此寫這兩個原理了。
總結
以上是生活随笔為你收集整理的Java设计模式——为什么要用枚举实现单例模式(避免反射、序列化问题)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java设计模式——单例模式的七种写法
- 下一篇: Java面试宝典系列之基础面试题-常见的