Java I/O 序列化機制

    對象序列化的目標是將對象保存到磁盤中,或者通過網絡進行傳輸。序列化機制允許把內存中的Java對象轉換成平臺無關的二進制流,並在需要的時候恢復成原來的Java對象。序列化是保持對象輕量級持久的方式。爲了讓某個類是可序列化的,該類必須實現以下的兩個接口之一:Serializable以及Externalizable。下面分別介紹這兩個接口以及對象序列化的相關內容。

1.Serializable接口

    Java中很多類已經實現了Serializable接口,該接口僅僅是一個標記接口,實現該接口無須實現任何方法,它只表明該類的實例是可序列化的。在嘗試序列化不支持 Serializable 接口的對象時,將拋出NotSerializableException。下面的代碼演示瞭如何將對象序列化爲二進制流以及如何從二進制流中反序列化從而恢復一個對象。

package io;
 
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
importjava.io.ObjectOutputStream;
import java.io.Serializable;
 
class Creature {
        Stringkind;
        Creature(){
               this("Mankind");
               System.out.println("InCreature Constructor");
        }
        Creature(Stringkind){
               this.kind= kind;
        }
}
 
class Man extends Creatureimplements Serializable
{
        /**
         * serialVersionUID:類的序列化版本號
         */
       
        privatestatic final long serialVersionUID = -8564385417353625750L;
        Stringname,gender;
        Man(Stringname,String gender){
               this.name= name;
               this.gender= gender;
               System.out.println("InMan Constructor");
        }
        publicString toString(){
               return"My name is "+name+",I am "+gender+" ,"+kind;
        }
        @Override
        publicboolean equals(Object o){
               if(o==null)return false;
               elseif(o instanceof Man){
                       Manman = (Man)o;
                       returnman.name.equals(name) &&
                                      man.gender.equals(gender);
               }
               elsereturn false;
        }
}
 
public class SerializeTest {
 
