Java序列化心得(二):自定義序列化

正如前文《Java序列化心得(一):序列化設計和默認序列化格式的問題》中所提到的,默認序列化方法存在各種各樣的問題,出于效率或安全等方面的考慮,往往需要開發人員自定義序列化方法生成自定義序列化格式。當然,對應地也需要自定義反序列化方法,這里統稱為“自定義序列化”(Custom Serialization)。

默認和自定義序列化方法的混合使用

大多數情況下,用戶不需要完全重新實現序列化方法,只需要在原有默認方法上進行改進,本章將舉例說明來默認和自定義序列化方法混合使用的情況。

首先我們看下面的例子:

public class Student implements Serializable {  
 
    private String firstName = null;      
    private String lastName = null;  
    private Integer age = null;  
    // unserializable field
    private transient School School= null;
    
    public Student () { }  

    public Student (String fname, String lname, Integer age, School school) {  
        this.firstName = fname;  
        this.lastName = lname;  
        this.age = age;  
        this.school = school;
    }
} 

public class School{  

    public String sName = null; 
    public String sId = null;

    public School(){
        this.sName = "";
        this.depId = "";
    }
    public School(String name, String id){
        this.sName = name;
        this.sId = id;
    }
} 

這里有兩個類,Student類和School類,前者在類中引用了后者。雖然Student類已經聲明“implements Serializable”,但是這個類不能順利地被序列化,因為它引用的School類并不是可以序列化的。也許有的讀者會說將School類聲明為序列化不就好了嘛?前文《Java序列化心得(一):序列化設計和默認序列化格式的問題》中已經提到:“不要輕易的決定將一個類序列化”,所以在沒有十分明確的需求下,不要輕易將School類改為“ implements Serializable”,否則School類如果以后被修改,將會影響到Student類序列化的格式,我們在設計類的序列化格式時最重要的原則就是保持其字節化格式的固定性,以降低維護它的代價。

那要如何序列化Student類為好呢?這里我們的序列化方案為:域school引用其他類,變化可能很大,所以采用自定義的方法序列化;其他域,如姓、名和年齡,反映的是Student類固有物理屬性,且都為基本類型,形式固定,故而采用默認的序列化方法。具體序列化代碼將在下面一個章節給出。

2. 自定義序列化的一般方法

細心的讀者已經發現了,Student 類中school域前多了關鍵字transient, 其作用在于:

當某個字段被聲明為transient后,默認序列化機制就會忽略該字.段

這樣一來,默認的序列化機制不對school經行處理,我們才能開始實現自定義方法: writeObject( ) 與readObject( ) , 其中:

  • void writeObject(ObjectOutputStream out) throws IOException

用來實現序列化的機制:從流中讀取字節數據,并轉化為類對象;

  • void readObject(ObjectInputStream in)
    throws IOException,ClassNotFoundException

用來實反現序列化機制:將對象轉化為字節數據,寫入帶流中。

具體實例代碼如下:

private void writeObject(ObjectOutputStream out) throws IOException {  
        //invoke default serialization method
        out.defaultWriteObject(); 
 
        if(school == null)
            school = new School();
        out.writeObject(school.sName);  
        out.writeObject(school.sId);  
    }  
 
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        //invoke default serialization method
        in.defaultReadObject();
  
        String name = (String) in.readObject();  
        String id = (String) in.readObject(); 
        school = new School(name, id);
    }  

我們以writeObject()為例進行說明:

  1. 首先writeObject()調用了默認的序列化方法defaultWriteObject()來處理非transient域firstNamelastNameage,將它們依次字節化寫入流中;
  2. 接下來通過自定義的方法將school域寫入流中:因為School類最主要的屬性是學校的編號(Id)和名稱(Name),我們也只關心這兩個屬性,而且這兩個屬性都是String類型,已經實現了“Serializable”接口,綜上所述,我們就可以直接將它們寫入流中,在反序列化過程中再根據這些屬性來生成School類的對象。

readObject()中的內容也相似:

  1. 調用defaultReadObject()方法,將非transient域firstNamelastNameage從流中讀出;
  2. 從流中讀出和School類相關的內容(Id和Name),并根據這些內容生成新的School對象賦給school域;

這里需要說明下:即使所有的域都是transient的,也建議在writeObject() 與readObject() 中調用默認的序列化方法defaultWriteObject()和defaultReadObject())。這是從兼容性方面考慮的,如果以后類的結構發生了調整,增加非transient域,現有的序列化和反序列化機制也可以奏效的,因此強烈建議大家不要忘記調用默認的序列化方法,即使沒有什么實際用處。

