對象序列化API,它提供了一個框架,用來將對象編碼成字節流,并從字節流編碼中重新構建對象。將一個對象編碼成一個字節流,稱作將該對象序列化,相反的處理過程稱作反序列化。一旦對象被序列化后,它的編碼就可以從一臺正在運行的虛擬機傳遞到另一臺虛擬機上,或者被存儲到磁盤上,供以后反序列化時用。序列化技術為遠程通信提供了標準和線路級對象表示法,也為javaBeans組件提供了標準和持久化數據格式。
要想使一個類的實例可被序列化,非常簡單,只要在它的聲明中加入”implements Serializable”字樣即可。正因為太容易了,所以普遍存在這樣一種誤解,認為程序員只需要做極少量的工作就可以支持序列化了。實際的情形要復雜得多。雖然使一個類可被序列化的直接開銷低到甚至可以忽略不計,但是為了序列化而付出的長期開銷往往是實實在在的。
為實現Serializable而付出的最大代價是,一旦一個類被發布,就大大降低了”改變這個類的實現”的靈活性。如果一個類實現了Serializable,它的字節流編碼(或者說序列化形式,serialized form)就變成了它的導出的API的一部分。一旦這個類被廣泛使用,往往必須永遠支持這種序列化形式,就好像你必須要支持導出的API的所有其他部分一樣。如果你不努力設計一個自定義的序列化形式(custom serialized form),而僅僅接受了默認的序列化形式,這種序列化形式將被永遠地束縛在該類最初的內部表示法上。換句話說,如果你接受了默認的序列化形式,這個類中私有的和包級私有的實例域將都變成導出的API的一部分,這不符合”最低限度地訪問域”的實踐準則,從而它就失去了作為信息隱藏工具的有效性。
如果你接受了默認的序列化形式,并且以后又要改變這個類的內部表示法,結果可能導致序列化形式的不兼容??蛻舳顺绦蚱髨D用這個類的舊版本來序列化一個類,然后用新版本進行反序列化,結果將導致程序失敗。在改變內部表示法的同時仍然維持原來的序列化形式(使用ObjectOutputStream.putFields和ObjectInputStream.readFields),這也是可能的,但是做起來比較困難,并且會在源代碼中留下一些可以明顯的隱患。因此,你應該仔細地設計一種高質量的序列化形式,并且在很長時間內都愿意使用這種形式。這樣做將會增加開發的初始成本,但這是值得的。設計良好的序列化形式也許會給類的演變帶來限制;但是設計不好的序列化形式則可能會使類根本無法演變。
序列化會使類的演變受到限制,這種限制的一個例子與流的唯一標識符(stream unique identifier)有關,通常它也被稱為序列版本UID(serial version UID)。每個可序列化的類都有一個唯一標識號與它相關聯。如果你沒有在一個名為serialVersionUID的私有靜態final的long域中顯式地指定該標識號,系統就會自動地將一個復雜的過程作用在這個類上,從而在運行時產生該標識號。這個自動產生的值會受到類名稱、它所實現的接口的名稱、以及所有公有的和受保護的成員的名稱所影響。如果你通過任何方式改變了這些信息,比如,增加了一個不是很重要的工具方法,自動產生的序列版本UID也會發生變化。因此,如果你沒有聲明一個顯式的序列版本UID,兼容性將會遭到破壞,在運行時導致InvalidClassException異常。
實現Serializable的第二個代價是,它增加了出現Bug和安全漏洞的可能性。通常情況下,對象是利用構造器來創建的;序列化機制是一種語言之外的對象創建機制(extralinguistic mechanism)。無論你是接受了默認的行為,還是覆蓋了默認的行為,反序列化機制(deserialization)都是一個”隱藏的構造器”,具備與其他構造器相同的特點。因為反序列化機制中沒有顯式的構造器,所以你很容易忘記要確保:反序列化過程必須也要保證所有”由構造器建立起來的約束關系”,并且不允許攻擊者訪問正在構造過程中的對象的內部信息。依靠默認的反序列化機制,可以很容易地使對象的約束關系遭到破壞,以及遭受到非法訪問。
實現Serializable的第三個代價是,隨著類發行新的版本,相關的測試負擔也增加了。當一個可序列化的類被修訂的時候,很重要的一點是,要檢查是否可以”在新版本中序列化一個實例,然后在舊版本中反序列化”,反之亦然。因此,測試所需要的工作量與”可序列化的類的數量和發行版本號”的乘積成正比,這個乘積可能會非常大。這些測試不可能自動構建,因為除了二進制兼容性(binary compatibility)以外,你還必須測試語義兼容性(semantic compatibility)。換句話說,你必須既要確?!毙蛄谢葱蛄谢边^程成功,也要確保結果產生的對象真正是原始對象的復制品??尚蛄谢惖淖兓酱?,它就越需要測試。如果在最初編寫一個類的時候,就精心設計了自定義的序列化形式,測試的要求就可以有所降低,但是也不能完全沒有測試。
實現Serializable接口并不是一個很輕松就可以做出的決定。它提供了一些實在的益處:如果一個類將要加入到某個框架中,并且該框架依賴于序列化來實現對象傳輸或者持久化,對于這個類來說,實現Serializable接口就非常有必要。更進一步來看,如果這個類要成為另一個類的一個組件,并且后者必須實現Serializable接口,若前者也實現了Serializable接口,它就會更易于被后者使用。然而,有許多實際的開銷都與實現Serializable接口有關。每當你實現一個類的時候,都需要權衡一下所付出的代價和帶來的好處。根據經驗,比如Date和BigInteger這樣的值類應該實現Serializable,大多數的集合類也應該如此。代表活動實體的類,比如線程池(thread pool),一般不應該實現Serializable。
為了繼承而設計的類應該很少實現Serializable,接口也應該很少會擴展它。如果違反了這條規則,擴展這個類或者實現這個接口的程序員就會背上沉重的負擔。然而在有些情況下違反這條規則卻是合適的。例如,如果一個類或者接口存在的目的主要是為了參與到某個框架中,該框架要求所有的參與者都必須實現Serializable,那么,對于這個類或者接口來說,實現或者擴展Serializable就是非常有意義的。
為了繼承而設計的類中真正實現了Serializable的有Throwable、Component和HttpServlet。因為Throwable實現了Serializable,所以RMI的異??梢詮姆掌鞫藗鞯娇蛻舳?。Component實現了Serializable,因此GUI可以被發送、保存和恢復。HttpServlet實現了Serializable,因此會話狀態可以被緩存。
如果一個專門為了繼承而設計的類不是可序列化的,就不可能編寫出可序列化的子類。特別是,如果超類沒有提供可訪問的無參構造器,子類也不可能做到可序列化。因此,對于為繼承而設計的不可序列化的類,你應該考慮提供一個無參構造器。
內部類不應該實現Serializable。它們使用編譯器產生的合成域來保存指向外圍實例的引用,以及保存來自外圍作用域的局部變量的值。因此,內部類的默認序列化形式是定義不清楚的。然而,靜態成員類卻是可以實現Serializable接口。
簡而言之,千萬不要認為實現Serializable接口會很容易。除非一個類在用了一段時間之后就會被拋棄,否則,實現Serializable接口就是個很嚴肅的承諾,必須認真對待。如果一個類是為了繼承而設計的,則吏加需要加倍小心。對于這樣的類而言,在“允許子類實現Serializable接口”或“禁止子類實現Serializable接口”兩者之間的一個折衷設計方案是,提供一個可訪問的無參構造器,這種設計方案允許(但不要求)子類實現Serializable接口。
設計一個類的序列化形式和設計該類的API 同樣重要,因此在沒有認真考慮好默認的序列化形式是否合適之前,不要貿然使用默認的序列化行為。在作出決定之前,你需要從靈活性、性能和正確性多個角度對這種編碼形式進行考察。一般來講,只有當你自行設計的自定義序列化形式與默認的形式基本相同時,才能接受默認的序列化形式。
比如,當一個對象的物理表示法等同于它的邏輯內容,可能就適合于使用默認的序列化形式。
見如下代碼示例:
public class Name implements Serializable { PRivate final String lastName; private final String firstName; private final String middleName; ... ... }從邏輯角度而言,該類的三個域字段精確的反應出它的邏輯內容。然而有的時候,即便默認的序列化形式是合適的,通常還必須提供一個readObject 方法以保證約束關系和安全性,如上例代碼中,firstName 和lastName 不能為null 等。
使用默認序列化形式會有以下幾個缺點: (1)它使這個類的導出API 永遠的束縛在該類的內部表示法上,即使今后找到更好的的實現方式,也無法擺脫原有的實現方式。 (2)它會消耗過多的空間。 (3)它會消耗過多的時間。 (4)它會引起棧溢出。
transient是Java語言的關鍵字,用來表示一個域不是該對象串行化的一部分。當一個對象被串行化的時候,transient型變量的值不包括在串行化的表示中,然而非transient型的變量是被包括進去的。
在序列化過程中,虛擬機會試圖調用對象類里的writeObject() 和readObject(),進行用戶自定義的序列化和反序列化,如果沒有則調用ObjectOutputStream.defaultWriteObject() 和ObjectInputStream.defaultReadObject()。
同樣,在ObjectOutputStream和ObjectInputStream中最重要的方法也是writeObject() 和 readObject(),遞歸地寫出/讀入byte。
所以用戶可以通過writeObject()和 readObject()自定義序列化和反序列化邏輯。對一些敏感信息加密的邏輯也可以放在此。
對于默認序列化還需要進一步說明的是,當一個或多個域字段被標記為transient 時,如果要進行反序列化,這些域字段都將被初始化為其類型默認值,如對象引用域被置為null,數值基本域的默認值為0,boolean域的默認值為false。如果這些值不能被任何transient 域所接受,你就必須提供一個readObject方法。它首先調用defaultReadObject,然后再把這些transient 域恢復為可接受的值。
最后需要說明的是,無論你是否使用默認的序列化形式,如果在讀取整個對象狀態的任何其他方法上強制任何同步,則也必須在對象序列化上強制這種同步。
無論你選擇了哪種序列化形式,都要為自己編寫的每個可序列化的類聲明一個顯式的序列版本UID。這樣可以避免序列版本UID成為潛在的不兼容根源,同時也會帶來小小的性能好處,因為不需要去算序列版本UID。
對于非final 的可序列化類,在readObject 方法和構造器之間還有其他類似的地方,readObject方法不可以調用可被覆蓋的方法,無論是直接調用還是間接調都不可以。如果違反了該規則,并且覆蓋了該方法,被覆蓋的方法將在子類的狀態被反序列化之前先運行。程序很可能會失敗。
總而言之,每當你編寫readObject方法的時候,都要這樣想:你正在編寫一個公有的構造器,無論給它傳遞什么樣的字節流,它都必須產生一個有效的實例。不要假設這個字節流一定代表著一個真正被序列化過的實例。雖然在本條目的例子中,類使用了默認的序列化形式,但是,所有討論到的有可能發生的問題也同樣適用于使用自定義序列化形式的類。下面以摘要的形式給出一些指導方針,有助于編寫出更加健壯的readObject方法:
對于對象引用域必須保持為私有的類,要保護性地拷貝這些域中的每個對象。不可變類的可變組件就屬于這一類別。對于任何約束條件,如果檢查失敗,則拋出一個InvalidObjectException異常。這些檢查動作應該跟在所有的保護性拷貝之后。如果整個對象圖在被反序列化之后必須進行驗證,就應該使用ObjectInputValidation接口[JavaSE6,Serialization]。無論是直接方式還是間接方式,都不要調用類中任何可被覆蓋的方法。如果這個類的聲明中加上了”implements Serializable”的字樣,它就不再是一個Singleton。無論該類使用了默認的序列化形式,還是自定義的序列化形式,都沒有關系;也跟它是否提供了顯式的readObject方法無關。任何一個readObject方法,不管是顯式的還是默認的,它都會返回一個新建的實例,這個新建的實例不同于該類初始化時創建的實例。
readResolve特性允許你用readObject創建的實例代替另一個實例[Serialization, 3.7]。對于一個正在被反序列化的對象,如果它的類定義了一個readResolve方法,并且具備正確的聲明,那么在反序列化之后,新建對象上的readResolve方法就會被調用。然后,該方法返回的對象引用將被返回,取代新建的對象。在這個特性的絕大多數用法中,指向新建對象的引用不需要再被保留,因此立即成為垃圾回收的對象。
如果Elvis類要實現Serializable接口,下面的readResolve方法就足以保證它的Singleton屬性:
// readResolve for instance control - you can do better!private Object readResolve() {// Return the one true Elvis and let the garbage collector// take care of the Elvis impersonator.return INSTANCE;}該方法忽略了被反序列化的對象,只返回該類初始化時創建的那個特殊的Elvis實例。因此,Elvis實例的序列化形式并不需要包含任何實際的數據;所有的實例域都應該被聲明為transient的。事實上,如果依賴readResolve進行實例控制,帶有對象引用類型的所有實例域則都必須聲明為transient的。
如果反過來,你將一個可序列化的實例受控的類編寫成枚舉,就可以絕對保證除了所聲明的常量之外,不會有別的實例。JVM對此提供了保障,這一點你可以確信無疑。
用readResolve進行實例控制并不過時。如果必須編寫可序列化的實例受控的類,它的實例在編譯時還不知道,你就無法將類表示成一個枚舉類型。
readResolve的可訪問性(accessibility)很重要。如果把readResolve方法放在一個final類上,它就應該是私有的。如果把readResolver方法放在一個非final的類上,就必須認真考慮它的可訪問性。如果它是私有的,就不適用于任何子類。如果它是包級私有的,就只適用于同一個包中的子類。如果它是受保護的或者公有的,就適用于所有沒有覆蓋它的子類。如果readResolve方法是受保護的或者公有的,并且子類沒有覆蓋它,對序列化過的子類實例進行反序列化,就會產生一個超類實例,這樣有可能導致ClassCastException異常。
總而言之,你應該盡可能地使用枚舉類型來實施實例控制的約束條件。如果做不到,同時又需要一個既可序列化又是實例受控的類,就必須提供一個readResolver方法,并確保該類的所有實例域都為基本類型,或者是瞬時的。
序列化代理模式相當簡單。首先,為可序列化的類設計一個私有的靜態嵌套類,精確地表示外圍類的實例的邏輯狀態。這個嵌套類被稱作序列化代理(serialization proxy),它應該有一個單獨的構造器,其參數類型就是那個外圍類。這個構造器只從它的參數中復制數據:它不需要進行任何一致性檢查或者保護性拷貝。按設計,序列代理的默認序列化形式是外圍類最好的序列化形式。外圍類及其序列代理都必須聲明實現Serializable接口。
具體demo可以參考jdk中EnumSet代碼,以下是該類代碼的簡化版:
public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E> implements Cloneable, java.io.Serializable{ /** * The class of all the elements of this set. */ final Class<E> elementType; /** * All of the values comprising T. (Cached for performance.) */ final Enum[] universe; private static Enum[] ZERO_LENGTH_ENUM_ARRAY = new Enum[0]; EnumSet(Class<E>elementType, Enum[] universe) { this.elementType = elementType; this.universe = universe; } //N多方法代碼已省略 /** * This class is used to serialize all EnumSet instances, regardless of * implementation type. It captures their "logical contents" and they * are reconstructed using public static factories. This is necessary * to ensure that the existence of a particular implementation type is * an implementation detail. * * @serial include */ private static class SerializationProxy <E extends Enum<E>> implements java.io.Serializable { /** * The element type of this enum set. * * @serial */ private final Class<E> elementType; /** * The elements contained in this enum set. * * @serial */ private final Enum[] elements; SerializationProxy(EnumSet<E> set) { elementType = set.elementType; elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY); } private Object readResolve() { EnumSet<E> result = EnumSet.noneOf(elementType); for (Enum e : elements) result.add((E)e); return result; } private static final long serialVersionUID = 362491234563181265L; } Object writeReplace() { return new SerializationProxy<>(this); } // readObject method for the serialization proxy pattern // See Effective Java, Second Ed., Item 78. private void readObject(java.io.ObjectInputStream stream) throws java.io.InvalidObjectException { throw new java.io.InvalidObjectException("Proxy required"); }}序列化代理模式有兩個局限性:
它不能與可以被客戶端擴展的類兼容。它也不能與對象圖中包含循環的某些類兼容:如果你企圖從一個對象的序列化代理的readResolve方法內部調用這個對象中的方法,就會得到一個ClassCastException異常,因為你還沒有這個對象,只有它的序列化代理。
最后,序列化代理模式所增強的功能和安全性并不是沒有代價的。在我的機器上,通過序列化代理來序列化和反序列化Period實例的開銷,比用保護性拷貝進行的開銷增加了14%。
總而言之,每當你發現自己必須在一個不能被客戶端擴展的類上編寫readObject或者writeObject方法的時候,就應該考慮使用序列化代理模式。要想穩健地將帶有重要約束條件的對象序列化時,這種模式可能是最容易的方法。
《Effective Java中文版 第2版》PDF版下載: http://download.csdn.net/detail/xunzaosiyecao/9745699
作者:jiankunking 出處:http://blog.csdn.net/jiankunking
新聞熱點
疑難解答