Java回顧之序列化


  第一篇:Java回顧之I/O

  第二篇:Java回顧之網絡通信

  第三篇:Java回顧之多線程

  第四篇:Java回顧之多線程同步

  第五篇:Java回顧之集合

 

  在這篇文章里,我們關注對象序列化。

  首先,我們來討論一下什么是序列化以及序列化的原理;然后給出一個簡單的示例來演示序列化和反序列化;有時有些信息是不應該被序列化的,我們應該如何控制;我們如何去自定義序列化內容;最后我們討論一下在繼承結構的場景中,序列化需要注意哪些內容。

  序列化概述

  序列化,簡單來講,就是以“流”的方式來保存對象,至於保存的目標地址,可以是文件,可以是數據庫,也可以是網絡,即通過網絡將對象從一個節點傳遞到另一個節點。

  我們知道在Java的I/O結構中,有ObjectOutputStream和ObjectInputStream,它們可以實現將對象輸出為二進制流,並從二進制流中獲取對象,那為什么還需要序列化呢?這需要從Java變量的存儲結構談起,我們知道對Java來說,基礎類型存儲在棧上,復雜類型(引用類型)存儲在堆中,對於基礎類型來說,上述的操作時可行的,但對復雜類型來說,上述操作過程中,可能會產生重復的對象,造成錯誤。

  而序列化的工作流程如下:

  1)通過輸出流保存的對象都有一個唯一的序列號。

  2)當一個對象需要保存時,先對其序列號進行檢查。

  3)當保存的對象中已包含該序列號時,不需要再次保存,否則,進入正常保存的流程。

  正是通過序列號的機制,序列化才可以完整准確的保存對象的各個狀態。

  序列化保存的是對象中的各個屬性的值,而不是方法或者方法簽名之類的信息。對於方法或者方法簽名,只要JVM能夠找到正確的ClassLoader,那么就可以invoke方法。

  序列化不會保存類的靜態變量,因為靜態變量是作用於類型,而序列化作用於對象。

  簡單的序列化示例

  序列化的完整過程包括兩部分:

  1)使用ObjectOutputStream將對象保存為二進制流,這一步叫做“序列化”。

  2)使用ObjectInputStream將二進制流轉換成對象,這一步叫做“反序列化”。

  下面我們來演示一個簡單的示例,首先定義一個Person對象,它包含name和age兩個信息。

定義Person對象
 1 class Person implements Serializable
 2 {
 3     private String name;
 4     private int age;
 5     public void setName(String name) {
 6         this.name = name;
 7     }
 8     public String getName() {
 9         return name;
10     }
11     public void setAge(int age) {
12         this.age = age;
13     }
14     public int getAge() {
15         return age;
16     }
17     
18     public String toString()
19     {
20         return "Name:" + name + "; Age:" + age;
21     }
22 }

  然后是兩個公共方法,用來完成讀、寫對象的操作:

 1 private static void writeObject(Object obj, String filePath)
 2 {
 3     try
 4     {
 5         FileOutputStream fos = new FileOutputStream(filePath);
 6         ObjectOutputStream os = new ObjectOutputStream(fos);
 7         os.writeObject(obj);
 8         os.flush();
 9         fos.flush();
10         os.close();
11         fos.close();
12         System.out.println("序列化成功。");
13     }
14     catch(Exception ex)
15     {
16         ex.printStackTrace();
17     }
18 }
19 
20 private static Object readObject(String filePath)
21 {
22     try
23     {
24         FileInputStream fis = new FileInputStream(filePath);
25         ObjectInputStream is = new ObjectInputStream(fis);
26         
27         Object temp = is.readObject();
28         
29         fis.close();
30         is.close();
31         
32         if (temp != null)
33         {
34             System.out.println("反序列化成功。");
35             return temp;
36         }
37     }
38     catch(Exception ex)
39     {
40         ex.printStackTrace();
41     }
42     
43     return null;
44 }

  這里,我們將對象保存的二進制流輸出到磁盤文件中。

  接下來,我們首先來看“序列化”的方法:

1 private static void serializeTest1()
2 {
3     Person person = new Person();
4     person.setName("Zhang San");
5     person.setAge(30);
6     System.out.println(person);
7     writeObject(person, "d:\\temp\\test\\person.obj");
8 }

  我們定義了一個Person實例,然后將其保存到d:\temp\test\person.obj中。

  最后,是“反序列化”的方法:

