對象序列化的目標是將對象保存在磁盤中或者在網絡中進行傳輸。實現的機制是允許將對象轉為與平台無關的二進制流。
java中對象的序列化機制是將允許對象轉為字節序列。這些字節序列可以使Java對象脫離程序存在,從而可以保存在磁盤上,也可以在網絡間傳輸。
對象的序列化是將一個Java對象寫入IO流;與此對應的,反序列化則是從IO流中恢復一個Java對象。
實現序列化
如果要將一個java對象序列化,那么對象的類需要是可序列化的。要讓類可序列化,那么這個類需要實現如下兩個接口:
- Serializable
- Externalizable
使用Serializable序列化
實現Serializable接口非常簡單,只要讓java實現Serializable接口即可,無需實現任何方法。
一個類一旦實現了Serializable接口,那么該類的對象就是可序列化的。實現類的對象的序列化可以使用ObjectOutputStream,實現步驟如下:
- 創建ObjectOutputStream對象;
- 調用ObjectOutputStream的writeObject方法輸出對象。
以下是一個實例:
package com.zhyea.test; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 序列化測試類 * * @author robin * @date 2014年12月18日 */ public class SerialTest { public static void main(String[] args) { ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream("D:\\object.txt")); Person robin = new Person("robin", 29); oos.writeObject(robin); } catch (IOException e) { e.printStackTrace(); } finally { if (null != oos) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } } } /** * 序列化測試用對象 * * @author robin * @date 2014年12月18日 */ class Person implements Serializable{ private static final long serialVersionUID = -6412852654889352693L; /** * 姓名 */ private String name; /** * 年齡 */ private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
如上的代碼實現了將一個Person對象保存在了磁盤的一個文本文件object.txt上。運行程序在D盤上生成了一個object.txt文件。以下是文件內容:
有亂碼(字節流轉字符流導致的),但仍不影響我們分辨出里面是不是我們保存的對象。
接下來需要反序列化將Person對象從磁盤上讀出。相應的反序列化需要使用的類是ObjectInputStream,反序列化步驟如下:
- 創建ObjectInputStream對象;
- 使用ObjectInputStream的readObject方法取出對象。
接下來,重構下我們的代碼,實現反序列化,如下:
package com.zhyea.test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 序列化測試類 * * @author robin * @date 2014年12月18日 */ public class SerialTest { public static void main(String[] args) { Person robin = new Person("robin", 29); String savePath = "D:\\object.txt"; SerialTest test = new SerialTest(); try { test.serialize(robin, savePath); Person person = (Person) test.deSerialize(savePath); System.out.println("Name:" + person.getName() + " Age:" + person.getAge()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 實現序列化 * * @param obj * 要被序列化保存的對象 * @param path * 保存地址 * @throws IOException */ public void serialize(Object obj, String path) throws IOException { ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream(path)); oos.writeObject(obj); } finally { if (null != oos) oos.close(); } } /** * 反序列化取出對象 * * @param path * 被序列化對象保存的位置 * @return * @throws IOException * @throws ClassNotFoundException */ public Object deSerialize(String path) throws IOException, ClassNotFoundException { ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream(path)); return ois.readObject(); } finally { if (null != ois) ois.close(); } } } /** * 序列化測試用對象 * * @author robin * @date 2014年12月18日 */ class Person implements Serializable { private static final long serialVersionUID = -6412852654889352693L; /** * 姓名 */ private String name; /** * 年齡 */ private int age; public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
關於對象序列化與反序列化還有幾點需要注意:
- 反序列化無需通過構造器初始化對象;
- 如果使用序列化機制向文件中寫入了多個對象,那么取出和寫入的順序必須一致;
- Java對類的對象進行序列化時,若類中存在對象引用(且值不為null),也會對類的引用對象進行序列化。
使用transient
在一些特殊場景下,比如銀行賬戶對象,出於保密考慮,不希望對存款金額進行序列化。或者類的一些引用類型的成員是不可序列化的。此時可以使用transient關鍵字修飾不想被或者不能被序列化的成員變量。
繼續調整我們的代碼來做演示:
package com.zhyea.test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; /** * 序列化測試類 * * @author robin * @date 2014年12月18日 */ public class SerialTest { public static void main(String[] args) { Person robin = new Person("robin", 29); School school = new School("XX學校"); Teacher tRobin = new Teacher(robin); tRobin.setSchool(school); tRobin.setSalary(12.0); String savePath = "D:\\object.txt"; SerialTest test = new SerialTest(); try { test.serialize(savePath, tRobin); Teacher t = (Teacher) test.deSerialize(savePath); System.out.println("Name:" + t.getPerson().getName() +" Salary:" + t.getSalary()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 實現序列化 * * @param obj * 要被序列化保存的對象 * @param path * 保存地址 * @throws IOException */ public void serialize(String path, Object ... obj) throws IOException { .... } /** * 反序列化取出對象 * * @param path * 被序列化對象保存的位置 * @return * @throws IOException * @throws ClassNotFoundException */ public Object deSerialize(String path) throws IOException, ClassNotFoundException { ... } } /** * Teacher類 * @author robin * @date 2014年12月18日 */ class Teacher implements Serializable{ private static final long serialVersionUID = -8751853088437904443L; private Person person; private transient School school; private transient double salary; public Teacher(Person person){ this.person = person; } /*略去get、set,請自行補充*/ } /** * School類,不可序列化 * * @author robin * @date 2014年12月18日 */ class School{ private String name; public School(String name){ this.name = name; } /*略去get、set,請自行補充*/ } /** * Person類,可序列化 * * @author robin * @date 2014年12月18日 */ class Person implements Serializable { .... }
在不對Teacher類的school成員添加transient標識的情況下,若school值不為null,會報NotSerializableException。異常信息如下:
在不對Teacher類的salary成員添加transient標識的時候,會如實輸出salary的值,添加后則只會輸出salary的默認初始值即0.0。
需要注意的是transient只能修飾屬性(filed),不能修飾類或方法。
自定義序列化
transient提供了一種簡潔的方式將被transient修飾的成員屬性完全隔離在序列化機制之外。這樣子固然不錯,但是Java還提供了一種自定義序列化機制讓開發者更自由地控制如何序列化各個成員屬性,或者不序列化某些屬性(與transient效果相同)。
在需要自定義序列化和反序列化的類中需要提供以下方法:
- private void writeObject(ObjectOutputStream out)
- private void readObject(ObjectInputStream in)
- private void readObjectNoData()
先說下前兩個方法writeObject和readObject,這兩個方法和ObjectOutputStream及ObjectInputStream里對應的方法名稱相同。實際上,盡管這兩個方法是private型的,但是仍然是在被序列化(或反序列化)階段被外部類ObjectOutputStream(或ObjectInputStream)調用。僅以序列化為例,ObjectOutputStream在執行自己的writeObject方法前會先通過反射在要被序列化的對象的類中(有點繞口是吧)查找有無自定義的writeObject方法,如有的話,則會優先調用自定義的writeObject方法。因為查找反射方法時使用的是getPrivateMethod,所以自定以的writeObject方法的作用域要被設置為private。通過自定義writeObject和readObject方法可以完全控制對象的序列化與反序列化。
如下是示例代碼:
package com.zhyea.test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import com.sun.xml.internal.ws.encoding.soap.DeserializationException; /** * 序列化測試類 * * @author robin * @date 2014年12月18日 */ public class SerialTest { public static void main(String[] args) { Person robin = new Person("robin", 29); String savePath = "D:\\object.txt"; SerialTest test = new SerialTest(); try { test.serialize(savePath, robin); Person person = (Person) test.deSerialize(savePath); System.out.println("Name:" + person.getName() +" Age:" + person.getAge()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 實現序列化 * * @param obj * 要被序列化保存的對象 * @param path * 保存地址 * @throws IOException */ public void serialize(String path, Person ... obj) throws IOException { ObjectOutputStream oos = null; ... } /** * 反序列化取出對象 * * @param path * 被序列化對象保存的位置 * @return * @throws IOException * @throws ClassNotFoundException */ public Object deSerialize(String path) throws IOException, ClassNotFoundException { ... } } /** * Person類,可序列化 * * @author robin * @date 2014年12月18日 */ class Person implements Serializable { private static final long serialVersionUID = -6412852654889352693L; /** * 姓名 */ private String name; /** * 年齡 */ private int age; public Person() {} public Person(String name, int age) { this.name = name; this.age = age; } /* 略去get和set,請自行實現 */ private void writeObject(ObjectOutputStream out) throws IOException{ out.writeObject(name); out.writeInt(age + 1); System.out.println("my write"); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{ this.name = "zhangsan"; this.age = 30; System.out.println("my read"); } }
以下是輸出結果:
關於readObjectNoData,在網上找了如下一段說明:
readObjectNoData 原始情況 pojo public class Person implements Serializable { private int age; public Person() { } //setter getter... } 序列化 Person p = new Person(); p.setAge(10); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("c:/person.ser")); oos.writeObject(p); oos.flush(); oos.close(); 類結構變化后, 序列化數據不變 pojo Animal implements Serializable 顯式編寫readObjectNoData public class Animal implements Serializable { private String name; public Animal() { } //setter getter... private void readObjectNoData() { this.name = "zhangsan"; } } Person extends Animal public class Person extends Animal implements Serializable { private int age; public Person() { } // setter getter... } 反序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("c:/person.ser")); Person sp = (Person) ois.readObject(); System.out.println(sp.getName()); readObject時, 會調用readObjectNoData
readObjectNoData在我理解看來像是一種異常處理機制,用來在序列化的流不完整的情況下返回正確的值。
使用 writeReplace和readResolve
writeReplace和readResolve是一種更徹底的序列化的機制,它甚至可以將序列化的目標對象替換為其它的對象。
但是與writeObject和readObject不同的是,這二者不是必須要一起使用的,而且盡量應分開使用。若一起使用的話,只有writeReplace會生效。
代碼可以說明一切,首先是writeReplace:
package com.zhyea.test; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; import java.io.Serializable; /** * 序列化測試類 * * @author robin * @date 2014年12月18日 */ public class SerialTest { public static void main(String[] args) { Person robin = new Person("robin", 29); String savePath = "D:\\object.txt"; SerialTest test = new SerialTest(); try { //序列化 test.serialize(savePath, robin); //反序列化 String person = (String) test.deSerialize(savePath); System.out.println(person); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 實現序列化 * * @param obj * 要被序列化保存的對象 * @param path * 保存地址 * @throws IOException */ public void serialize(String path, Person ... obj) throws IOException { ObjectOutputStream oos = null; .... } /** * 反序列化取出對象 * * @param path * 被序列化對象保存的位置 * @return * @throws IOException * @throws ClassNotFoundException */ public Object deSerialize(String path) throws IOException, ClassNotFoundException { .... } } /** * Person類,可序列化 * * @author robin * @date 2014年12月18日 */ class Person implements Serializable { private static final long serialVersionUID = -6412852654889352693L; /** * 姓名 */ private String name; /** * 年齡 */ private int age; public Person() {} public Person(String name, int age) { this.name = name; this.age = age; } /* set和get方法請自行添加 */ private Object writeReplace() throws ObjectStreamException{ System.out.println("my writeReplace"); return "robin"; } private Object readResolve() throws ObjectStreamException{ System.out.println("my readResolve"); return "zhangsan"; } private void writeObject(ObjectOutputStream out) throws IOException{ .... } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{ .... } }
以下是運行結果:
在Person類中,保留了之前的writeObject和readObject方法,並且還添加了readResolve方法。但是從運行結果可以看出來,這3個方法都沒有被調用,只有writeReplace方法被使用。可以理解為當使用writeReplace時,其他的自定義方法都不會被調用,即writeReplace的優先級最高。
現在注釋掉writeReplace方法,再次執行,結果如下:
這次writeObject,readObject和readResolve方法都被調用。readResolve方法緊跟着readObject方法被調用且最終返回的值是readResolve返回的值,readObject里反序列化生成的對象被拋棄。
此外還有一點需要說明:writeReplace和readResolve可以使用任何作用域,這意味着子類也可以調用超類的這兩個方法。但是如果子類還有不同的序列化及反序列化需求,這就需要子類重寫這個方法,有些時候這樣做是沒有必要的。因此一般情況下將這兩個方法的作用域設置為private。
使用Externalizable
一開始有提到過實現Externalizable接口也可以實現類的序列化。使用這種方法,可以由開發者完全決定如何序列化和反序列化目標對象。Externalizable接口提供了writeExternal和readExternal兩個方法。
實際上這種方法和前面的自定義序列化方法很相似,只是Externalizable強制自定義序列化。在使用了Externalizable的類中仍可以使用writeReplace和readResolve方法。使用Externalizable進行序列化較之使用Serializable性能略好,但是復雜度較高。
版本問題
執行序列化和反序列化時有可能會遇到JRE版本問題。尤其是在網絡的兩端進行通信時,這種情況更為多見。
為了解決這種問題,Java允許為序列化的類提供一個serialVersionUID的常量標識該類的版本。只要serialVersionUID的值不變,Java就會把它們當作相同的序列化版本。
如果不顯式定義serialVersionUID,那么JVM就會計算出一個serialVersionUID的值。不同的編譯器下會產生不同的serialVersionUID值。serialVersionUID值不同則會導致編譯失敗。可以使用jdk的bin目錄下的serial.exe查看可序列化類的serialVersionUID,指令如下:
serial Person
如果對類的修改確實會導致反序列化失敗,則應主動調整serialVersionUID的值。導致類的反序列化失敗的修改有以下幾種情形:
- 只是修改了類的方法,不會影響反序列化。
- 只是修改了類的static Field或transient Field,不會影響反序列化。
- 修改了類的非static和非transient Field,會影響序列化。
序列化注意事項
關於對象的序列化,總結下注意事項:
- 對象的類名、Field(包括基本類型、數組及對其他對象的引用)都會被序列化,對象的static Field,transient Field及方法不會被序列化;
- 實現Serializable接口的類,如不想某個Field被序列化,可以使用transient關鍵字進行修飾;
- 保證序列化對象的引用類型Filed的類也是可序列化的,如不可序列化,可以使用transient關鍵字進行修飾,否則會序列化失敗;
- 反序列化時必須要有序列化對象的類的class文件;
- 當通過文件網絡讀取序列化對象的時候,必需按寫入的順序來讀取。