Effective java筆記(十),序列化

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

74、謹慎的實現(xiàn)Serializable接口

一個類實現(xiàn)Serializable接口需要付出的代價:

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

每個可序列化的類都有一個唯一的名為serialVersionUID的標(biāo)識號與它相關(guān)聯(lián)。若類在私有的靜態(tài)final的long域中沒有顯式的指定這個標(biāo)識號,系統(tǒng)就會自動的為該類產(chǎn)生一個標(biāo)識號,這時類的兼容性將會遭到破壞,在運行時導(dǎo)致InvalidClassException異常。

為了繼承而設(shè)計的類應(yīng)該盡可能少的去實現(xiàn)Serializable接口,用戶自定義的接口也應(yīng)該盡可能少的繼承Serializable接口。例外,Throwable、Component和HttpServlet抽象類。

內(nèi)部類不應(yīng)該實現(xiàn)Serializable即可。靜態(tài)成員類可以實現(xiàn)Serializable接口。

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

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

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類的實例域精確的反應(yīng)了它的邏輯內(nèi)容,可以使用默認的序列化形式。注意:雖然lastName、firstName和middleName域是私有的,但它們?nèi)匀恍枰凶⑨屛臋n。因為,這些私有域定義了一個公有的API,即這個類的序列化形式。@serial標(biāo)簽用來告知Javadoc工具,把這些文檔信息放在有關(guān)序列化形式的特殊文檔頁中。

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

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

如:

//默認序列化形式
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的首要任務(wù)仍是調(diào)用defaultXxxObject方法,這樣可以極大的增強靈活性。另外盡管writeObject是私有的,仍然需要文檔注釋。

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

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

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

76、保護性編寫readObject方法

readObject方法實際上相當(dāng)于一個公有的構(gòu)造器,如同其它構(gòu)造器一樣,readObject方法必須檢查其參數(shù)的有效性,并且在必要的時候進行保護性拷貝。readObject是一個「用字節(jié)流作為唯一參數(shù)」的構(gòu)造器,當(dāng)面對一個人工仿造的字節(jié)流時,readObject產(chǎn)生的對象可能會違反它所屬類的約束條件,所以必須在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中增加了有效性檢查,但通過偽造字節(jié)流創(chuàng)建可變的Period實例仍是可能的。做法是:字節(jié)流以Period實例開頭,然后附加上兩個額外的引用執(zhí)行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的字節(jié)
bos.write(ref);
ref[4] = 4; //指向period中私有域end的字節(jié)
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方法中,必須對這些組件進行保護性拷貝。否則,它內(nèi)部的約束條件可能遭受破壞。如:

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域必須在對象構(gòu)造時初始化,為了使用readObject方法,必須將start和end域做成非final的。

編寫readObject方法的指導(dǎo)原則:

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

77、對于實例控制,枚舉類型優(yōu)先于readResolve

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

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

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

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;
    }
}

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

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

public enum Elvis {
    INSTANCE;
    ....
}

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

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

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

序列化代理模式能夠極大的減少實現(xiàn)Serializable接口所帶來的風(fēng)險。

實現(xiàn)序列化代理模式的步驟:

  • 首先為可序列化的類設(shè)計一個私有的靜態(tài)嵌套類,精確的表示外圍類實例的邏輯狀態(tài)。它有一個單獨的構(gòu)造器,其參數(shù)類型為外圍類。外圍類及其序列化代理都必須實現(xiàn)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());
    }

    //在序列化之前,將外圍類的實例轉(zhuǎn)變成它的序列化代理
    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);
        }
    }
}

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容