第十一章迎变、序列化

本章關(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接口

  1. 要想使一個(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)也增加了啡邑。

  2. 實(shí)現(xiàn)Serializable接口提供了實(shí)在的好處:

    如果一個(gè)類將要加入到某個(gè)框架中贱勃,并且該框架依賴于序列化來實(shí)現(xiàn)對(duì)象傳輸或者持久化,對(duì)于這個(gè)類來說谤逼,實(shí)現(xiàn)Serializable接口就非常有必要贵扰。

  3. 為了繼承而設(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)可以被緩存)果漾。

  4. 內(nèi)部類(inner class)不應(yīng)該實(shí)現(xiàn)Serializable球切,它們使用編譯器產(chǎn)生的合成域來指向外圍實(shí)例的引用,以及保存來自外圍作用域的局部變量的值绒障。


第七十五條吨凑、考慮使用自定義的序列化形式

  1. 如果沒有先認(rèn)真考慮默認(rèn)的序列化形式是否合適,則不要貿(mào)然接受户辱。接受默認(rèn)的序列化形式是一個(gè)非常重要的決定鸵钝。需要從靈活性、性能和正確性多個(gè)角度對(duì)這種編碼形式進(jìn)行考察庐镐。

  2. 默認(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)系和安全性嚷炉。

  3. 當(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ì)引起棧溢出。
  4. 合理的序列化形式實(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());
                }
            }
        }
    
    
  5. 對(duì)于有些對(duì)象的約束關(guān)系要依賴于特定的實(shí)現(xiàn)細(xì)節(jié),用默認(rèn)的序列化形式會(huì)破壞其約束關(guān)系构诚。比如考慮散列表的情形蚌斩。

  6. 如果正在使用默認(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ù)為可接受的值砰盐。

  7. 無論是否使用默認(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方法

  1. 每當(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

  1. 之前講過的Singleton模式,一般這種類限制了構(gòu)造器的訪問扫茅,以確保永遠(yuǎn)只創(chuàng)建一個(gè)實(shí)例蹋嵌。但是,如果這種類的聲明加上了implements Serializable葫隙,就不再是Singleton栽烂。任何一個(gè)readObject方法,都會(huì)返回一個(gè)新建的實(shí)例恋脚,這個(gè)新建的實(shí)例不同于該類初始化時(shí)創(chuàng)建的實(shí)例腺办。

  2. 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的

  3. 如果將一個(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));
                }
            }
        }
    
    
  4. readResolve的可訪問性很重要,如果把它放在一個(gè)final類上汗销,就應(yīng)該是私有的犹褒,如果放在非final類上,就必須認(rèn)真考慮它的訪問性弛针。

  5. 總結(jié):應(yīng)該盡可能使用枚舉類型來實(shí)施實(shí)例控制的約束條件叠骑,如果做不到,同時(shí)有需要一個(gè)既可以序列化優(yōu)勢(shì)實(shí)例受控的類削茁,就必須提供一個(gè)readResolve方法宙枷,并確保該類的所有實(shí)例域都是基本類型或者是transient的。


第七十八條茧跋、考慮用序列化代理代替序列化實(shí)例

  1. 序列化代理模式(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);
            }
        }
    
    
  2. 這種代理模式的優(yōu)點(diǎn):不必太費(fèi)心思,不必顯式地執(zhí)行有效性檢查钝吮,不必知道哪些域可能會(huì)受到狡猾的序列化攻擊的危險(xiǎn)埋涧。

  3. 序列化代理模式的兩個(gè)局限:不能與可以被客戶端擴(kuò)展的類兼容,也不能與對(duì)象圖中包含循環(huán)的某些類兼容奇瘦;開銷增大棘催。

  4. 總結(jié):當(dāng)你發(fā)現(xiàn)自己必須在一個(gè)不能被客戶端擴(kuò)展的類上編寫readObject或者writeObject的時(shí)候,就應(yīng)該考慮使用序列化代理模式链患;想要穩(wěn)健地將帶有重要約束條件的對(duì)象序列化時(shí)巧鸭,這種模式可能是最容易的方法瓶您。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末麻捻,一起剝皮案震驚了整個(gè)濱河市纲仍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贸毕,老刑警劉巖郑叠,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異明棍,居然都是意外死亡乡革,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門摊腋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來沸版,“玉大人,你說我怎么就攤上這事兴蒸∈恿福” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵橙凳,是天一觀的道長蕾殴。 經(jīng)常有香客問我,道長岛啸,這世上最難降的妖魔是什么钓觉? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮坚踩,結(jié)果婚禮上荡灾,老公的妹妹穿的比我還像新娘。我一直安慰自己瞬铸,他們只是感情好卧晓,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赴捞,像睡著了一般逼裆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上赦政,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天胜宇,我揣著相機(jī)與錄音,去河邊找鬼恢着。 笑死桐愉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的掰派。 我是一名探鬼主播从诲,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼靡羡!你這毒婦竟也來了系洛?” 一聲冷哼從身側(cè)響起俊性,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎描扯,沒想到半個(gè)月后定页,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绽诚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年典徊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恩够。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡卒落,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜂桶,到底是詐尸還是另有隱情导绷,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布屎飘,位于F島的核電站妥曲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏钦购。R本人自食惡果不足惜檐盟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望押桃。 院中可真熱鬧葵萎,春花似錦、人聲如沸唱凯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磕昼。三九已至卷雕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間票从,已是汗流浹背漫雕。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留峰鄙,地道東北人浸间。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像吟榴,于是被迫代替她去往敵國和親魁蒜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容