ITEM 87: CONSIDER USING A CUSTOM SERIALIZED FORM
??當(dāng)您在時間壓力下編寫類時,通常應(yīng)該將精力集中在設(shè)計最佳API上。有時,這意味著發(fā)布一個“一次性”實(shí)現(xiàn),您知道它將在未來的版本中被替換。通常這不是問題,但是如果類實(shí)現(xiàn)Serializable 并使用默認(rèn)的序列化形式,您將永遠(yuǎn)無法完全擺脫一次性實(shí)現(xiàn)。它將永遠(yuǎn)指定序列化的表單。這不僅僅是一個理論問題。這種情況發(fā)生在 Java 庫中的幾個類上,包括BigInteger。
??不考慮默認(rèn)的序列化表單是否合適,就不要接受它。從靈活性、性能和正確性的角度來看,接受默認(rèn)的序列化形式應(yīng)該是一種明智的決定。一般來說,只有當(dāng)默認(rèn)的序列化表單與設(shè)計自定義序列化表單時所選擇的編碼基本相同時,才應(yīng)該接受默認(rèn)的序列化表單。
??對象的默認(rèn)序列化形式是以對象為根的對象圖的物理表示的相當(dāng)有效的編碼。換句話說,它描述了對象中包含的數(shù)據(jù)以及從該對象可訪問的每個對象中的數(shù)據(jù)。它還描述了所有這些對象相互連接的拓?fù)?。理想的對象序列化形式只包含由對象表示的邏輯?shù)據(jù)。它獨(dú)立于物理表征。
??如果對象的物理表示與其邏輯內(nèi)容相同,則可能使用默認(rèn)的序列化形式。例如,對于以下類來說,默認(rèn)的序列化形式是合理的,它簡單地表示一個人的名字:
// Good candidate for default serialized form
public class Name implements Serializable {
/**
* Last name. Must be non-null.
* @serial
*/
private final String lastName;
/**
* First name. Must be non-null.
* @serial
*/
private final String firstName;
/**
* Middle name, or null if there is none.
* @serial
*/
private final String middleName;
... // Remainder omitted
}
??從邏輯上講,名稱由三個字符串組成,分別表示姓、名和中間名。Name中的實(shí)例字段精確地反映了這個邏輯內(nèi)容。
??即使您確定默認(rèn)的序列化形式是合適的,您通常也必須提供一個 readObject 方法來確保不變量和安全性。對于 Name, readObject 方法必須確保字段 lastName 和firstName 是非空的。item 88 和 item 90 詳細(xì)討論了這個問題。
??注意,有關(guān)于 lastName、firstName 和 middleName 字段的文檔注釋,即使它們是私有的。這是因?yàn)檫@些私有字段定義了一個公共API,它是類的序列化形式,并且這個公共 API 必須被記錄。@serial 標(biāo)記的存在告訴 Javadoc 將該文檔放在一個記錄序列化表單的特殊頁面上。
??與 Name 相反,考慮下面的類,它表示一個字符串列表(暫時忽略你可能最好使用一個標(biāo)準(zhǔn)列表實(shí)現(xiàn)):
// Awful candidate for default serialized form
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
... // Remainder omitted
}
??從邏輯上講,這個類表示一個字符串序列。在物理上,它將序列表示為雙鏈表。如果您接受默認(rèn)的序列化表單,那么序列化表單將在兩個方向上辛苦地鏡像鏈表中的每個條目以及條目之間的所有鏈接。
??當(dāng)對象的物理表示形式與其邏輯數(shù)據(jù)內(nèi)容有本質(zhì)區(qū)別時,使用默認(rèn)的序列化形式有四個缺點(diǎn):
? 它將導(dǎo)出的API永久地綁定到當(dāng)前的內(nèi)部表示。在上面的例子中,是私有的StringList。入口類成為公共 API 的一部分。如果表示法在將來的版本中發(fā)生了更改,StringList 類仍然需要在輸入時接受鏈表表示法,并在輸出時生成它。類永遠(yuǎn)不會擺脫所有處理鏈表條目的代碼,即使它不再使用它們。
? 它會占用過多的空間。在上面的示例中,序列化的表單不必要地表示鏈表中的每個條目和所有鏈接。這些條目和鏈接只是實(shí)現(xiàn)細(xì)節(jié),不值得包含在序列化的形式中。由于序列化的表單過于龐大,將其寫入磁盤或通過網(wǎng)絡(luò)發(fā)送將會非常緩慢。
? 它會消耗過多的時間。序列化邏輯不了解對象圖的拓?fù)浣Y(jié)構(gòu),因此它必須經(jīng)歷一次代價高昂的圖遍歷。在上面的示例中,只要考慮 next 引用就足夠了。
? 它會導(dǎo)致堆棧溢出。默認(rèn)的序列化過程對對象圖執(zhí)行遞歸遍歷,這可能導(dǎo)致堆棧溢出,即使對于中等大小的對象圖也是如此。序列化包含 1,000-1,800 個元素的StringList 實(shí)例會在我的機(jī)器上生成 StackOverflowError。令人驚訝的是,串行化導(dǎo)致堆棧溢出的最小列表大小(在我的機(jī)器上)因運(yùn)行而異。顯示這個問題的最小列表大小可能取決于平臺實(shí)現(xiàn)和命令行標(biāo)志;有些實(shí)現(xiàn)可能根本沒有這個問題。
??合理的 StringList 序列化形式是列表中的字符串?dāng)?shù)量,后面跟著字符串本身。這構(gòu)成了由 StringList 表示的邏輯數(shù)據(jù),去掉了其物理表示的細(xì)節(jié)。下面是修改后的StringList,帶有 writeobject 和 readObject 方法,它們實(shí)現(xiàn)了這個序列化的表單。提醒一下,transient 修飾符表示一個實(shí)例字段將從類的默認(rèn)序列化形式中被省略:
// StringList with a reasonable custom serialized form
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// No longer Serializable!
private static class Entry {
String data;
Entry next;
Entry previous;
}
// Appends the specified string to the list
public final void add(String s) { ... }
/**
* Serialize this {@code StringList} instance. *
* @serialData The size of the list (the number of strings
* it contains) is emitted ({@code int}), followed by all of
* its elements (each a {@code String}), in the proper
* sequence. */
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// Write out all elements in the proper order.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
int numElements = s.readInt();
// Read in all elements and insert them in list
for (int i = 0; i < numElements; i++)
add((String) s.readObject());
}
... // Remainder omitted
}
??writeObject 做的第一件事是調(diào)用 defaultWriteObject, readObject 做的第一件事是調(diào)用 defaultReadObject,盡管所有 StringList 的字段都是瞬時的。您可能聽說過,如果一個類的所有實(shí)例字段都是瞬態(tài)的,那么您可以不必調(diào)用 defaultWriteObject 和defaultReadObject,但是序列化規(guī)范要求您無論如何都要調(diào)用它們。這些調(diào)用的存在使得在以后的版本中添加非瞬態(tài)實(shí)例字段成為可能,同時保持向后和向前的兼容性。如果實(shí)例在以后的版本中序列化,在以前的版本中反序列化,添加的字段將被忽略。如果早期版本的 readObject 方法未能調(diào)用 defaultReadObject,反序列化將失敗,并出現(xiàn) StreamCorruptedException.
??請注意,writeObject 方法有一個文檔注釋,盡管它是私有的。這類似于對 Name 類中的私有字段的文檔注釋。這個私有方法定義了一個公共 API,它是序列化的形式,并且這個公共API應(yīng)該被記錄下來。與字段的 @serial 標(biāo)記一樣,方法的 @serialData 標(biāo)記告訴 Javadoc 實(shí)用程序?qū)⒃撐臋n放在序列化的表單頁面上。
??為了給前面的性能討論增加一些伸縮性,如果字符串的平均長度是 10 個字符,那么修改后的 StringList 序列化形式所占的空間大約是原始序列化形式的一半。在我的機(jī)器上,序列化修改后的 StringList 版本的速度是序列化原始版本(列表長度為10)的兩倍多。最后,修改后的格式不存在堆棧溢出問題,因此可以序列化的 StringList 的大小實(shí)際上沒有上限。
??雖然默認(rèn)的序列化形式對 StringList 不好,但對于某些類來說,它可能更糟。對于StringList,默認(rèn)的序列化形式是不靈活的,性能也很差,但是它是正確的,因?yàn)樾蛄谢头葱蛄谢粋€ StringList 實(shí)例會產(chǎn)生一個原始對象的忠實(shí)副本,并且它的所有不變量都保持不變。不變量綁定到特定于實(shí)現(xiàn)的細(xì)節(jié)的任何對象都不是這樣。
??例如,考慮哈希表的情況。物理表示是包含鍵-值項的散列桶序列。條目所在的bucket 是其鍵的哈希碼的函數(shù),通常不能保證在各個實(shí)現(xiàn)中都是相同的。事實(shí)上,它甚至不能保證每次運(yùn)行都是相同的。因此,接受哈希表的默認(rèn)序列化形式會造成嚴(yán)重的錯誤。對哈希表進(jìn)行序列化和反序列化會產(chǎn)生一個不變量嚴(yán)重?fù)p壞的對象。
??無論您是否接受默認(rèn)的序列化表單,當(dāng)調(diào)用 defaultWriteObject 方法時,每個沒有標(biāo)記為 transient 的實(shí)例字段都將被序列化。因此,每個可以聲明為 transient 的實(shí)例字段都應(yīng)該是。這包括派生字段,它們的值可以從主要數(shù)據(jù)字段計算,比如緩存的哈希值。它還包括一些字段,這些字段的值綁定到 JVM 的一個特定運(yùn)行,例如表示本地數(shù)據(jù)結(jié)構(gòu)指針的長字段。在決定使字段非瞬態(tài)之前,請確信它的值是對象邏輯狀態(tài)的一部分。如果使用自定義序列化表單,那么大多數(shù)或所有實(shí)例字段都應(yīng)該標(biāo)記為transient,如上面的 StringList 示例所示。
??如果您使用默認(rèn)的序列化形式,并且已經(jīng)將一個或多個字段標(biāo)記為 transient,請記住,當(dāng)實(shí)例被反序列化時,這些字段將被初始化為默認(rèn)值:對象引用字段為 null,數(shù)值基元字段為零,布爾字段為false [JLS, 4.12.5]。如果這些值對于任何臨時字段都是不可接受的,那么必須提供一個 readObjectmethod 來調(diào)用 defaultReadObject 方法,然后將臨時字段恢復(fù)為可接受的值(item 88)?;蛘?,這些字段可以在第一次使用時惰性地初始化(item 83)。
??無論是否使用默認(rèn)的序列化形式,都必須在對象序列化上強(qiáng)加任何同步,而這種同步可能會強(qiáng)加在讀取對象的整個狀態(tài)的任何其他方法上。例如,如果你有一個線程安全的對象(item 82),它通過同步每個方法來實(shí)現(xiàn)線程安全,你選擇使用默認(rèn)的序列化形式,使用下面的 write-Object 方法:
// writeObject for synchronized class with default serialized form
private synchronized
void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
}
??如果您將同步放到 writeObject 方法中,您必須確保它與其他活動遵循相同的鎖順序約束,否則您將面臨資源順序死鎖的風(fēng)險[Goetz06, 10.1.5]。
??無論選擇哪種序列化形式,都要在編寫的每個可序列化類中聲明一個顯式的串行版本UID。這消除了串行版本 UID 作為不兼容性的潛在來源(item 86)。還有一個小小的性能好處。如果沒有提供串行版本 UID,則在運(yùn)行時執(zhí)行昂貴的計算來生成一個 UID。聲明一個串行版本的 UID 很簡單。只需將這一行添加到您的類:
private static final long serialVersionUID = randomLongValue;
??如果您編寫一個新類,為 randomLongValue 選擇什么值并不重要。您可以通過在類上運(yùn)行 serialver 實(shí)用程序來生成值,但也可以憑空選擇一個數(shù)字。串行版本 uid 不需要是唯一的。如果修改缺少串行版本 UID 的現(xiàn)有類,并且希望新版本接受現(xiàn)有串行化實(shí)例,則必須使用為舊版本自動生成的值。您可以通過在舊版本的類上運(yùn)行serialver 實(shí)用程序來獲得這個數(shù)字,舊版本的類即存在序列化實(shí)例的類。
??如果您希望創(chuàng)建與現(xiàn)有版本不兼容的類的新版本,只需更改串行版本 UID 聲明中的值。這將導(dǎo)致試圖反序列化以前版本的序列化實(shí)例時拋出 InvalidClassException 異常。不要更改串行版本 UID,除非您想破壞與一個類的所有現(xiàn)有序列化實(shí)例的兼容性。
??總而言之,如果您決定一個類應(yīng)該是可序列化的(item 86),請認(rèn)真考慮序列化的形式應(yīng)該是什么。只有當(dāng)該默認(rèn)序列化形式是對象邏輯狀態(tài)的合理描述時,才使用該默認(rèn)序列化形式;否則,設(shè)計一個適合描述對象的自定義序列化表單。您應(yīng)該分配與設(shè)計導(dǎo)出方法相同的時間來設(shè)計類的序列化形式(item 51)。就像你不能從以后的版本中刪除導(dǎo)出的方法一樣,你也不能從序列化的表單中刪除字段;它們必須被永久保存,以確保序列化的兼容性。選擇錯誤的序列化形式會對類的復(fù)雜性和性能產(chǎn)生永久性的負(fù)面影響。