第一篇: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兩個信息。

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信息是我們不希望序列化的:

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
這正是我們期望的結果。