        publicstatic void writeMan(Man man){
               try{
                       //創建一個ObjectOutputStream輸出流
                       ObjectOutputStreamoos = new ObjectOutputStream(
                               newFileOutputStream("man.txt"));
                       //將per對象寫入輸出流
                       oos.writeObject(man);
               }
               catch(IOException ex)
               {
                       ex.printStackTrace();
               }
        }
        publicstatic Man readMan(){
               Manman=null;
               try{
                       //創建一個ObjectInputStream輸入流
                       ObjectInputStreamois = new ObjectInputStream(
                               newFileInputStream("man.txt"));
                       //從輸入流中讀取一個Java對象,讀出來的爲Object類型,需要將其強制類型轉換爲Man類
                       man= (Man)ois.readObject();
               }
               catch(Exception ex)
               {
                       ex.printStackTrace();
               }
               returnman;
        }
        publicstatic void main(String[] args) {
               //TODO Auto-generated method stub
               Manman = new Man("lcd", "male");
               writeMan(man);
               Manman1 = readMan();
               System.out.println(man1);//My name is lcd,I am male ,Mankind
               System.out.println(man.equals(man1));//true,兩個對象的內容相同
               System.out.println(man==man1);//false,兩個對象的引用不同,即不是同一個對象
               System.out.println(man.name==man1.name);//false     
        }
}

    上面的代碼中,我們將Man類的對象man寫入man.txt文件中,然後從文件流中恢復該對象。從最後的輸出結果可以看出,恢復的對象與原來的對象各個字段的值是相等,但比較引用時,返回false,說明兩個對象並不是同一個對象。除此之外,我們發現Man繼承至Creature類,但Creature類並不是可序列化的,Java序列化機制對這種情況有如下規定:要允許不可序列化類的子類型序列化,可以假定該子類型負責保存和恢復超類型的公用(public)、受保護的 (protected) 和(如果可訪問)包 (package) 字段的狀態。僅在子類型擴展的類有一個可訪問的無參數構造方法來初始化該類的狀態時,纔可以假定子類型有此職責。如果不是這種情況,則聲明一個類爲可序列化類是錯誤的。該錯誤將在運行時檢測到。根據上面的規定,Creature類中必須有一個Man子類可訪問的默認的無參數構造函數,在反序列化時,該Creature的構造器將會被調用來初始化類中的實例變量。在運行過程中,我們確實看到了反序列化過程中該方法被調用。若Creature中找不到對應的構造函數(包括將無參構造函數聲明爲private導致子類無法訪問、或者沒有默認的構造函數,只存在有參數的構造函數),反序列化過程將拋出異常。
    在序列化和反序列化過程中需要特殊處理的類必須使用下列準確簽名來實現特殊方法:

    private voidwriteObject(java.io.ObjectOutputStream out) throws IOException;該方法負責寫入特定類的對象的狀態,以便相應的 readObject 方法可以恢復它。通過調用out.defaultWriteObject 可以調用保存 Object 的字段的默認機制。該方法本身不需要涉及屬於其超類或子類的狀態。通過使用 writeObject 方法或使用 DataOutput 支持的用於基本數據類型的方法將各個字段寫入ObjectOutputStream,狀態可以被保存。
     private void readObject(java.io.ObjectInputStreamin) throws IOException, ClassNotFoundException;該方法負責從流中讀取並恢復類字段。它可以調用in.defaultReadObject 來調用默認機制,以恢復對象的非靜態和非瞬態字段。該方法本身不需要涉及屬於其超類或子類的狀態。隨後需要手動恢復不是採用默認機制保存的信息。
     private void readObjectNoData() throws ObjectStreamException;方法負責初始化特定類的對象狀態。這在接收方使用的反序列化實例類的版本不同於發送方,並且接收者版本擴展的類不是發送者版本擴展的類時發生。在序列化流已經被篡改時也將發生;因此,不管源流是“敵意的”還是不完整的,readObjectNoData方法都可以用來正確地初始化反序列化的對象。

   ANY-ACCESS-MODIFIERObject writeReplace() throws ObjectStreamException;使用該準確的方法聲明,如果此方法存在,在序列化時將被調用,將當前準備序列化的對象替換成該方法返回的對象,該對象必須是可序列化的!

    ANY-ACCESS-MODIFIER ObjectreadResolve() throws ObjectStreamException;實現此方法可以在從流中恢復對象時,使用該方法返回的指定對象來代替。該方法在序列化單例類、枚舉類時尤其有用。

    以上各方法的調用順序爲:在序列化流時,首先調用ObjectOutputStream的writeObject方法來寫入對象,系統調用該對象的writeReplace()方法[若該方法存在則調用,否則跳過此步],如果該方法返回另一個對象,系統將再次調用另一個對象的writeReplace()方法...直到方法不再返回另一個對象爲止;之後,程序調用該對象自定義的writeObject()方法[若存在,則調用,否則調用默認的defaultWriteObject()方法]來保存對象的狀態。在讀出對象時,首先是調用流的readObject()方法,之後轉到調用對象自定義的readObject()方法[若存在,則調用,否則爲defaultReadObject],readResovle()方法會緊接着被調用[若存在],該方法的返回值將會代替原來反序列化的對象,原來的對象被丟棄。

2.Externalizable接口

    Java提供的另一個序列化的機制是實現Externalizable接口,該接口是繼承自Serializable接口的。這種序列化方式完全由程序員決定存儲和恢復對象數據。該接口有如下兩個方法:

     voidreadExternal(ObjectInput in):對象實現 readExternal 方法來恢復其內容,它通過調用 DataInput 的方法來恢復其基礎類型,調用 readObject 來恢復對象、字符串和數組。

     voidwriteExternal(ObjectOutput out):該對象可實現 writeExternal 方法來保存其內容,它可以通過調用 DataOutput 的方法來保存其基本值,或調用 ObjectOutput 的 writeObject 方法來保存對象、字符串和數組。

     這兩個方法其實相當於對象中的readObject()方法和writeObject()方法,該接口同樣可以使用writeReplace方法和readResovle()方法來選擇替代對象。

