Effective java筆記(十),序列化

將一個對象編碼成字節流稱作將該對象「序列化」。相反,從字節流編碼中重新構建對象被稱作「反序列化」。一旦對象被「序列化」后,它的編碼就可以從一臺虛擬機傳遞到另一臺虛擬機,或被存儲到磁盤上,供以后「反序列化」使用。序列化技術為JavaBean組件結構提供了標準的持久化數據格式。

74、謹慎的實現Serializable接口

一個類實現Serializable接口需要付出的代價:

  • 一旦一個類被發布,就大大降低了「改變這個類的實現」的靈活性。若一個類實現了Serializable接口,它就成了這個類導出API的一部分。
  • 增加了出現Bug和安全漏洞的可能性。序列化機制是一種語言之外的對象創建機制,反序列化是一個「隱藏的構造器」,具備與其他構造器相同的特點。因此,反序列化過程必須要保證所有的約束關系。
  • 隨著發行新的版本,相關的測試負擔也增加了。

每個可序列化的類都有一個唯一的名為serialVersionUID的標識號與它相關聯。若類在私有的靜態final的long域中沒有顯式的指定這個標識號,系統就會自動的為該類產生一個標識號,這時類的兼容性將會遭到破壞,在運行時導致InvalidClassException異常。

為了繼承而設計的類應該盡可能少的去實現Serializable接口,用戶自定義的接口也應該盡可能少的繼承Serializable接口。例外,Throwable、Component和HttpServlet抽象類。

內部類不應該實現Serializable即可。靜態成員類可以實現Serializable接口。

75、考慮使用自定義的序列化形式

對于一個對象來說,理想的序列化形式應該只包含該對象所表示的邏輯數據,而邏輯數據與物理表示法(存儲結構)應該是獨立的。如果一個對象的物理表示法等同于它的邏輯內容,就適用于使用默認的序列化形式。如:

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;

    private final String middleName;

    ....
}

在這段代碼中,Name類的實例域精確的反應了它的邏輯內容,可以使用默認的序列化形式。注意:雖然lastName、firstName和middleName域是私有的,但它們仍然需要有注釋文檔。因為,這些私有域定義了一個公有的API,即這個類的序列化形式。@serial標簽用來告知Javadoc工具,把這些文檔信息放在有關序列化形式的特殊文檔頁中。

當一個對象的物理表示法與它的邏輯內容之間有實質性的不同時,使用默認序列化形式有如下缺點:

  • 它將這個類的導出API永遠束縛在了該類的內部表示法上。如,私有內部類變成公有API的一部分。
  • 會消耗過多的空間和時間
  • 會引起棧溢出
  • 其約束關系可能遭到嚴重破壞,如散列表

如:

//默認序列化形式
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;
    }
    ....
}

自定義序列化:


public final class StringList implements Serializable {

    private static final long serialVersionUID = ...;
    private transient int size = 0; //不會被序列化
    private transient Entry head = null;

    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }
    
    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);
        for(Entry e = head; e != null; e = e.next ) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) 
        throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int num = s.readInt();
        for(int i=0; i < num; i++) {
            add((String)s.readObject());
        }
    }
    .....
}

注意:盡管StringList的所有域都是transient,但writeObject和readObject的首要任務仍是調用defaultXxxObject方法,這樣可以極大的增強靈活性。另外盡管writeObject是私有的,仍然需要文檔注釋。

無論自定義序列化還是默認序列化,對于一個線程安全的對象,必須在序列化方法上強制同步。如:

private synchronized void writeObject(ObjectOutputStream s) 
    throws IOException {
    s.defaultWriteObject();
}

總之,當要將一個類序列化時,應該仔細考慮采用默認序列化還是自定義序列化。選擇錯誤的序列化形式對于一個類的復雜性和性能都會有永久的負面影響。

76、保護性編寫readObject方法

readObject方法實際上相當于一個公有的構造器,如同其它構造器一樣,readObject方法必須檢查其參數的有效性,并且在必要的時候進行保護性拷貝。readObject是一個「用字節流作為唯一參數」的構造器,當面對一個人工仿造的字節流時,readObject產生的對象可能會違反它所屬類的約束條件,所以必須在readObject中增加約束性檢查,若有效性檢查失敗,拋出InvalidObjectException異常。如:

public final class Period {
    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 getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }

    //反序列化時增加約束條件
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        if(start.compareTo(end) > 0) {
            throw new InvalidObjectException(start + " after " + end);
        }
    }
}

在這段代碼中,盡管readObject中增加了有效性檢查,但通過偽造字節流創建可變的Period實例仍是可能的。做法是:字節流以Period實例開頭,然后附加上兩個額外的引用執行Period實例中兩個私有的Date域。攻擊者從ObjectInputStream中讀取Period實例,然后讀取其后的「惡意引用」,通過這個引用攻擊者就可以修改Period中私有的Date域。如:

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
Period period = new Period(new Date(), new Date());
out.writeObject(period);
byte[] ref = {0x71, 0, 0x7e, 0, 5}; //指向period中私有域start的字節
bos.write(ref);
ref[4] = 4; //指向period中私有域end的字節
bos.write(ref);

//反序列化
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Period period1 = (Period)in.readObject();
//ref1指向period1中私有域start指向的對象,可通過這個引用修改不可變對象
Date ref1 = (Date)in.readObject(); 
Date ref2 = (Date)in.readObject();

