(點選上方公眾號,可快速關註)
來源:拿筆小星_ ,
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 essay-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 valueOf(Class 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 提倡使用單元素列舉型別的方式來實現單例,首先建立一個列舉很簡單,其次列舉常量是執行緒安全的,最後有天然的可序列化機制和防反射的機制。
參考
-
《單例樣式的七種寫法》
-
《單例與序列化的那些事兒》
-
《Effective Java》
系列
【關於投稿】
如果大家有原創好文投稿,請直接給公號傳送留言。
① 留言格式:
【投稿】+《 文章標題》+ 文章連結
② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/
③ 最後請附上您的個人簡介哈~
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能