Effective Java 第三版—— 89. 對於例項控制,列舉型別優於READRESOLVE
Tips
書中的原始碼地址: https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些程式碼裡方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。

Effective Java, Third Edition
89. 對於例項控制,列舉型別優於READRESOLVE
條目 3描述了單例(Singleton)模式,並給出了以下示例的單例類。 此類限制對其構造方法的訪問,以確保只建立一個例項:
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() {... } public void leaveTheBuilding() { ... } }
如條目 3所述,如果將 implements Serializable
新增到類的宣告中,則此類將不再是單例。 類是否使用預設的序列化形式或自定義序列化形式(條目 87)並不重要,該類是否提供顯式的readObject方法(條目 88項)也無關緊要。 任何readObject方法,無論是顯式方法還是預設方法,都會返回一個新建立的例項,該例項與在類初始化時建立的例項不同。
readResolve特性允許你用另一個例項替換readObject方法 [Serialization, 3.7]建立的例項。如果正在反序列化的物件的類,使用正確的宣告定義了readResolve方法,則在新建立的物件反序列化之後,將在該物件上呼叫該方法。該方法返回的物件引用,代替新建立的物件返回。在該特性的大多數使用中,不保留對新建立物件的引用,因此它立即就有資格進行垃圾收集。
如果 Elvis
類用於實現Serializable,則以下read-Resolve方法足以保證單例性質:
// readResolve for instance control - you can do better! private Object readResolve() { // Return the one true Elvis and let the garbage collector // take care of the Elvis impersonator. return INSTANCE; }
此方法忽略反序列化物件,返回初始化類時建立的區分的 Elvis
例項。因此, Elvis
例項的序列化形式不需要包含任何實際資料;所有例項屬性都應該宣告為transient。事實上, 如果依賴readResolve方法進行例項控制,那麼所有具有物件引用型別的例項屬性都必須宣告為transient 。否則,有決心的攻擊者有可能在執行readResolve方法之前,保護對反序列化物件的引用,使用的技術有點類似於條目 88中的 MutablePeriod
類攻擊。
這種攻擊有點複雜,但其基本思想很簡單。如果單例包含一個非瞬時狀態物件引用屬性,則在執行單例的readResolve方法之前,將對該屬性的內容進行反序列化。這允許一個精心設計的流在物件引用屬性的內容被反序列化時,“竊取”對原來反序列化的單例物件的引用。
下面是它的工作原理。首先,編寫一個 stealer
類,該類具有readResolve方法和一個例項屬性,該例項屬性引用序列化的單例,其中 stealer
“隱藏”在其中。在序列化流中,用一個 stealer
例項替換單例的非瞬時狀態屬性。現在有了一個迴圈:單例包含了 stealer
,而 stealer
又引用了單例。
因為單例包含 stealer
,所以當反序列化單例時, stealer
的readResolve方法首先執行。因此,當 stealer
的readResolve方法執行時,它的例項屬性仍然引用部分反序列化(且尚未解析)的單例。
stealer
的readResolve方法將引用從其例項屬性複製到靜態屬性,以便在readResolve方法執行後訪問引用。然後,該方法為其隱藏的屬性返回正確型別的值。如果不這樣做,當序列化系統試圖將 stealer
引用儲存到該屬性時,虛擬機器會丟擲ClassCastException異常。
要使其具體化,請考慮以下有問題的單例:
// Broken singleton - has nontransient object reference field! public class Elvis implements Serializable { public static final Elvis INSTANCE = new Elvis(); private Elvis() { } private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" }; public void printFavorites() { System.out.println(Arrays.toString(favoriteSongs)); } private Object readResolve() { return INSTANCE; } }
下面是一個“stealer”類,按照上面的描述構造:
public class ElvisStealer implements Serializable { static Elvis impersonator; private Elvis payload; private Object readResolve() { // Save a reference to the "unresolved" Elvis instance impersonator = payload; // Return object of correct type for favoriteSongs field return new String[] { "A Fool Such as I" }; } private static final long serialVersionUID = 0; }
最後,這是一個醜陋的程式,它反序列化了一個手工製作的流,生成有缺陷單例的兩個不同例項。這個程式省略了反序列化方法,因為它與條目88(第354頁)的方法相同:
public class ElvisImpersonator { // Byte stream couldn't have come from a real Elvis instance! private static final byte[] serializedForm = { (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05, 0x45, 0x6c, 0x76, 0x69, 0x73, (byte)0x84, (byte)0xe6, (byte)0x93, 0x33, (byte)0xc3, (byte)0xf4, (byte)0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53, 0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b, 0x78, 0x70, 0x71, 0x00, 0x7e, 0x00, 0x02 }; public static void main(String[] args) { // Initializes ElvisStealer.impersonator and returns // the real Elvis (which is Elvis.INSTANCE) Elvis elvis = (Elvis) deserialize(serializedForm); Elvis impersonator = ElvisStealer.impersonator; elvis.printFavorites(); impersonator.printFavorites(); } }
執行此程式將生成以下輸出,最終證明可以建立兩個不同的Elvis例項(兩種具有不同的音樂品味):
[Hound Dog, Heartbreak Hotel] [A Fool Such as I]
可以通過宣告 favoriteSongs
屬性為transient來解決問題,但最好通過把Elvis成為單個元素列舉型別來修復它(條目 3)。 正如 ElvisStealer
類攻擊所證明的那樣,使用readResolve方法來防止攻擊者訪問“臨時”反序列化例項是非常脆弱的,需要非常小心。
如果將可序列化的例項控制類編寫為列舉,Java會保證除了宣告的常量之外,不會再有有任何例項,除非攻擊者濫用 AccessibleObject.setAccessible
等特權方法。 任何能夠做到這一點的攻擊者已經擁有足夠的許可權來執行任意本機程式碼,並且所有的賭注都已關閉。 以下是下面是 Elvis
作為列舉的例子:
// Enum singleton - the preferred approach public enum Elvis { INSTANCE; private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" }; public void printFavorites() { System.out.println(Arrays.toString(favoriteSongs)); } }
使用readResolve進行例項控制並不是過時的。 如果必須編寫一個可序列化的例項控制類,例項在編譯時是未知的,那麼無法將該類表示為列舉型別。
readResolve的可訪問性非常重要。 如果在final類上放置readResolve方法,它應該是私有的。 如果將readResolve方法放在非final類上,則必須仔細考慮其可訪問性。 如果它是私有的,則不適用於任何子類。 如果它是包級私有的,它將僅適用於同一包中的子類。 如果它是受保護的或公共的,它將適用於所有不重寫它的子類。 如果readResolve方法是受保護或公共訪問,並且子類不重寫它,則反序列化子類例項將生成一個父類例項,這可能會導致ClassCastException異常。
總而言之,使用列舉型別儘可能強制例項控制不變性。 如果這是不可能的,並且還需要一個類可序列化和例項控制,則必須提供readResolve方法並確保所有類的例項屬性都是基本型別,或瞬時狀態。