因此,對于每個可序列化的不可變類,若它包含了私有的可變組件(對象的引用),那么在它的readObject方法中,必須對這些組件進行保護性拷貝。否則,它內部的約束條件可能遭受破壞。如:

private void readObject(ObjectInputStream s) {
    s.defaultReadObject();
    start = new Date(start.getTime());
    end = new Date(end.getTime());
    if(start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

注意:final域必須在對象構造時初始化,為了使用readObject方法,必須將start和end域做成非final的。

編寫readObject方法的指導原則:

  • 對于對象引用域必須保持為私有的類,要保護性的拷貝這些域中的每個對象。
  • 對于任何約束條件,若檢查失敗,則拋出一個InvalidObjectException異常。檢查應在保護性拷貝之后。
  • 無論直接方式還是間接方式,都不要調用類中任何可被覆蓋的方法,否則反序列時可能會失敗。

77、對于實例控制,枚舉類型優先于readResolve

public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { } 
    ....
}

如上所示,若這個Singleton類的聲明上加上「implements Serializable」,它就不再是一個Singleton。無論該類使用的是默認的序列化形式還是自定義的序列化形式。因為任何一個readObject方法,它都會返回一個新建的實例

對于一個正在被反序列化的對象,若它的類定義了一個readResolve方法,那么在反序列化后,新建對象上的readResolve方法就會被調用。然后該方法返回的對象引用將被返回,取代新建的對象,而新建的對象將被垃圾回收。

public class Elvis implements Serializable {
    public static final transient Elvis INSTANCE = new Elvis();
    private Elvis() { } 
    ....

    private Object readResolve() {
        //Return the one true Elvis
        return INSTANCE;
    }
}

該方法忽略了被反序列化的對象,只返回該類初始化時創建的Elvis實例。若依賴readResolve進行實例控制,帶有對象引用類型的所有實例域則都必須聲明為transient的。,否則能被人工仿造的字節流攻擊。靜態成員不屬于對象,不參與序列化。

通過將一個可序列化的實例受控的類編寫成枚舉,可以絕對保證除了所聲明的常量外,不會有別的實例。如:

public enum Elvis {
    INSTANCE;
    ....
}

另外,readResolve的可訪問性很重要。若把readResolve方法放在一個final類上,它就應該是私有的。若readResolve方法是受保護的或共有的,并且子類沒有覆蓋它,對序列化過的子類實例進行反序列化,就會產生一個超類實例,這可能導致ClassCastException異常。

總之,應該盡可能的使用枚舉類型來實施實例控制的約束條件,若做不到,就必須提供一個readResolve方法,并將引用類型的域聲明為transient的。

78、考慮用序列化代理代替序列化實例

序列化代理模式能夠極大的減少實現Serializable接口所帶來的風險。

實現序列化代理模式的步驟:

  • 首先為可序列化的類設計一個私有的靜態嵌套類,精確的表示外圍類實例的邏輯狀態。它有一個單獨的構造器,其參數類型為外圍類。外圍類及其序列化代理都必須實現Serializable接口。
  • 將writeReplace方法添加到外圍類中。
  • 在SerializableProxy類中提供readResolve方法,它返回邏輯上相等的外圍類的實例。

如:

//外圍類不需要serialVersionUID
public final 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 getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }

    //在序列化之前,將外圍類的實例轉變成它的序列化代理
    private Object writeReplace(){
        return new SerializationProxy(this);
    }

    //防止被攻擊者使用
    private void readObject(ObjectInputStream stream) 
        throws InvalidObjectException{
        throw new InvalidObjectException("Proxy required");
    }

    private static class SerializationProxy implements Serializable {
        private static final long serialVersionUID = ...;
        private final Date start;
        private final Date end;

        SerializationProxy(Period p) {
            this.start = p.start;
            this.end = p.end;
        }

        private Object readResolve() {
            return new Period(start, end);
        }
    }
}

正如保護性拷貝一樣,序列化代理可以阻止偽字節流的攻擊及內部域的盜用攻擊。與使用保護性拷貝不同,使用序列化代理允許Period的域為final的,這可以保證Period類真正不可變。序列化代理模式更容易實現,它不必考慮哪些域會被序列化攻擊,也不必顯示的執行有效性檢查。

序列化代理的局限性:不能與可以被客戶端擴展的類兼容,也不能與對象圖中包含循環的類兼容,比保護性拷貝性能低。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容

  • JAVA序列化機制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 10,903評論 0 24
  • 對象的創建與銷毀 Item 1: 使用static工廠方法,而不是構造函數創建對象:僅僅是創建對象的方法,并非Fa...
    孫小磊閱讀 2,017評論 0 3
  • 官方文檔理解 要使類的成員變量可以序列化和反序列化,必須實現Serializable接口。任何可序列化類的子類都是...
    獅_子歌歌閱讀 2,425評論 1 3
  • 正如前文《Java序列化心得(一):序列化設計和默認序列化格式的問題》中所提到的,默認序列化方法存在各種各樣的問題...
    登高且賦閱讀 8,468評論 0 19
  • 本章關注對象序列化API,它提供了一個框架,用來將對象編碼成字節流,并從字節流編碼中重新構建對象。 相反的處理過程...
    Timorous閱讀 256評論 0 1