1 private static void deserializeTest1()
2 {    
3     Person temp = (Person)readObject("d:\\temp\\test\\person.obj");
4     
5     if (temp != null)
6     {
7         System.out.println(temp);
8     }
9 }

  它從d:\temp\test\person.obj中讀取對象,然后進行輸出。

  上述兩個方法的執行結果如下:

Name:Zhang San; Age:30
序列化成功。
反序列化成功。
Name:Zhang San; Age:30

  可以看出,讀取的對象和保存的對象是完全一致的。

  隱藏非序列化信息

  有時,我們的業務對象中會包含很多屬性,而有些屬性是比較隱私的,例如年齡、銀行卡號等,這些信息是不太適合進行序列化的,特別是在需要通過網絡來傳輸對象信息時,這些敏感信息很容易被竊取。

  Java使用transient關鍵字來處理這種情況,針對那些敏感的屬性,我們只需使用該關鍵字進行修飾,那么在序列化時,對應的屬性值就不會被保存。

  我們還是看一個實例,這次我們定義一個新的Person2,其中age信息是我們不希望序列化的:

定義Person2對象
 1 class Person2 implements Serializable
 2 {
 3     private String name;
 4     private transient int age;
 5     public void setName(String name) {
 6         this.name = name;
 7     }
 8     public String getName() {
 9         return name;
10     }
11     public void setAge(int age) {
12         this.age = age;
13     }
14     public int getAge() {
15         return age;
16     }
17     
18     public String toString()
19     {
20         return "Name:" + name + "; Age:" + age;
21     }
22 }

  注意age的聲明語句:

1 private transient int age;

  下面是“序列化”和“反序列化”的方法:

 1 private static void serializeTest2()
 2 {
 3     Person2 person = new Person2();
 4     person.setName("Zhang San");
 5     person.setAge(30);
 6     System.out.println(person);
 7     writeObject(person, "d:\\temp\\test\\person2.obj");
 8 }
 9 
10 private static void deserializeTest2()
11 {    
12     Person2 temp = (Person2)readObject("d:\\temp\\test\\person2.obj");
13     
14     if (temp != null)
15     {
16         System.out.println(temp);
17     }
18 }

  它的輸出結果如下:

Name:Zhang San; Age:30
序列化成功。
反序列化成功。
Name:Zhang San; Age:0

  可以看到經過反序列化的對象,age的信息變成了Integer的默認值0。

  自定義序列化過程

  我們可以對序列化的過程進行定制,進行更細粒度的控制。

  思路是在業務模型中添加readObject和writeObject方法。下面看一個實例,我們新建一個類型,叫Person3:

 1 class Person3 implements Serializable
 2 {
 3     private String name;
 4     private transient int age;
 5     public void setName(String name) {
 6         this.name = name;
 7     }
 8     public String getName() {
 9         return name;
10     }
11     public void setAge(int age) {
12         this.age = age;
13     }
14     public int getAge() {
15         return age;
16     }
17     
18     public String toString()
19     {
20         return "Name:" + name + "; Age:" + age;
21     }
22     
23     private void writeObject(ObjectOutputStream os)
24     {
25         try
26         {
27             os.defaultWriteObject();
28             os.writeObject(this.age);
29             System.out.println(this);
30             System.out.println("序列化成功。");
31         }
32         catch(Exception ex)
33         {
34             ex.printStackTrace();
35         }
36     }
37     
38     private void readObject(ObjectInputStream is)
39     {
40         try
41         {
42             is.defaultReadObject();
43             this.setAge(((Integer)is.readObject()).intValue() - 1);
44             System.out.println("反序列化成功。");
45             System.out.println(this);
46         }
47         catch(Exception ex)
48         {
49             ex.printStackTrace();
50         }
51     }
52 }

  請注意觀察readObject和writeObject方法,它們都是private的,接受的參數是ObjectStream,然后在方法體內調用了defaultReadObject或者defaultWriteObject方法。

  這里age同樣是transient的,但是在保存對象的過程中,我們單獨對其進行了保存,在讀取時,我們將age信息讀取出來,並進行了減1處理。

  下面是測試方法:

 1 private static void serializeTest3()
 2 {
 3     Person3 person = new Person3();
 4     person.setName("Zhang San");
 5     person.setAge(30);
 6     System.out.println(person);
 7     try
 8     {
 9         FileOutputStream fos = new FileOutputStream("d:\\temp\\test\\person3.obj");
10         ObjectOutputStream os = new ObjectOutputStream(fos);
11         os.writeObject(person);
12         fos.close();
13         os.close();
14     }
15     catch(Exception ex)
16     {
17         ex.printStackTrace();
18     }
19 }
20 
21 private static void deserializeTest3()
22 {    
23     try
24     {
25         FileInputStream fis = new FileInputStream("d:\\temp\\test\\person3.obj");
26         ObjectInputStream is = new ObjectInputStream(fis);
27         is.readObject();
28         fis.close();
29         is.close();
30     }
31     catch(Exception ex)
32     {
33         ex.printStackTrace();
34     }
35 }

  輸出結果如下:

