設計模式【1.2】-- 列舉式單例有那麼好用麼?
1. 單例是什麼?
單例模式:是一種建立型設計模式,目的是保證全域性一個類只有一個例項物件,分為懶漢式和餓漢式。所謂懶漢式,類似於懶載入,需要的時候才會觸發初始化例項物件。而餓漢式正好相反,專案啟動,類載入的時候,就會建立初始化單例物件。
前面說過單例模式以及如何破壞單例模式,我們一般情況儘可能阻止單例模式被破壞,於是各種序列化,反射,以及克隆的手段,我們都需要考慮進來,最終的程式碼如下:
import java.io.Serializable; public class Singleton implements Serializable { private static int num = 0; // valitile禁止指令重排 private volatile static Singleton singleton; // 禁止多次反射呼叫構造器 private Singleton() { synchronized (Singleton.class) { if (num == 0) { num++; } else { throw new RuntimeException("Don't use this method"); } } } public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } // 禁止序列化的時候,重新生成物件 private Object readResolve() { return singleton; } }
前面提過破壞序列化的四種方式:
- 沒有將構造器私有化,可以直接呼叫。
- 反射呼叫構造器
- 實現了
cloneable
介面 - 序列化與反序列化
2. 列舉的單例可以被破壞麼?
但是突然想到一個問題,一般都說列舉的方式實現單例比較好,較為推薦。真的是這樣麼?這樣真的是安全的麼?
那我們就試試,看看各種手段,能不能破壞它的單例。首先我們來寫一個單例列舉類:
public enum SingletonEnum {
INSTANCE;
public SingletonEnum getInstance(){
return INSTANCE;
}
}
在命令列執行以下的命令看上面的列舉類編譯之後到底是什麼東西?
javac SingletonEnum.java
javap SingletonEnum
public final class singleton.SingletonEnum extends java.lang.Enum<singleton.SingletonEnum> { public static final singleton.SingletonEnum INSTANCE; public static singleton.SingletonEnum[] values(); public static singleton.SingletonEnum valueOf(java.lang.String); public singleton.SingletonEnum getInstance(); static {}; }
可以看出,實際上,編譯後的程式碼是繼承於Enum
類的,並且是泛型。用final
修飾,其實也是類,那就是不可以被繼承原因。而且INSTANCE
也是final
修飾的,也是不可變的。但是這樣看,上面的都是public
方法。那構造方法呢?沒有被重寫成為private
麼?
要是沒有重寫的話,那就很容易破壞單例啊!我們使用javap -p SingletonEnum
看看結果:
可以看出確實建構函式已經被私有化,那麼外部就不能直接呼叫到構造方法了。那其他方法呢?我們試試放射呼叫構造器:
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class SingletonTests {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
SingletonEnum singleton1 = SingletonEnum.INSTANCE;
SingletonEnum singleton2 = SingletonEnum.INSTANCE;
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
Constructor<SingletonEnum> constructor = null;
constructor = SingletonEnum.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonEnum singleton3 = constructor.newInstance();
System.out.println(singleton1 == singleton3);
}
}
執行結果如下:
692404036
692404036
Exception in thread "main" java.lang.NoSuchMethodException: singleton.SingletonEnum.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at singleton.SingletonTests.main(SingletonTests.java:15)
咦,怎麼回事?反射失敗了???
看起來報錯是getDeclaredConstructor()
失敗了,那我們看看到底有哪些構造器:
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
Constructor<SingletonEnum>[] constructor = null;
constructor = (Constructor<SingletonEnum>[]) SingletonEnum.class.getDeclaredConstructors();
for(Constructor<SingletonEnum> singletonEnumConstructor:constructor){
System.out.println(singletonEnumConstructor);
}
}
執行結果如下,發現只有一個構造器,裡面引數是String
和int
,所以啊,反射呼叫無引數構造器肯定也是如此。
private singleton.SingletonEnum(java.lang.String,int)
畢竟它是繼承於Enum
的,那我猜想它大概也只有這個方法,驗證以下,開啟原始碼:
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;
}
可以看出,這裡面只有兩個屬性:name
和ordinal
,構造器被重寫了,正是String
和int
,驗證了我們的猜想,也就是沒有辦法使用無引數構造器來構造出破壞單例的物件。那要是我們使用有引數構造呢?試試!!!
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class SingletonTests {
public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
SingletonEnum singleton1 = SingletonEnum.INSTANCE;
SingletonEnum singleton2 = SingletonEnum.INSTANCE;
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
Constructor<SingletonEnum> constructor = null;
constructor = SingletonEnum.class.getDeclaredConstructor(String.class,int.class);//其父類的構造器
constructor.setAccessible(true);
SingletonEnum singleton3 = constructor.newInstance("INSTANCE",0);
System.out.println(singleton1 == singleton3);
}
}
結果呢?還是一樣的報錯,這是什麼東東?
692404036
692404036
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at singleton.SingletonTests.main(SingletonTests.java:18)
看起來意思是不能反射建立enum物件,啥?這錯誤一看,就是Constructor.newInstance()
417行丟擲來的,我們看看:
@CallerSensitive
public 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 volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
原來反射的原始碼中,列舉型別的已經被限制了,一旦呼叫就會丟擲異常,那這條路走不通了,也就證明了反射無法破壞列舉的單例。new
物件更是行不通了。
那clone
呢?開啟Enum
的原始碼我們裡面就斷了這個念頭,這裡面的clone()
方法,已經被final
修飾了,不能被子類重寫,一呼叫就丟擲異常。所以clone
這條路也不可能破壞列舉的單例模式。
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
那序列化呢?如果我們序列化之後,再反序列化,會出現什麼情況?
import java.io.*;
import java.lang.reflect.InvocationTargetException;
public class SingletonTests {
public static void main(String[] args) throws Exception, InvocationTargetException, InstantiationException, NoSuchMethodException {
SingletonEnum singleton1 = SingletonEnum.getInstance();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("file"));
objectOutputStream.writeObject(singleton1);
File file = new File("file");
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
SingletonEnum singleton2 = (SingletonEnum) objectInputStream.readObject();
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
}
上面的程式碼執行之後,結果如下:
1627674070
1627674070
說明序列化反序列化回來之後,其實是同一個物件!!!所以無法破壞單例模式。為什麼呢?我們來分析一下原始碼!!!
先看看序列化的時候,實際上呼叫的是ObjectOutputStream.writeObject(Object obj)
writerObject()Object obj
方法裡面呼叫了writeObject0(obj,false)
,writeObject0(obj,false)
裡面看到列舉型別的序列化寫入:
writeEnum(Enum<?>)
裡面是怎麼序列化的呢?
private void writeEnum(Enum<?> en,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
// 標識是列舉型別
bout.writeByte(TC_ENUM);
ObjectStreamClass sdesc = desc.getSuperDesc();
// 型別描述
writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
handles.assign(unshared ? null : en);
// 將名字寫入name()
writeString(en.name(), false);
}
看起來序列化的時候,是用名字寫入序列化流中,那反序列化的時候呢?是怎麼操作的呢?
public final Object readObject()
throws IOException, ClassNotFoundException {
return readObject(Object.class);
}
裡面呼叫的是另外一個readObject()
方法,readObject()
方法其實是呼叫了readObject0(type,false)
。
看到反序列化的時候,列舉型別的時候,是怎麼實現的呢?裡面有一個readEnum()
:
我們來看看readEnum()
,裡面其實裡面是先讀取了名字name
,再通過名字Enum.valueOf()
獲取列舉。
所以上面沒有使用反射,還是獲取了之前的物件,綜上所述,列舉的序列化和反序列化並不會影響單例模式。
3. 總結一下
經過上面一頓分析,列舉不可以直接呼叫建構函式,不可以反射破壞單例模式,因為內部實現阻止了,實現clone
介面也不可以,這個方法已經設定為final
。序列化和反序列化的時候,內部沒有使用反射去實現,而是查詢之前的物件,直接返回,所以還是同一個物件。
這樣一來,怪不得《effective java》裡面推薦這個寫法,既簡潔,還能夠防止各種破壞,還有不用的理由麼?
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。這個世界希望一切都很快,更快,但是我希望自己能走好每一步,寫好每一篇文章,期待和你們一起交流。
此文章僅代表自己(本菜鳥)學習積累記錄,或者學習筆記,如有侵權,請聯絡作者核實刪除。人無完人,文章也一樣,文筆稚嫩,在下不才,勿噴,如果有錯誤之處,還望指出,感激不盡~