Effective Java 第三版—— 86. 非常謹慎地實現SERIALIZABLE介面
Tips
書中的原始碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些程式碼裡方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。

Effective Java, Third Edition
86. 非常謹慎地實現SERIALIZABLE介面
允許對類的例項進行序列化可以非常簡單,只需將implements Serializable
新增到類的宣告中即可。因為這很容易做到,所以有一個普遍的誤解,認為序列化只需要程式設計師付出很少的努力。事實要複雜得多。雖然使類可序列化的即時成本可以忽略不計,但長期成本通常是巨大的。
實現Serializable的一個主要成本是,一旦類的實現被髮布,會降低更改該類實現的靈活性。當類實現Serializable時,其位元組流編碼(或序列化形式)成為其匯出API的一部分。一旦這個類被廣泛分發後,通常就需要永遠支援序列化形式,就像需要支援匯出API的所有其他部分一樣。如果不努力設計自定義序列化形式(custom serialized form),而只是接受預設值,則序列化形式將永遠繫結到類的原始內部表示上。換句話說,如果接受預設的序列化形式,類的私有和包級私有例項屬性將成為其匯出API的一部分,並且最小化屬性訪問的實踐(條目 15)也失去其作為資訊隱藏工具的有效性。
如果接受預設的序列化形式,日後更改類的內部表示,則會導致序列化形式中的不相容更改。 嘗試使用舊版本的類序列化例項並使用新版本對其進行反序列化(反之亦然)的客戶端將遇到程式失敗。 可以在保持原始序列化形式(使用ObjectOutputStream.putFields
和ObjectInputStream.readFields
)的同時更改內部表示,但這可能很困難並且在原始碼中留下可見的缺陷。 如果選擇將類序列化,應該仔細設計一個願意長期使用的高質量序列化形式(條目 87,90)。 這樣做會增加開發的初始成本,但值得付出努力。 即使是精心設計的序列化形式也會限制一個類的演變; 一個設計不良的序列化形式可能是後果嚴重的。
限制類的序列化演變的一個簡單示例涉及到流的唯一識別符號(stream unique identifiers),通常稱為序列版本UID(serial version UIDs)。 每個可序列化的類都有一個與之關聯的唯一標識號。 如果未通過宣告名為serialVersionUID的靜態fianl的long型別的來指定此數字,則系統會在執行時通過加密雜湊函式(SHA-1)根據類的結構來自動生成它。 此值受類的名稱,它實現的介面及其大多數成員(包括編譯器生成的組合成(synthetic members)員)的影響。 如果更改任何這些內容,例如,通過新增一個便捷的方法,生成的序列版本UID就會更改。 如果未能宣告序列版本UID,則相容性將被破壞,從而導致執行時出現InvalidClassException異常。
實現Serializable的第二個成本是它增加了錯誤和安全漏洞的可能性(條目 85)。 通常,使用構造方法建立物件; 序列化是一種語言之外的建立物件的機制。 無論接受預設行為還是重寫預設行為,反序列化都是一個“隱藏的構造方法”,與其他構造方法具有相同的問題。 因為沒有與反序列化相關聯的顯式構造方法,所以很容易忘記必須確保它保證構造方法建立的所有不變性,並且它不允許攻擊者訪問構造中的物件的內部。 依賴於預設的反序列化機制,可以輕鬆地將物件置於不變性破壞和非法訪問之外(第88項)。
實現Serializable的第三個成本是它增加了與釋出新版本類相關的測試負擔。 修改可序列化類時,重要的是檢查是否可以序列化新版本中的例項可以在舊版本中反序列化,反之亦然。 因此,所需的測試量與可序列化類的數量和可能很大的釋出數量的乘積成比。 必須確保“序列化——反序列化”過程成功,並確保它生成原始物件的忠實副本。 如果在首次編寫類時仔細設計自定義序列化形式,那麼測試的需求就會減少(條目 87,90)。
實現Serializable並不是一個輕鬆的決定。如果一個類要參與依賴於Java序列化來進行物件傳輸或永續性的框架,那麼這一點是非常重要的。此外,它還極大地簡化了將類作為必須實現Serializable的另一個類中的元件的使用。然而,與實現Serializable相關的成本很多。每次設計一個類時,都要權衡利弊。歷史上,像BigInteger和Instant這樣的值類實現了序列化,集合類也實現了Serializable。表示活動實體(如執行緒池)的類很少實現Serializable。
為繼承而設計的類(條目 19)應該很少實現Serializable介面,介面也很少去繼承它。 違反此規則會給繼承類或實現介面的任何人帶來沉重的負擔。但是 有時候違反規則是合適的。 例如,如果一個類或介面主要存在於要求所有參與者實現Serializable的框架中,對類或介面來說,實現或繼承Serializable是有意義的。
專為實現Serializable的繼承而設計的類包括Throwable和Component。 Throwable實現Serializable,因此RMI可以從伺服器向客戶端傳送異常。 Component實現Serializable,因此可以傳送,儲存和恢復GUI,但即使在Swing和AWT的全盛時期,這種機制在實踐中很少使用。
如果實現了具有可序列化和可擴充套件的例項屬性的類,則需要注意幾個風險。如果例項屬性的值上有任何不變行,關鍵是要防止子類重寫finalize方法,該類可以通過重寫finalize方法並宣告它為final來實現這一點。否則,該類將容易受到終結器攻擊(finalizer attacks)(條目 8)。最後,如果類的例項屬性初始化為其預設值(整數型別為零,布林值為false,物件引用型別為null),則會違反不變性,必須新增readObjectNoData方法:
// readObjectNoData for stateful extendable serializable classes private void readObjectNoData() throws InvalidObjectException { throw new InvalidObjectException("Stream data required"); }
在Java 4中添加了此方法,包括向現有可序列化類[Serialization,3.5]新增可序列化父類的極端情況。
關於不實現Serializable介面的決定有一點需要注意。 如果為繼承而設計的類,此類不可序列化,則可能需要額外的努力編寫可序列化的子類。 這種類的正常反序列化要求父類具有可訪問的無參構造方法[Serialization,1.10]。 如果不提供這樣的構造方法,則子類被迫使用序列化代理模式(serialization proxy pattern)(條目 90)。
內部類(條目 24)不應實現Serializable。 它們使用編譯器生成的合成屬性(synthetic fields)來保持對外圍例項(enclosing instances)的引用,還儲存來自外圍作用範圍的區域性變數的值。這些屬性與類定義的對應關係,以及匿名類和本地類的名稱都是未指定的。 因此,內部類的預設序列化形式是不明確的。 但是,靜態成員類可以實現Serializable。
總而言之,不要認為實現Serializable是簡單的事情。除非類只在受保護的環境中使用,在這種環境中,版本永遠不必相互操作,伺服器永遠不會暴露於不受信任的資料,否則實現Serializable是一項嚴肅的承諾,應該非常謹慎。如果類允許繼承,則需要更加格外小心。