深入理解單例模式 ( 下 )
(點擊
上方公眾號
,可快速關注)
來源:拿筆小星_ ,
blog.csdn.net/u013096088/article/details/81161084
《Effective Java》已經告訴我們,在單例類中提供一個readResolve方法就可以完成單例特性。這裡大家可以自己去測試。
接下來,我們去看看Java提供的反序列化是如何創建對象的!
ObjectInputStream
對象的序列化過程通過ObjectOutputStream和ObjectInputputStream來實現的,那麼帶著剛剛的問題,分析一下ObjectInputputStream的readObject 方法執行情況到底是怎樣的。
為了節省篇幅,這裡給出ObjectInputStream的readObject的調用棧:
大家順著此圖的關係,去看readObject方法的實現。
首先進入readObject0方法里,關鍵代碼如下:
switch (tc) {
//省略部分代碼
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
//省略部分代碼
這裡就是判斷目標對象的類型,不同類型執行不同的動作。我們的是個普通的Object對象,自然就是進入case TC_OBJECT的代碼塊中。然後進入readOrdinaryObject方法中。
readOrdinaryObject方法的代碼片段:
private Object readOrdinaryObject(boolean unshared)
throws IOException {
//此處省略部分代碼
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//此處省略部分代碼
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
重點看代碼塊:
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
這裡創建的這個obj對象,就是本方法要返回的對象,也可以暫時理解為是ObjectInputStream的readObject返回的對象。
isInstantiable:如果一個serializable/externalizable的類可以在運行時被實例化,那麼該方法就返回true。針對serializable和externalizable我會在其他文章中介紹。
desc.newInstance:該方法通過反射的方式調用無參構造方法新建一個對象。
所以。到目前為止,也就可以解釋,為什麼序列化可以破壞單例了?即序列化會通過反射調用無參數的構造方法創建一個新的對象。
接下來再看,為什麼在單例類中定義readResolve就可以解決該問題呢?還是在readOrdinaryObjec方法里繼續往下看。
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
這段代碼也很清楚地給出答案了!
如果目標類有readResolve方法,那就通過反射的方式調用要被反序列化的類的readResolve方法,返回一個對象,然後把這個新的對象複製給之前創建的obj(即最終返回的對象)。那readResolve 方法里是什麼?就是直接返回我們的單例對象。
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {
System.err.println("Elvis Constructor is invoked!");
}
private Object readResolve() {
return INSTANCE;
}
}
所以,原理也就清楚了,主要在Singleton中定義readResolve方法,並在該方法中指定要返回的對象的生成策略,就可以防止單例被破壞。
單元素枚舉類型
第三種實現單例的方式是,聲明一個單元素的枚舉類:
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
這個方法跟提供公有的欄位方法很類似,但它更簡潔,提供天然的可序列化機制和能夠強有力地保證不會出現多次實例化的情況 ,甚至面對複雜的序列化和反射的攻擊下。這種方法可能看起來不太自然,但是擁有單元素的枚舉類型可能是實現單例模式的最佳實踐。注意,如果單例必須要繼承一個父類而非枚舉的情況下是無法使用該方式的(不過可以聲明一個實現了介面的枚舉)。
我們分析一下,枚舉類型是如何阻止反射來創建實例的?直接源碼:
看Constructor類的newInstance方法。
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;
}
這行代碼(clazz.getModifiers() & Modifier.ENUM) != 0 就是用來判斷目標類是不是枚舉類型,如果是拋出異常IllegalArgumentException("Cannot reflectively create enum objects"),無法通過反射創建枚舉對象!很顯然,反射無效了。
接下來,再看一下反序列化是如何預防的。依然按照上面說的順序去找到枚舉類型對應的readEnum方法,如下:
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
readString(false):首先獲取到枚舉對象的名稱name。
Enum<?> en = Enum.valueOf((Class)cl, name):再指定名稱的指定枚舉類型獲得枚舉常量,由於枚舉中的name是唯一,切對應一個枚舉常量。所以我們獲取到了唯一的常量對象。這樣就沒有創建新的對象,維護了單例屬性。
看看Enum.valueOf 的JavaDoc文檔:
返回具有指定名稱的指定枚舉類型的枚舉常量。 該名稱必須與用於聲明此類型中的枚舉常量的標識符完全匹配。 (不允許使用無關的空白字元。)
具體實現:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
enumConstantDirectory():返回一個Map,維護著名稱到枚舉常量的映射。我們就是從這個Map里獲取已經聲明的枚舉常量,通過這個緩存池一樣的組件,讓我們可以重用這個枚舉常量!
總結
常見的單例寫法有他的弊端,存在安全性問題,如:反射,序列化的影響。
《Effective Java》作者Josh Bloch 提倡使用單元素枚舉類型的方式來實現單例,首先創建一個枚舉很簡單,其次枚舉常量是線程安全的,最後有天然的可序列化機制和防反射的機制。
參考
《單例模式的七種寫法》
http://www.hollischuang.com/archives/205
《單例與序列化的那些事兒》
http://www.hollischuang.com/archives/1144
《Effective Java》
系列
深入理解單例模式 ( 上 )
【關於投稿】
如果大家有原創好文投稿,請直接給公號發送留言。
① 留言格式:
【投稿】+《 文章標題》+ 文章鏈接
② 示例:
【投稿】《不要自稱是程序員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/
③ 最後請附上您的個人簡介哈~
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能
TAG:ImportNew |