Effective Java 第三版—— 86. 很是謹慎地實現SERIALIZABLE接口

Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java

Effective Java, Third Edition

86. 很是謹慎地實現SERIALIZABLE接口

容許對類的實例進行序列化能夠很是簡單,只需將implements Serializable添加到類的聲明中便可。由於這很容易作到,因此有一個廣泛的誤解,認爲序列化只須要程序員付出不多的努力。事實要複雜得多。雖然使類可序列化的即時成本能夠忽略不計,但長期成本一般是巨大的。git

實現Serializable的一個主要成本是,一旦類的實現被髮布,會下降更改該類實現的靈活性。當類實現Serializable時,其字節流編碼(或序列化形式)成爲其導出API的一部分。一旦這個類被普遍分發後,一般就須要永遠支持序列化形式,就像須要支持導出API的全部其餘部分同樣。若是不努力設計自定義序列化形式(custom serialized form),而只是接受默認值,則序列化形式將永遠綁定到類的原始內部表示上。換句話說,若是接受默認的序列化形式,類的私有和包級私有實例屬性將成爲其導出API的一部分,而且最小化屬性訪問的實踐(條目 15)也失去其做爲信息隱藏工具的有效性。程序員

若是接受默認的序列化形式,往後更改類的內部表示,則會致使序列化形式中的不兼容更改。 嘗試使用舊版本的類序列化實例並使用新版本對其進行反序列化(反之亦然)的客戶端將遇到程序失敗。 能夠在保持原始序列化形式(使用ObjectOutputStream.putFieldsObjectInputStream.readFields)的同時更改內部表示,但這可能很困難而且在源代碼中留下可見的缺陷。 若是選擇將類序列化,應該仔細設計一個願意長期使用的高質量序列化形式(條目 87,90)。 這樣作會增長開發的初始成本,但值得付出努力。 即便是精心設計的序列化形式也會限制一個類的演變; 一個設計不良的序列化形式多是後果嚴重的。github

限制類的序列化演變的一個簡單示例涉及到流的惟一標識符(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。ide

爲繼承而設計的類(條目 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是一項嚴肅的承諾,應該很是謹慎。若是類容許繼承,則須要更加格外當心。

相關文章
相關標籤/搜索