Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java
條目 3描述了單例(Singleton)模式,並給出瞭如下示例的單例類。 此類限制對其構造方法的訪問,以確保只建立一個實例:git
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public void leaveTheBuilding() { ... } }
如條目 3所述,若是將implements Serializable
添加到類的聲明中,則此類將再也不是單例。 類是否使用默認的序列化形式或自定義序列化形式(條目 87)並不重要,該類是否提供顯式的readObject方法(條目 88項)也可有可無。 任何readObject方法,不管是顯式方法仍是默認方法,都會返回一個新建立的實例,該實例與在類初始化時建立的實例不一樣。github
readResolve特性容許你用另外一個實例替換readObject方法 [Serialization, 3.7]建立的實例。若是正在反序列化的對象的類,使用正確的聲明定義了readResolve方法,則在新建立的對象反序列化以後,將在該對象上調用該方法。該方法返回的對象引用,代替新建立的對象返回。在該特性的大多數使用中,不保留對新建立對象的引用,所以它當即就有資格進行垃圾收集。app
若是Elvis
類用於實現Serializable,則如下read-Resolve方法足以保證單例性質:ui
// 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方法以前,將對該屬性的內容進行反序列化。這容許一個精心設計的流在對象引用屬性的內容被反序列化時,「竊取」對原來反序列化的單例對象的引用。code
下面是它的工做原理。首先,編寫一個stealer
類,該類具備readResolve方法和一個實例屬性,該實例屬性引用序列化的單例,其中stealer
「隱藏」在其中。在序列化流中,用一個stealer
實例替換單例的非瞬時狀態屬性。如今有了一個循環:單例包含了stealer
,而stealer
又引用了單例。orm
由於單例包含stealer
,因此當反序列化單例時,stealer
的readResolve方法首先運行。所以,當stealer
的readResolve方法運行時,它的實例屬性仍然引用部分反序列化(且還沒有解析)的單例。對象
stealer
的readResolve方法將引用從其實例屬性複製到靜態屬性,以便在readResolve方法運行後訪問引用。而後,該方法爲其隱藏的屬性返回正確類型的值。若是不這樣作,當序列化系統試圖將stealer
引用存儲到該屬性時,虛擬機會拋出ClassCastException異常。blog
要使其具體化,請考慮如下有問題的單例:
// 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方法並確保全部類的實例屬性都是基本類型,或瞬時狀態。