Name:Zhang San; Age:30
序列化成功。
反序列化成功。
Name:Zhang San; Age:29

  可以看到,經過反序列化得到的對象,其age屬性已經減1。

  探討serialVersionUID

  在上文中,我們描述序列化原理時,曾經提及每個對象都會有一個唯一的序列號,這個序列號,就是serialVersionUID。

  當我們的對象實現Serializable接口時,該接口可以為我們生成serialVersionUID。

  有兩種方式來生成serialVersionUID,一種是固定值:1L,一種是經過JVM計算,不同的JVM采取的計算算法可能不同。

  下面就是兩個serialVersionUID的示例:

1 private static final long serialVersionUID = 1L;
2 private static final long serialVersionUID = -2380764581294638541L;

  第一行是采用固定值生成的;第二行是JVM經過計算得出的。

  那么serialVersionUID還有其他用途嗎?

  我們可以使用它來控制版本兼容。如果采用JVM生成的方式,我們可以看到,當我們業務對象的代碼保持不變時,多次生成的serialVersionUID也是不變的,當我們對屬性進行修改時,重新生成的serialVersionUID會發生變化,當我們對方法進行修改時,serialVersionUID不變。這也從另一個側面說明,序列化是作用於對象屬性上的。

  當我們先定義了業務對象,然后對其示例進行了“序列化”,這時根據業務需求,我們修改了業務對象,那么之前“序列化”后的內容還能經過“反序列化”返回到系統中嗎?這取決於業務對象是否定義了serialVersionUID,如果定義了,那么是可以返回的,如果沒有定義,會拋出異常。

  來看下面的示例,定義新的類型Person4:

 1 class Person4 implements Serializable
 2 {
 3     private String name;
 4     private int age;
 5     public void setName(String name) {
 6         this.name = name;
 7     }
 8     public String getName() {
 9         return name;
10     }
11     public void setAge(int age) {
12         this.age = age;
13     }
14     public int getAge() {
15         return age;
16     }
17     private void xxx(){}
18     
19     public String toString()
20     {
21         return "Name:" + name + "; Age:" + age;
22     }
23 }

  然后運行下面的方法:

1 private static void serializeTest4()
2 {
3     Person4 person = new Person4();
4     person.setName("Zhang San");
5     person.setAge(30);
6     
7     writeObject(person, "d:\\temp\\test\\person4.obj");
8 }

  接下來修改Person4,追加address屬性:

 1 class Person4 implements Serializable
 2 {
 3     private String name;
 4     private int age;
 5     private String address;
 6     public void setName(String name) {
 7         this.name = name;
 8     }
 9     public String getName() {
10         return name;
11     }
12     public void setAge(int age) {
13         this.age = age;
14     }
15     public int getAge() {
16         return age;
17     }
18     private void xxx(){}
19     
20     public String toString()
21     {
22         return "Name:" + name + "; Age:" + age;
23     }
24     public void setAddress(String address) {
25         this.address = address;
26     }
27     public String getAddress() {
28         return address;
29     }
30 }

  然后運行“反序列化”方法:

1 private static void deserializeTest4()
2 {    
3     Person4 temp = (Person4)readObject("d:\\temp\\test\\person4.obj");
4     
5     if (temp != null)
6     {
7         System.out.println(temp);
8     }
9 }

  可以看到,運行結果如下:

java.io.InvalidClassException: sample.serialization.Person4; local class incompatible: stream classdesc serialVersionUID = -2380764581294638541, local class serialVersionUID = -473458100724786987
    at java.io.ObjectStreamClass.initNonProxy(Unknown Source)
    at java.io.ObjectInputStream.readNonProxyDesc(Unknown Source)
    at java.io.ObjectInputStream.readClassDesc(Unknown Source)
    at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
    at java.io.ObjectInputStream.readObject0(Unknown Source)
    at java.io.ObjectInputStream.readObject(Unknown Source)
    at sample.serialization.Sample.readObject(Sample.java:158)
    at sample.serialization.Sample.deserializeTest4(Sample.java:105)
    at sample.serialization.Sample.main(Sample.java:16)

  但是當我們在Person4中添加serialVersionUID后,再次執行上述各步驟,得出的運行結果如下:

反序列化成功。
Name:Zhang San; Age:30

  有繼承結構的序列化

  業務對象會產生繼承,這在管理系統中是經常看到的,如果我們有下面的業務對象:

 1 class Person5
 2 {
 3     private String name;
 4     private int age;
 5     public void setName(String name) {
 6         this.name = name;
 7     }
 8     public String getName() {
 9         return name;
10     }
11     public void setAge(int age) {
12         this.age = age;
13     }
14     public int getAge() {
15         return age;
16     }
17     
18     public String toString()
19     {
20         return "Name:" + name + "; Age:" + age;
21     }
22     
23     public Person5(String name, int age)
24     {
25         this.name = name;
26         this.age = age;
27     }
28 }
29 
30 class Employee extends Person5 implements Serializable
31 {
32     public Employee(String name, int age) {
33         super(name, age);
34     }
35 
36     private String companyName;
37 
38     public void setCompanyName(String companyName) {
39         this.companyName = companyName;
40     }
41 
42     public String getCompanyName() {
43         return companyName;
44     }
45     
46     public String toString()
47     {
48         return "Name:" + super.getName() + "; Age:" + super.getAge() + "; Company:" + this.companyName;
49     }
50 }

  Employee繼承在Person5,Employee實現了Serializable接口,Person5沒有實現,那么運行下面的方法:

 1 private static void serializeTest5()
 2 {
 3     Employee emp = new Employee("Zhang San", 30);
 4     emp.setCompanyName("XXX");
 5     
 6     writeObject(emp, "d:\\temp\\test\\employee.obj");
 7 }
 8 
 9 private static void deserializeTest5()
10 {    
11     Employee temp = (Employee)readObject("d:\\temp\\test\\employee.obj");
12     
13     if (temp != null)
14     {
15         System.out.println(temp);
16     }
17 }

  會正常運行嗎?事實上不會,它會拋出如下異常:

java.io.InvalidClassException: sample.serialization.Employee; no valid constructor
    at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
    at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
    at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
    at java.io.ObjectInputStream.readObject0(Unknown Source)
    at java.io.ObjectInputStream.readObject(Unknown Source)
    at sample.serialization.Sample.readObject(Sample.java:158)
    at sample.serialization.Sample.deserializeTest5(Sample.java:123)
    at sample.serialization.Sample.main(Sample.java:18)

  原因:在有繼承層次的業務對象,進行序列化時,如果父類沒有實現Serializable接口,那么父類必須提供默認構造函數

  我們為Person5添加如下默認構造函數:

1 public Person5()
2 {
3     this.name = "Test";
4     this.age = 1;
5 }

  再次運行上述代碼,結果如下:

Name:Zhang San; Age:30; Company:XXX
序列化成功。
反序列化成功。
Name:Test; Age:1; Company:XXX

  可以看到,反序列化后的結果,父類中的屬性,已經被父類構造函數中的賦值代替了!

  因此,我們推薦在有繼承層次的業務對象進行序列化時,父類也應該實現Serializable接口。我們對Person5進行修改,使其實現Serializable接口,執行結果如下:

Name:Zhang San; Age:30; Company:XXX
序列化成功。
反序列化成功。
Name:Zhang San; Age:30; Company:XXX

  這正是我們期望的結果。


免責聲明!

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



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