本章關(guān)注對(duì)象序列化API蓖乘,它提供了一個(gè)框架,用來將對(duì)象編碼成字節(jié)流峦睡,并從字節(jié)流編碼中重新構(gòu)建對(duì)象翎苫。 相反的處理過程是反序列化deserializing。一旦對(duì)象被序列化后榨了,它的編碼就可以從一臺(tái)正在運(yùn)行的虛擬機(jī)被傳遞到另一臺(tái)虛擬機(jī)上煎谍,或者被存儲(chǔ)到磁盤上,供以后反序列化時(shí)用龙屉。序列化技術(shù)為遠(yuǎn)程通信提供了標(biāo)準(zhǔn)的線路級(jí)對(duì)象表示法呐粘,也為JavaBean組件結(jié)構(gòu)提供了標(biāo)準(zhǔn)的持久化數(shù)據(jù)格式。
第七十四條转捕、謹(jǐn)慎地實(shí)現(xiàn)Serializable接口
-
要想使一個(gè)類的實(shí)例可以被序列化作岖,只要在它的聲明中加入
implements Serializable
字樣即可,但實(shí)際情況可能要復(fù)雜得多五芝,雖然使一個(gè)類可被實(shí)例化的直接開銷非常低痘儡,但是為了序列化而付出的長期開銷往往是實(shí)實(shí)在在的。
實(shí)現(xiàn)Serializable接口的代價(jià):-
最大的代價(jià)是:一旦一個(gè)類被發(fā)布与柑,就大大降低了“改變這個(gè)類的實(shí)現(xiàn)”的靈活性谤辜。
如果一個(gè)類實(shí)現(xiàn)了Serializable接口蓄坏,它的字節(jié)流編碼就變成了它導(dǎo)出的API的一部分价捧,一旦這個(gè)類被廣泛使用,往往永遠(yuǎn)支持這個(gè)序列化形式涡戳。如果你接受了默認(rèn)的序列化形式结蟋,并且以后又要修改這個(gè)類的內(nèi)部表示法,可能會(huì)導(dǎo)致序列化形式的不兼容渔彰。
序列化會(huì)使類的演變受到限制嵌屎,這種限制的一個(gè)例子與流的唯一標(biāo)識(shí)符有關(guān),通常它也被稱為序列版本UID(serialVersionUID)恍涂,每個(gè)可序列化的類都有一個(gè)唯一標(biāo)識(shí)號(hào)與它相關(guān)聯(lián)宝惰。如果你沒有顯式地指定該標(biāo)識(shí)號(hào)(
private static final long serialVersionUID = ...
),系統(tǒng)就會(huì)自動(dòng)地在運(yùn)行時(shí)產(chǎn)生該標(biāo)識(shí)號(hào)再沧。這個(gè)自動(dòng)產(chǎn)生的值會(huì)受到類名稱尼夺、它實(shí)現(xiàn)的接口名稱、以及所有的公有和受保護(hù)的成員的名稱所影響。因此淤堵,如果你沒有聲明一個(gè)顯式的序列版本UID寝衫,兼容性會(huì)遭到破壞。 第二個(gè)代價(jià)是:它增加了出現(xiàn)Bug和安全漏洞的可能性拐邪。通常情況下慰毅,對(duì)象是通過構(gòu)造器來創(chuàng)建的;序列化機(jī)制是一種語言之外的對(duì)象創(chuàng)建機(jī)制扎阶。反序列化機(jī)制是一個(gè)隱藏的構(gòu)造器汹胃,依靠默認(rèn)的反序列化機(jī)制,很容易使對(duì)象的約束關(guān)系遭到破壞乘陪,以及遭受非法訪問统台。
第三個(gè)代價(jià):隨著類發(fā)行新的版本,相關(guān)的測試負(fù)擔(dān)也增加了啡邑。
-
-
實(shí)現(xiàn)Serializable接口提供了實(shí)在的好處:
如果一個(gè)類將要加入到某個(gè)框架中贱勃,并且該框架依賴于序列化來實(shí)現(xiàn)對(duì)象傳輸或者持久化,對(duì)于這個(gè)類來說谤逼,實(shí)現(xiàn)Serializable接口就非常有必要贵扰。
為了繼承而設(shè)計(jì)的類應(yīng)該盡可能少地去實(shí)現(xiàn)Serializable接口,用戶的接口也應(yīng)該盡可能少地實(shí)現(xiàn)流部。如果違反了這條規(guī)則戚绕,擴(kuò)展這個(gè)類或者實(shí)現(xiàn)這個(gè)接口的程序員就會(huì)背上沉重的負(fù)擔(dān)。但是有例外:Throwable(RMI的異持剑可以從服務(wù)器端傳到客戶端)類舞丛,Component(GUI可以被發(fā)送、保存和恢復(fù))和HttpServlet抽象類(會(huì)話狀態(tài)可以被緩存)果漾。
內(nèi)部類(inner class)不應(yīng)該實(shí)現(xiàn)Serializable球切,它們使用編譯器產(chǎn)生的合成域來指向外圍實(shí)例的引用,以及保存來自外圍作用域的局部變量的值绒障。
第七十五條吨凑、考慮使用自定義的序列化形式
如果沒有先認(rèn)真考慮默認(rèn)的序列化形式是否合適,則不要貿(mào)然接受户辱。接受默認(rèn)的序列化形式是一個(gè)非常重要的決定鸵钝。需要從靈活性、性能和正確性多個(gè)角度對(duì)這種編碼形式進(jìn)行考察庐镐。
默認(rèn)的序列化形式描述了該對(duì)象內(nèi)部所包含的數(shù)據(jù)恩商,以及每一個(gè)可以從這個(gè)對(duì)象到達(dá)的其他對(duì)象的內(nèi)部數(shù)據(jù)。它也描述了所有這些對(duì)象被鏈接起來后的拓?fù)浣Y(jié)構(gòu)必逆。對(duì)于一個(gè)對(duì)象來說怠堪,理想的序列化形式應(yīng)該只包含該對(duì)象所表示的邏輯數(shù)據(jù)韧献,而邏輯數(shù)據(jù)與物理表示法應(yīng)該是各自獨(dú)立的。如果一個(gè)對(duì)象的物理表示法等同于它的邏輯內(nèi)容研叫,可能就適合于使用默認(rèn)的序列化形式锤窑。即使你確定了默認(rèn)的序列化形式是合適的,通常還必須提供一個(gè)readObject方法以保證約束關(guān)系和安全性嚷炉。
-
當(dāng)一個(gè)對(duì)象的物理表示法與它的邏輯數(shù)據(jù)內(nèi)容有實(shí)質(zhì)性區(qū)別的時(shí)候渊啰,使用默認(rèn)的序列化形式會(huì)有以下四個(gè)缺點(diǎn):
- 它使這個(gè)類的導(dǎo)出API永遠(yuǎn)地束縛在該類的內(nèi)部表示法上;
- 它會(huì)消耗過多的空間申屹;
- 它會(huì)消耗過多的時(shí)間绘证;
- 它會(huì)引起棧溢出。
-
合理的序列化形式實(shí)例:
writeObject方法的首要任務(wù)是調(diào)用defaultWriteObject,readObject的首要方法是調(diào)用defaultReadObject哗讥。無論你是否使用默認(rèn)的序列化形式嚷那,當(dāng)defaultWriteObject方法被調(diào)用的時(shí)候,每個(gè)未被標(biāo)記為transient的實(shí)例域都會(huì)被序列化杆煞。在決定將一個(gè)域做成非transient的之前魏宽,請(qǐng)一定要確信它的值將是該對(duì)象邏輯狀態(tài)的一部分。如果要自定義序列化形式决乎,大多數(shù)或者所有的實(shí)例域都應(yīng)該標(biāo)記為transient队询。
import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * Created by laneruan on 2017/8/3. * 一個(gè)自定義的序列化形式實(shí)例 */ public class StringListSerialization implements Serializable { private transient int size = 0; //瞬時(shí)的 private transient Entry head = null; private static class Entry{ String data; Entry next; Entry previous; } public final void add(String s){ } private void writeObject(ObjectOutputStream s) throws IOException{ s.defaultWriteObject(); s.writeInt(size); 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(); for(int i = 0;i<numElements;i++){ add((String)s.readObject()); } } }
對(duì)于有些對(duì)象的約束關(guān)系要依賴于特定的實(shí)現(xiàn)細(xì)節(jié),用默認(rèn)的序列化形式會(huì)破壞其約束關(guān)系构诚。比如考慮散列表的情形蚌斩。
-
如果正在使用默認(rèn)的序列化形式,并且把一個(gè)或者多個(gè)域標(biāo)記為transient范嘱,則需要記姿蜕拧:當(dāng)一個(gè)實(shí)例被反序列化的時(shí)候,這些域?qū)⒈怀跏蓟癁樗鼈兊哪J(rèn)值(default value):
對(duì)于對(duì)象引用域丑蛤,默認(rèn)值為null叠聋。
對(duì)于數(shù)值基本域,默認(rèn)值為0盏阶。
對(duì)于boolean域晒奕,默認(rèn)值為false闻书。
如果這些值不能被任何transient域所接受名斟,則必須要提供readObject方法,它首先調(diào)用defaultreadObject方法魄眉,再把這些transient域恢復(fù)為可接受的值砰盐。 -
無論是否使用默認(rèn)形式,都需要注意:
-
如果在讀取整個(gè)對(duì)象狀態(tài)的任何其他方法上強(qiáng)制任何同步坑律,則也必須在對(duì)象序列化上強(qiáng)制這種同步岩梳。如果把同步放在writeObject方法上囊骤,就必須確保它遵守與其他動(dòng)作相同的鎖排列約束條件。
private synchronized void writeObject(ObjectOutputStream s) throws IOException{ s.defaultWriteObject(); }
要為自己編寫的每個(gè)可序列的類聲明一個(gè)顯式的序列版本UID冀值。這樣可以避免序列版本UID成為潛在的不兼容根源也物,且?guī)硇⌒〉男阅軆?yōu)勢(shì)。
private static final long serialVersionUID = randomLongValue;
-
第七十六條列疗、保護(hù)性地編寫readObject方法
- 每當(dāng)你編寫readObject方法的時(shí)候滑蚯,都要這樣想:你正在編寫一個(gè)公有的構(gòu)造器,無論給它傳遞什么樣的字節(jié)流抵栈,它都必須產(chǎn)生一個(gè)有效的實(shí)例告材,不要假設(shè)這個(gè)字節(jié)流一定代表著一個(gè)真正被序列化過的實(shí)例,下面給出一些指導(dǎo)性建議古劲,有助于編寫出更加健壯的readObject方法:
- 對(duì)于對(duì)象引用域必須保持為私有的域斥赋,要保護(hù)性地拷貝這個(gè)域中的每個(gè)對(duì)象,不可變類的可變組件就屬于這一類別产艾;
- 對(duì)于任何約束條件疤剑,如果檢查失敗,則拋出一個(gè)InvalidObjectException闷堡,這些檢查動(dòng)作應(yīng)該跟在所有的保護(hù)性拷貝后面骚露;
- 如果整個(gè)對(duì)象圖在被反序列化之后必須進(jìn)行驗(yàn)證,就應(yīng)該使用ObjectInputValidation接口缚窿;
- 無論是直接還是間接方式棘幸,都不要調(diào)用類中任何可被覆蓋的方法。
第七十七條倦零、對(duì)于實(shí)例控制误续,枚舉類型優(yōu)先于readResolve
之前講過的Singleton模式,一般這種類限制了構(gòu)造器的訪問扫茅,以確保永遠(yuǎn)只創(chuàng)建一個(gè)實(shí)例蹋嵌。但是,如果這種類的聲明加上了
implements Serializable
葫隙,就不再是Singleton栽烂。任何一個(gè)readObject方法,都會(huì)返回一個(gè)新建的實(shí)例恋脚,這個(gè)新建的實(shí)例不同于該類初始化時(shí)創(chuàng)建的實(shí)例腺办。readSolve特性允許你用readObject創(chuàng)建的實(shí)例代替另一個(gè)實(shí)例,對(duì)于一個(gè)正在被反序列化的對(duì)象糟描,如果它的類定義了一個(gè)readSolve方法怀喉,并且具備正確的聲明,那么反序列化之后船响,新建對(duì)象上的readSolve方法就會(huì)被調(diào)用躬拢,然后躲履,該方法返回的對(duì)象引用將被返回,取代新建的對(duì)象聊闯。如果依賴readSolve進(jìn)行實(shí)例控制工猜,帶有引用類型的所有實(shí)例域則都必須聲明為transient的。
-
如果將一個(gè)可序列化的實(shí)例受控類編寫成枚舉菱蔬,就可以絕對(duì)保證除了所聲明的常量之外域慷,不會(huì)有別的實(shí)例(JVM保證的)
import java.io.Serializable; import java.util.Arrays; /** * Created by laneruan on 2017/8/3. */ public class singletonReadSolve implements Serializable { public static final singletonReadSolve INSTANCE = new singletonReadSolve(); private singletonReadSolve(){} //這個(gè)方法足以保證Singleton屬性 private String[] favoriteSongs = {"Hound dog","Heartbreak Hotel"}; public void printFavorites(){ System.out.println(Arrays.toString(favoriteSongs)); } private Object readResolve(){ return INSTANCE; } //用enum類型進(jìn)行實(shí)例控制 public enum singletonEnum{ INSTANCE; private String[] favoriteSongs = {"Hound dog","Heartbreak Hotel"}; public void printFavorites(){ System.out.println(Arrays.toString(favoriteSongs)); } } }
readResolve的可訪問性很重要,如果把它放在一個(gè)final類上汗销,就應(yīng)該是私有的犹褒,如果放在非final類上,就必須認(rèn)真考慮它的訪問性弛针。
總結(jié):應(yīng)該盡可能使用枚舉類型來實(shí)施實(shí)例控制的約束條件叠骑,如果做不到,同時(shí)有需要一個(gè)既可以序列化優(yōu)勢(shì)實(shí)例受控的類削茁,就必須提供一個(gè)readResolve方法宙枷,并確保該類的所有實(shí)例域都是基本類型或者是transient的。
第七十八條茧跋、考慮用序列化代理代替序列化實(shí)例
-
序列化代理模式(serialization proxy pattern):首先慰丛,為可序列化的類設(shè)計(jì)一個(gè)私有的靜態(tài)嵌套類,精確地表示外圍類的實(shí)例的邏輯狀態(tài)瘾杭。這個(gè)嵌套類稱為序列化代理诅病,它有一個(gè)單獨(dú)的構(gòu)造器,其參數(shù)就是那個(gè)外圍類粥烁,這個(gè)構(gòu)造器只從它的參數(shù)中復(fù)制數(shù)據(jù)贤笆。外圍類和其序列代理都必須聲明實(shí)現(xiàn)Serializable接口。
import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Date; /** * Created by laneruan on 2017/8/3. * */ //代理類SerializationProxy public class SerializationProxy implements Serializable{ private final Date start; private final Date end; SerializationProxy(Period p){ this.start = p.start(); this.end = p.end(); } private static final long serialVersionUID = 234038490L; private void readObject(ObjectInputStream stream) throws InvalidObjectException{ throw new InvalidObjectException("Proxy required"); } //它返回一個(gè)邏輯上的外圍類實(shí)例讨阻,這是該模式的魅力所在 // 導(dǎo)致序列化系統(tǒng)在反序列化時(shí)將序列化代理類轉(zhuǎn)變回外圍類的實(shí)例 private Object readResolve(){ return new Period(start,end); } } //外圍類Period class Period implements Serializable{ private final Date start; private final Date end; public Period(Date start,Date end){ this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if(this.start.compareTo(this.end)>0){ throw new IllegalArgumentException(start + "after" + end); } } public Date start(){return new Date(start.getTime());} public Date end(){return new Date(end.getTime());} public String toString(){ return start + "-" + end; } //有了這個(gè)方法芥永,序列化系統(tǒng)永遠(yuǎn)不會(huì)產(chǎn)生外圍類的序列化實(shí)例 private Object writeReplace(){ return new SerializationProxy(this); } }
這種代理模式的優(yōu)點(diǎn):不必太費(fèi)心思,不必顯式地執(zhí)行有效性檢查钝吮,不必知道哪些域可能會(huì)受到狡猾的序列化攻擊的危險(xiǎn)埋涧。
序列化代理模式的兩個(gè)局限:不能與可以被客戶端擴(kuò)展的類兼容,也不能與對(duì)象圖中包含循環(huán)的某些類兼容奇瘦;開銷增大棘催。
總結(jié):當(dāng)你發(fā)現(xiàn)自己必須在一個(gè)不能被客戶端擴(kuò)展的類上編寫readObject或者writeObject的時(shí)候,就應(yīng)該考慮使用序列化代理模式链患;想要穩(wěn)健地將帶有重要約束條件的對(duì)象序列化時(shí)巧鸭,這種模式可能是最容易的方法瓶您。