最后,為了保證序列化版本的一致性,要加上手動顯示定義版本號serialVersionUID

完整的Student類代碼如下:

public class Student implements Serializable {  

    private final static long serialVersionUID = 1L
 
    private String firstName = null;      
    private String lastName = null;  
    private Integer age = null;  
    // unserializable field
    private transient School School= null;
    
    public Student () { }  

    public Student (String fname, String lname, Integer age, School school) {  
        this.firstName = fname;  
        this.lastName = lname;  
        this.age = age;  
        this.school = school;
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {  
        //invoke default serialization method
        out.defaultWriteObject();  

        if(school == null)
            school = new School();
        out.writeObject(school.sName);  
        out.writeObject(school.sId);  
    }  
 
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        //invoke default serialization method
        in.defaultReadObject();
  
        String name = (String) in.readObject();  
        String id = (String) in.readObject(); 
        school = new School(name, id);
    }  
} 

將readObject() 方法看成是構造器

《Effective Java》中曾經提到,readObject()的作用相當于參數為ObjectInputStream類型的構造器,因此要像構造器一樣,對于參數的有效性進行檢查。

舉給例子,Student類中的age域代表這個學生的年齡,很顯然應該是非負整數,但是如果有人惡意偽造的輸入流,并把age對應的值設為-1,上面的readObject()方法不能提供數字有效性的檢查,age=-1情況就會發生,這顯然是錯誤的。

因此,我們在設計readObject()方法時要考慮其生成實例的有效性,確保實例中各個域值都符合構造對象時的約束。

帶有數據約束檢查的readObject()實例如下:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        //invoke default serialization method
        in.defaultReadObject();
        
        //check the value of "age" field
        if(this.age < 0)
              throw new InvalidObjectException("invalid data: age < 0");
  
        String name = (String) in.readObject();  
        String id = (String) in.readObject(); 
        school = new School(name, id);
    }  

如果發現age域的數值小于0,則說明此數據有問題,readObject()將會拋出異常“InvalidObjectException”。

除此之外,要解決反序列化異常問題,還可以手動實現:

private void readObjectNoData() throws ObjectStreamException;

readObject()遇到諸如序列化版本不一致或者是輸入流被篡改/損壞時, 異常被拋出后會自動調用該方法,給對象提供一個合理的默認值(比如整數型為0,Boolen型為fasle,類引用為null等等);

readResolve() 與單例模式

Serializable接口還有兩個接口方法可以實現序列化對象的替換,即writeReplace()和readResolve()。這里我們先主要來談談readReplace()方法。

Object readResolve() throws ObjectStreamException;

readResolve()方法會在readObject()調用之后自動調用,顧名思義,其最主要就是將反序列化之后的對象替換掉:其返回類型Object,因此該函數雖然沒有任何參數,但是可以通過this訪問到反序列化的對象,將其替換成任何對象類型再返回,而這個返回值將作為反序列機制輸入流的最終輸入結果。

readResolve()最重要的應用場景就是保護性恢復單例模式的對象,這種類型全局中都應該保證只有一個實例,因此readResolve()可以和單例工廠結合,根據實際情況把對象替換掉。

比如在某種場景下,所有學生都是一個學校的,這時候School類就應該是單例的,序列化傳入的數據有可能和當前實例并不一致,這時如果School類要實現序列化就需要readResolve()幫助,代碼如下:

public class School{  

    public String sName = null; 
    public String sId = null;

    public static final School instance= new School();
    
    // Singleton pattern requires constructors to be private.
    private School(){
        this.sName = "";
        this.depId = "";
    }

    // Singleton pattern requires constructors to be private.
    private School(String name, String id){
        this.sName = name;
        this.spId = id;
    }
    
    private Object readResolve(){
        return instance;
    }

無論默認序列化機制傳入什么樣的數據,都會被替換為當前School類中保存的實例;

序列化代理和writeReplace()

正如前文《Java序列化心得(一):序列化設計和默認序列化格式的問題》中所提到的, 當一個類被實現序列化之后,就有可能增加Bug和安全隱患。要想解決這一問題,序列化代理模式就是可以利用手段。

一般而言,序列化代理類是作為內部嵌入類存在于主類中,主類和它內部的序列化代理類都要求聲明“implement Serializable”。在結構上,序列化代理類的默認序列化格式就應該是主類序列化格式的完美體現。例如Student類的序列化代理可以設計為

private class SerializationProxy4Student implements Serializable {  

    private final static long serialVersionUID = 1L // Any number will do
 
    private String firstName = null;      
    private String lastName = null;  
    private Integer age = null;  
    private String schoolName = null;
    private String schoolId = null;
    
    SerializationProxy4Student (Student s) {
        this.firstname = s.firstname;
        this.lastname = s.lastname;
        this. age = s.age;
        this.schoolName = s.school.sName;
        this.schoolId = s.school.sId;
     }  
} 

正如上面說提到的,SerializationProxy4Student的默認序列化格式就是上面我們自定義序列化中所體現的。我們只需要在每次序列化中將Student類對象轉化為SerializationProxy4Student類對象寫入流中即可,那如何替換呢?這就需要writeReplace() 函數的幫助。

Object writeReplace() throws ObjectStreamException;

writeReplace()方法被實現后,序列化機制會先調用writeReplace()方法將當前對象替換成另一個對象(該方法會返回替換后的對象)并將其寫入流中,這便是序列化代理需要的功能。在實現writeReplace()時要注意一下幾點:

  1. 實現writeReplace()方法之后,在不再需要writeObject()和readObject(),因為writeReplace()的返回值會被自動調用默認序列化機制寫入輸出流中,同時因為對象類型已經被替換,并且是不可逆的,所以readObject()的調用也不是需要,甚至還會給攻擊者偽造輸入流提供機會來,所以不建議使用。可以在writeObject()和readObject()中拋出異常,來保證這一點,例如:
private void readObject( ObjectInputStream stream) throw InvaildObjectException{
    throw new InvaildObjectException("Proxy Pattern Required");
}
  1. 因為writeReplace()返回值將自動序列化,所以其返回類型必須是可序列化的,這也是就是要求序列化代理類中的域都是可序列化的;

  2. readResolve()并不是用來恢復writeReplace()的,二者并不是成對出現的,也沒有必然聯系,切記。

下面我們回到序列化代理模式中,定義好序列化代理類,在主類中就可以調用writeReplace()方法替換序列化類型,例如:

private Object writeReplace ( ) {
    return new SerializationProxy4Student(this); // this:Student instance
}

使用序列化代理的最大好處就在于:將序列化的內容和類的結構分離開。無論主類(比如Student類)如何修改和被繼承,其序列化的格式都是代理類(SerializationProxy4Student類)的序列化格式,這是固定的,所以接受該類序列化的其他代碼也不用擔心主類的變化,他們只是專注于處理代理類,代理類已經包含了所有被需要的信息。

如果主類有著十分巨大的版本變化,新舊版本的序列化的格式還都在被使用中,這種情況下可以構造多個序列化代理類,就可以根據情況支持多種序列化格式,而不必修改原有的接口,代碼也有更好的兼容性和可擴展性。

Externalizable接口:強制自定義序列化

上文中關于序列化的自定義方法的介紹越來越復雜,自定義的程度也越來越深,那有沒有完全定制的序列化方法嗎?這就是** Externalizable**接口。

Externalizable接口繼承于Serializable,當使用該接口時,強制要求序列化的細節都由開發人員去完成,即實現writeExternal()與readExternal()方法。

另外,使用Externalizable進行序列化時,當讀取對象時,會調用被序列化類的無參構造器去創建一個新的對象,然后再將被保存對象的字段的值分別填充到新對象中。這邊是前文《Java序列化心得(一):序列化設計和默認序列化格式的問題》中所提到的要保留無參數構造器的原因。

比較而言,Externalizable更為高效, 但Serializable更加靈活,其最重要的特色就是可以自動序列化,因此使用廣泛。所以一般只有在對效率要求較高的情況下才會考慮Externalizable,但通常情況下Serializable使用的更多。
關于性能比較可以參考http://www.tuicool.com/articles/2Q3M73

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • JAVA序列化機制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 10,931評論 0 24
  • 將一個對象編碼成字節流稱作將該對象「序列化」。相反,從字節流編碼中重新構建對象被稱作「反序列化」。一旦對象被「序列...
    Alent閱讀 794評論 0 1
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,778評論 18 399
  • 一、 序列化和反序列化概念 Serialization(序列化)是一種將對象以一連串的字節描述的過程;反序列化de...
    步積閱讀 1,458評論 0 10
  • 想說說最近的日子 那天做完心理疏導 我找了地方坐了一會 心里回蕩著老師的那句話 你看起來好多了 或許糟糕透了 就很...
    82fa6c830610閱讀 265評論 0 0