Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼里方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。
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是一項嚴肅的承諾,應該非常謹慎。如果類允許繼承,則需要更加格外小心。