深入理解JAVA I/O系列五:對象序列化


序列化

對象序列化的目標是將對象保存到磁盤中,或者允許在網絡中直接傳輸對象。對象序列化機制允許把內存中的JAVA對象轉換成跟平台無關的二進制流,從而允許將這種二進制流持久地保存在磁盤上,通過網絡將這種二進制流傳輸到另一個網絡節點,其他程序一旦獲得了這種二進制流,都可以講二進制流恢復成原來的JAVA對象。

序列化為何存在

我們知道當虛擬機停止運行之后,內存中的對象就會消失;另外一種情況就是JAVA對象要在網絡中傳輸,如RMI過程中的參數和返回值。這兩種情況都必須要將對象轉換成字節流,而從用於保存到磁盤空間中或者能在網絡中傳輸。

由於RMI是JAVA EE技術的基礎---所有分布式應用都需要跨平台、跨網絡。因此序列化是JAVA EE的基礎,通常建議,程序創建的每個JavaBean類都可以序列化。

如何序列化

如果要讓每個對象支持序列化機制,必須讓它的類是可序列化的,則該類必須實現如下兩個接口之一:

1、Serializable

2、Extmalizable

這里有幾個原則,我們一起來看下:

1、Serializable是一個標示性接口,接口中沒有定義任何的方法或字段,僅用於標示可序列化的語義。

2、靜態變量和成員方法不可序列化。

3、一個類要能被序列化,該類中的所有引用對象也必須是可以被序列化的。否則整個序列化操作將會失敗,並且會拋出一個NotSerializableException,除非我們將不可序列化的引用標記為transient。

4、聲明成transient的變量不被序列化工具存儲,同樣,static變量也不被存儲。

一、先來看下將一個對象序列化之后存儲到文件中:

public class Person implements Serializable
{
    int age;
    String address;
    double height;
    public Person(int age, String address, double height)
    {
        this.age = age;
        this.address = address;
        this.height = height;
    }
}
public class SerializableTest
{
    public static void main(String[] args) throws IOException, IOException
    {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
                "d:/data.txt"));
        Person p = new Person(25,"China",180);
        oos.writeObject(p);
        oos.close();
    }
}

執行結果:

 

1、對象序列化之后,寫入的是一個二進制文件,所有打開亂碼是正常現象,不過透過亂碼我們還是可以看到文件中存儲的就是我們創建的那個對象那個。

2、Person對象實現了Serializable接口,這個接口沒有任何方法需要被實現,只是一個標記接口,表示這個類的對象可以被序列化。 

     

3、在該程序中,我們是調用ObjectOutputStream對象的writeObject()方法輸出可序列化對象的。該對象還提供了輸出基本類型的方法。

    writeInt

    writeUTF(String str)

    writeFloat(float val)

二、接下來我們來看下從文件中反序列化對象的過程:

 1 public class SerializableTest
 2 {
 3     public static void main(String[] args) throws IOException, IOException,
 4             ClassNotFoundException
 5     {
 6         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
 7                 "d:/data.txt"));
 8         Person p = new Person(25, "China", 180);
 9         oos.writeObject(p);
10         oos.close();
11 
12         ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
13                 "d:/data.txt"));
14         Person p1 = (Person) ois.readObject();
15         System.out.println("age=" + p1.age + ";address=" + p1.address
16                 + ";height=" + p1.height);
ois.close();
17 } 18 }

執行結果:

age=25;address=China;height=180.0

1、從第12行開始就是反序列化的過程。其中輸入流用到的是ObjectInputStream,與前面的ObjectOutputStream相對應。

2、在調用readObject()方法的時候,有一個強轉的動作。所以在反序列化時,要提供java對象所屬類的class文件。

3、如果使用序列化機制向文件中寫入了多個對象,在反序列化時,需要按實際寫入的順序讀取。

對象引用的序列化

1、上面介紹對象的成員變量都是基本數據類型,如果對象的成員變量是引用類型,會有什么不同嗎?

     這個引用類型的成員變量必須也是可序列化的,否則擁有該類型成員變量的類的對象不可序列化。

2、在引用對象這個地方,會出現一種特殊的情況。例如,有兩個Teacher對象,它們的Student實例變量都引用了同一個Person對象,而且該Person對象還有一個引用變量引用它。如下圖所示: 

                                            

 

 這里有三個對象per、t1、t2,如果都被序列化,會存在這樣一個問題,在序列化t1的時候,會隱式的序列化person對象。在序列化t2的時候,也會隱式的序列化person對象。在序列化per的時候,會顯式的序列化person對象。所以在反序列化的時候,會得到三個person對象,這樣就會造成t1、t2所引用的person對象不是同一個。顯然,這並不符合圖中所展示的關系,也違背了java序列化的初衷。

為了避免這種情況,JAVA的序列化機制采用了一種特殊的算法:

1、所有保存到磁盤中的對象都有一個序列化編號。