3.序列化版本號

     Java序列化機制允許爲序列化類提供一個“ANY-ACCESS-MODIFIERstatic final long serialVersionUID”值,該field用於標識該Java類的序列化版本號。該序列號在反序列化過程中用於驗證序列化對象的發送者和接收者是否爲該對象加載了與序列化兼容的類。如果接收者加載的該對象的類的 serialVersionUID 與對應的發送者的類的版本號不同,則反序列化將會導致InvalidClassException。如果可序列化類未顯式聲明 serialVersionUID,則序列化運行時將基於該類的各個方面計算該類的默認 serialVersionUID 值。強烈建議 所有可序列化類都顯式聲明serialVersionUID 值,原因是計算默認的 serialVersionUID 對類的詳細信息具有較高的敏感性,容易造成對象的反序列化因爲版本不兼容而失敗。若類中定義了serialVersionUID的值,則:

     (1)如果修改了類中的方法,或者靜態的Field、瞬態Field,則反序列化不受任何影響,類定義中無須修改serialVersionUID的值;

     (2)修改了類中的非靜態的Field、非瞬態Field,則可能導致序列化版本不兼容而失敗,因此需要更新serialVersionUID 的值。如果僅僅是因爲修改而包含了更多或者更少的Field,則仍然能夠反序列化,這些多出來的Field值將被設置爲默認值或者被忽略,否則序列化失敗。

4.對象序列化要點總結

     (1)在一個Serializable對象進行還原的過程中,沒有調用任何的構造器,包括默認的構造器。整個對象都是通過InputStream中取得數據恢復回來的。當這個Serializable類有多個父類時,包括直接父類和間接父類,這些父類要麼有非private的無參數構造器(供反序列化時進行調用進行父類Field的初始化),要麼必須也是可序列化的,否則在反序列化時將拋出InvalidClassException異常。如果父類是不可序列化的,只是帶有無參數構造函數,則父類中定義的Field值不會序列化到二進制流中。而對於一個Externalizable對象而言,在反序列化時,會調用該對象的默認構造函數,該構造函數必須聲明爲public否則將拋出InvalidClassException異常。

     (2)所有保存到磁盤中的對象都有一個序列編號,當程序試圖在某個流中序列化一個對象時,程序首先檢查對象流中該對象是否已經被序列化過,只有該對象從未(在該對象流中)被序列化過,系統纔會將該對象轉換成字節序列並輸出。如果該對象已經被序列化過,程序只是輸出一個序列化編號,而不是重新序列化該對象。當序列化可變對象時,只有第一次調用writeObject方法來輸出對象時纔會將對象轉換爲字節序列,在後面的程序中,即使該對象的Field值發生了改變,再次調用writeObject方法輸出對象,改變後的Field值也不會輸出。除非在兩次調用writeObject方法之間,調用了reset方法,該重置方法將丟棄已寫入流中的所有對象的狀態。重新設置狀態,使其與新的ObjectOutputStream 相同。將流中的當前點標記爲 reset,相應的 ObjectInputStream 也將在這一點重置。以前寫入流中的對象不再被視爲正位於流中。它們會再次被寫入流。

     (3)對象的類名,Field(包括基本類型,數組,對其他對象的引用)都會被序列化;方法、靜態Field、transient字段都不會被序列化。如果一個可序列化的對象包含對某個不可序列化的對象的引用,那麼整個序列化操作將會失敗,並且會拋出一個NotSerializableException。

     (4)反序列化時,必須有序列化對象的class文件,否則拋出ClassNotFoundException。

     (5)當通過文件,網絡來讀取序列化後的對象時,必須按照實際寫入的順序讀取。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章