??允許對類的實例進行序列化可以非常簡單,只需將單詞implements Serializable添加到其聲明中即可勋陪。因為這很容易做到皮服,所以有一個普遍的誤解,認為序列化只需要程序員付出很少的努力逢防。事實要復雜得多。雖然使類可序列化的即時成本可以忽略不計蒲讯,但長期成本通常是巨大的忘朝。
??實現(xiàn)Serializable的一個主要成本是,一旦類的實現(xiàn)被釋放判帮,它就會降低更改該類實現(xiàn)的靈活性局嘁。當類實現(xiàn)Serializable時,其字節(jié)流編碼(或序列化形式)成為其導出API的一部分晦墙。一旦廣泛分發(fā)了一個類悦昵,通常就需要永遠支持序列化的表單,就像需要支持導出API的所有其他部分一樣晌畅。如果您不努力設計自定義序列化表單但指,而只是接受默認值,則序列化表單將永遠綁定到類的原始內(nèi)部表示抗楔。換句話說棋凳,如果您接受默認的序列化表單,類的私有和包私有實例字段將成為其導出API的一部分谓谦,并且最小化字段訪問的實踐(item15)將失去其作為信息隱藏工具的有效性贫橙。
??如果您接受默認的序列化表單,然后更改類的內(nèi)部表示反粥,則會導致序列化表單中不兼容的更改。試圖使用類的舊版本序列化實例并使用新版本反序列化實例的客戶機(反之亦然)將經(jīng)歷程序失敗。在維護原始序列化表單的同時才顿,可以更改內(nèi)部表示(使用ObjectOutputStream.putFields和ObjectInputStream.readFields),但是這很困難莫湘,并且會在源代碼中留下明顯的缺陷。如果您選擇使類可序列化郑气,您應該仔細設計一個高質(zhì)量的序列化表單幅垮,以便長期使用(第87、90項)尾组。這樣做會增加開發(fā)的初始成本忙芒,但是這樣做是值得的。即使是設計良好的序列化形式讳侨,也會限制類的演化;設計不良的序列化表單可能會造成嚴重后果呵萨。
??可串行化性對演化施加的約束的一個簡單示例涉及流惟一標識符(通常稱為串行版本)
uid。每個可序列化的類都有一個與之關(guān)聯(lián)的惟一標識號跨跨。如果您沒有通過聲明一個名為serialVersionUID的靜態(tài)final長字段來指定這個數(shù)字潮峦,系統(tǒng)在運行時通過對類的結(jié)構(gòu)應用加密哈希函數(shù)(SHA-1)自動生成它。這個值受類的名稱勇婴、它實現(xiàn)的接口及其大多數(shù)成員(包括編譯器生成的合成成員)的影響忱嘹。如果你改變了這些的任何,比如耕渴,通過添加一個方便的方法拘悦,生成的串行版本UID發(fā)生了變化。如果您未能聲明串行版本UID橱脸,兼容性將被破壞窄做,從而在運行時導致InvalidClassException異常。
??實現(xiàn)Serializable的第二個代價是增加了bug和安全漏洞的可能性(item85 )慰技。通常椭盏,對象是用構(gòu)造函數(shù)創(chuàng)建的;序列化是一種用于創(chuàng)建對象的超語言機制。無論您接受默認行為還是覆蓋它吻商,反序列化都是一個“隱藏構(gòu)造函數(shù)”掏颊,與其他構(gòu)造函數(shù)具有相同的所有問題。由于沒有與反序列化關(guān)聯(lián)的顯式構(gòu)造函數(shù)艾帐,因此很容易忘記必須確保它保證構(gòu)造函數(shù)建立的所有不變量乌叶,并且不允許攻擊者訪問正在構(gòu)造的對象的內(nèi)部。賴默認的反序列化機制可以很容易地讓對象暴露于不變的破壞和非法訪問(item88 )柒爸。
??實現(xiàn)Serializable的第三個成本是准浴,它增加了與發(fā)布類的新版本相關(guān)的測試負擔。當一個可序列化的類被修改時捎稚,重要的是檢查是否可以在新版本中序列化一個實例乐横,并在舊版本中反序列化它求橄,反之亦然。因此葡公,所需的測試量與可序列化類的數(shù)量和版本的數(shù)量成正比罐农,這兩個數(shù)量可能很大。您必須確保序列化-反序列化過程成功催什,并確保它生成原始對象的忠實副本涵亏。如果在第一次編寫類時仔細設計了自定義序列化表單,那么測試的需求就會減少 (Items 87, 90).
??實現(xiàn)Serializable并不是一個輕松的決定蒲凶。如果一個類要參與一個依賴于Java序列化來進行對象傳輸或持久性的框架气筋,這是非常重要的。此外旋圆,它還極大地簡化了將類作為必須實現(xiàn)Serializable的另一個類中的組件的使用宠默。然而,與實現(xiàn)Serializable相關(guān)的成本很多臂聋。每次設計一個類時光稼,都要權(quán)衡利弊。歷史上孩等,像BigInteger和Instant這樣的值類實現(xiàn)了序列化艾君,集合類也實現(xiàn)了序列化。表示活動實體(如線程池)的類很少應該實現(xiàn)Serializable肄方。
??為繼承而設計的類(item19)應該很少實現(xiàn) 可序列化的冰垄,接口很少應該擴展它。違反此規(guī)則會給擴展類或?qū)崿F(xiàn)接口的任何人帶來很大的負擔权她。有時違反規(guī)則是適當?shù)暮绮琛@纾绻粋€類或接口的存在主要是為了參與一個要求所有參與者實現(xiàn)Serializable的框架隅要,那么類或接口實現(xiàn)或擴展Serializable可能是有意義的蝴罪。
??為繼承而設計的實現(xiàn)序列化的類包括
Throwable和Component。Throwable實現(xiàn)了Serializable步清,因此RMI可以將異常從服務器發(fā)送到客戶機要门。Component實現(xiàn)了序列化
gui可以被發(fā)送、保存和恢復廓啊,但是即使在Swing和AWT的鼎盛時期欢搜,這個工具在實踐中也很少使用。
??如果您使用實例字段實現(xiàn)了一個既可序列化又可擴展的類谴轮,那么需要注意幾個風險炒瘟。如果實例字段值上有任何不變量,關(guān)鍵是要防止子類覆蓋finalize方法第步,該類可以通過覆蓋finalize并聲明它為final來實現(xiàn)這一點疮装。否則缘琅,該類將容易受到終結(jié)器攻擊(item8)。最后斩个,如果類的實例字段初始化為默認值(整數(shù)類型為0胯杭,布爾值為false驯杜,對象引用類型為null)受啥,那么必須添加readObjectNoData方法:
??這個方法是在Java 4中添加的,以涵蓋一個涉及到將可序列化超類添加到現(xiàn)有可序列化類[serializable, 3.5]的特殊情況鸽心。
??關(guān)于不實現(xiàn)Serializable的決定滚局,有一個警告。如果為繼承而設計的類不可序列化顽频,則可能需要額外的工作來編寫可序列化的子類藤肢。此類的常規(guī)反序列化要求超類具有可訪問的無參數(shù)構(gòu)造函數(shù)[Serialization, 1.10]。如果不提供這樣的構(gòu)造函數(shù)糯景,子類將被迫使用序列化代理模式( item90 )嘁圈。
??內(nèi)部類(內(nèi)部類(第24項)不應該實現(xiàn)Serializable。)不應該實現(xiàn)Serializable蟀淮。它們使用編譯器生成的合成字段存儲對封閉實例的引用最住,并存儲來自封閉范圍的局部變量的值。這些字段與類定義的對應關(guān)系怠惶,以及匿名類和本地類的名稱都是未指定的涨缚。因此,內(nèi)部類的默認序列化形式定義不正確策治。但是脓魏,靜態(tài)成員類可以實現(xiàn)Serializable。
??總而言之通惫,實現(xiàn)Serializable的簡單性是似是而非的茂翔。除非類只在受保護的環(huán)境中使用,在這種環(huán)境中履腋,版本永遠不必互操作珊燎,服務器永遠不會暴露給不可信的數(shù)據(jù),否則實現(xiàn)
Serializable是一個嚴肅的承諾府树,應該非常謹慎地做出俐末。如果類允許繼承,則需要格外小心奄侠。
本文寫于2019.7.24卓箫,歷時1天