2、當程序試圖序列化一個對象時,會先檢查該對象是否已經被序列化過,只有該對象從未(在本次虛擬機中)被序列化,系統才會將該對象轉換成字節序列並輸出。

3、如果對象已經被序列化,程序將直接輸出一個序列化編號,而不是重新序列化。

自定義序列化

1、前面介紹可以用transient關鍵字來修飾實例變量,該變量就會被完全隔離在序列化機制之外。還是用前面相同的程序,只是將address變量用transient來修飾:

 1 public class Person implements Serializable
 2 {
 3     int age;
 4     transient String address;
 5     double height;
 6     public Person(int age, String address, double height)
 7     {
 8         this.age = age;
 9         this.address = address;
10         this.height = height;
11     }
12 }
 1 public class SerializableTest
 2 {
 3     public static void main(String[] args) throws IOException, IOException,
 4             ClassNotFoundException
 5     {
 6         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
 7                 "d:/data.txt"));
 8         Person p = new Person(25, "China", 180);
 9         oos.writeObject(p);
10         oos.close();
11 
12         ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
13                 "d:/data.txt"));
14         Person p1 = (Person) ois.readObject();
15         System.out.println("age=" + p1.age + ";address=" + p1.address
16                 + ";height=" + p1.height);
ois.close();
17 } 18 }

序列化的結果: 

反序列化結果:

age=25;address=null;height=180.0

2、在二進制文件中,沒有看到"China"的字樣,反序列化之后address的value值為null。

3、這說明使用tranisent修飾的變量,在經過序列化和反序列化之后,JAVA對象會丟失該實例變量值。

鑒於上述的這種情況,JAVA提供了一種自定義序列化機制。這樣程序就可以自己來控制如何序列化各實例變量,甚至不序列化實例變量。

在序列化和反序列化過程中需要特殊處理的類應該提供如下的方法,這些方法用於實現自定義的序列化。

writeObject()

readObject()

這兩個方法並不屬於任何的類和接口,只要在要序列化的類中提供這兩個方法,就會在序列化機制中自動被調用。

其中writeObject方法用於寫入特定類的實例狀態,以便相應的readObject方法可以恢復它。通過重寫該方法,程序員可以獲取對序列化的控制,可以自主決定可以哪些實例變量需要序列化,怎樣序列化。該方法調用out.defaultWriteObject來保存JAVA對象的實例變量,從而可以實現序列化java對象狀態的目的。

 1 public class Person implements Serializable
 2 {
 3     /**
 4      * 
 5      */
 6     private static final long serialVersionUID = 1L;
 7     int age;
 8     String address;
 9     double height;
10     public Person(int age, String address, double height)
11     {
12         this.age = age;
13         this.address = address;
14         this.height = height;
15     }
16     
17     //JAVA BEAN自定義的writeObject方法
18     private void writeObject(ObjectOutputStream out) throws IOException
19     {
20         System.out.println("writeObejct ------");
21         out.writeInt(age);
22         out.writeObject(new StringBuffer(address).reverse());
23         out.writeDouble(height);
24     }
25     
26     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
27     {
28         System.out.println("readObject ------");
29         this.age = in.readInt();
30         this.address = ((StringBuffer)in.readObject()).reverse().toString();
31         this.height = in.readDouble();
32     }
33 }
 1 public class SerializableTest
 2 {
 3     public static void main(String[] args) throws IOException, IOException,
 4             ClassNotFoundException
 5     {
 6         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
 7                 "d:/data.txt"));
 8         Person p = new Person(25, "China", 180);
 9         oos.writeObject(p);
10         oos.close();
11 
12         ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
13                 "d:/data.txt"));
14         Person p1 = (Person) ois.readObject();
15         System.out.println("age=" + p1.age  + ";address=" + p1.address
16                 + ";height=" + p1.height);
17         ois.close();
18     }
19 }

序列化結果:

反序列化結果:                                       

1、這個地方跟前面的區別就是在Person類中提供了writeObject方法和readObject方法,並且提供了具體的實現。

2、在ObjectOutputStream調用writeObject方法執行過程,肯定調用了Person類的writeObject方法,因為在控制台上將代碼中第20行的日志輸出了。

3、自定義實現的好處是:程序員可以更加精細或者說可以去定制自己想要實現的序列化,如例子中將address變量值反轉。利用這種特點,我們可以在序列化過程中對一些敏感信        息做特殊的處理。

4、在這里因為我們在要序列化的類中提供了這兩個方法,所以被調用了,如果不提供,我認為會默認調用ObjectOutputStream/ObjectInputStream提供的這兩個方法。

                     

                                                                      

 

序列化問題

1、靜態變量不會被序列化。

2、子類序列化時:

     如果父類沒有實現Serializable接口,沒有提供默認構造函數,那么子類的序列化會出錯;

     如果父類沒有實現Serializable接口,提供了默認的構造函數,那么子類可以序列化,父類的成員變量不會被序列化。

     如果父類實現了Serializable接口,則父類和子類都可以序列化。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM