Java對象的序列化與反序列化
序列化與反序列化
序列化 (Serialization)是將對象的狀態(tài)信息轉(zhuǎn)換為可以存儲或傳輸?shù)男问降倪^程。一般將一個對象存儲至一個儲存媒介,例如檔案或是記億體緩沖等。在網(wǎng)絡(luò)傳輸過程中,可以是字節(jié)或是XML等格式。而字節(jié)的或XML編碼格式可以還原完全相等的對象。這個相反的過程又稱為反序列化。
Java對象的序列化與反序列化
在Java中,我們可以通過多種方式來創(chuàng)建對象,并且只要對象沒有被回收我們都可以復(fù)用該對象。但是,我們創(chuàng)建出來的這些Java對象都是存在于JVM的堆內(nèi)存中的。只有JVM處于運行狀態(tài)的時候,這些對象才可能存在。一旦JVM停止運行,這些對象的狀態(tài)也就隨之而丟失了。
但是在真實的應(yīng)用場景中,我們需要將這些對象持久化下來,并且能夠在需要的時候把對象重新讀取出來。Java的對象序列化可以幫助我們實現(xiàn)該功能。
對象序列化機制(object serialization)是Java語言內(nèi)建的一種對象持久化方式,通過對象序列化,可以把對象的狀態(tài)保存為字節(jié)數(shù)組,并且可以在有需要的時候?qū)⑦@個字節(jié)數(shù)組通過反序列化的方式再轉(zhuǎn)換成對象。對象序列化可以很容易的在JVM中的活動對象和字節(jié)數(shù)組(流)之間進行轉(zhuǎn)換。
在Java中,對象的序列化與反序列化被廣泛應(yīng)用到RMI(遠程方法調(diào)用)及網(wǎng)絡(luò)傳輸中。
相關(guān)接口及類
Java為了方便開發(fā)人員將Java對象進行序列化及反序列化提供了一套方便的API來支持。其中包括以下接口和類:
- java.io.Serializable
- java.io.Externalizable
- ObjectOutput
- ObjectInput
- ObjectOutputStream
- ObjectInputStream
Serializable 接口
類通過實現(xiàn) java.io.Serializable 接口以啟用其序列化功能。未實現(xiàn)此接口的類將無法使其任何狀態(tài)序列化或反序列化。可序列化類的所有子類型本身都是可序列化的。序列化接口沒有方法或字段,僅用于標(biāo)識可序列化的語義。
當(dāng)試圖對一個對象進行序列化的時候,如果遇到不支持 Serializable 接口的對象。在此情況下,將拋出 NotSerializableException。
雖然Serializable接口中并沒有定義任何屬性和方法,但是如果一個類想要具備序列化能力也比必須要實現(xiàn)它。其實,主要是因為序列化在真正的執(zhí)行過程中會使用instanceof判斷一個類是否實現(xiàn)類Serializable,如果未實現(xiàn)則直接拋出異常。關(guān)于這部分內(nèi)容,我會單開一篇文章講解。
如果要序列化的類有父類,要想同時將在父類中定義過的變量持久化下來,那么父類也應(yīng)該集成java.io.Serializable接口。
下面是一個實現(xiàn)了java.io.Serializable接口的類
- package com.hollischaung.serialization.SerializableDemos;
- import java.io.Serializable;
- /**
- * Created by hollis on 16/2/17.
- * 實現(xiàn)Serializable接口
- */
- public class User1 implements Serializable {
- private String name;
- private int 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;
- }
- @Override
- public String toString() {
- return "User{" +
- "name='" + name + '\'' +
- ", age=" + age +
- '}';
- }
- }
通過下面的代碼進行序列化及反序列化
- package com.hollischaung.serialization.SerializableDemos;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.ObjectInputStream;
- import java.io.ObjectOutputStream;
- /**
- * Created by hollis on 16/2/17.
- * SerializableDemo1 結(jié)合SerializableDemo2說明 一個類要想被序列化必須實現(xiàn)Serializable接口
- */
- public class SerializableDemo1 {
- public static void main(String[] args) {
- //Initializes The Object
- User1 user = new User1();
- user.setName("hollis");
- user.setAge(23);
- System.out.println(user);
- //Write Obj to File
- try (FileOutputStream fos = new FileOutputStream("tempFile"); ObjectOutputStream oos = new ObjectOutputStream(
- fos)) {
- oos.writeObject(user);
- } catch (IOException e) {
- e.printStackTrace();
- }
- //Read Obj from File
- File file = new File("tempFile");
- try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
- User1 newUser = (User1)ois.readObject();
- System.out.println(newUser);
- } catch (IOException | ClassNotFoundException e) {
- e.printStackTrace();
- }
- }
- }
- //OutPut:
- //User{name='hollis', age=23}
- //User{name='hollis', age=23}
如果你觀察夠細微的話,你可能會發(fā)現(xiàn),我在上面的測試代碼中使用了IO流,但是我并沒有顯示的關(guān)閉他。這其實是Java 7中的新特性try-with-resources。這其實是Java中的一個語法糖,背后原理其實是編譯器幫我們做了關(guān)閉IO流的工作。后面我會單獨出一篇文章介紹下如何使用語法糖提高代碼質(zhì)量。
上面的代碼中,我們將代碼中定義出來的User對象通過序列化的方式保存到文件中,然后再從文件中將他到序列化成Java對象。結(jié)果是我們的對象的屬性均被持久化了下來。
Externalizable接口
除了Serializable 之外,java中還提供了另一個序列化接口Externalizable
為了了解Externalizable接口和Serializable接口的區(qū)別,先來看代碼,我們把上面的代碼改成使用Externalizable的形式。
- package com.hollischaung.serialization.ExternalizableDemos;
- import java.io.Externalizable;
- import java.io.IOException;
- import java.io.ObjectInput;
- import java.io.ObjectOutput;
- /**
- * Created by hollis on 16/2/17.
- * 實現(xiàn)Externalizable接口
- */
- public class User1 implements Externalizable {
- private String name;
- private int 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;
- }
- public void writeExternal(ObjectOutput out) throws IOException {
- }
- public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
- }
- @Override
- public String toString() {
- return "User{" +
- "name='" + name + '\'' +
- ", age=" + age +
- '}';
- }
- }
- package com.hollischaung.serialization.ExternalizableDemos;
- import java.io.*;
- /**
- * Created by hollis on 16/2/17.
- * 對一個實現(xiàn)了Externalizable接口的類進行序列化及反序列化
- */
- public class ExternalizableDemo1 {
- public static void main(String[] args) {
- //Write Obj to file
- User1 user = new User1();
- user.setName("hollis");
- user.setAge(23);
- try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"))){
- oos.writeObject(user);
- } catch (IOException e) {
- e.printStackTrace();
- }
- //Read Obj from file
- File file = new File("tempFile");
- try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))){
- User1 newInstance = (User1) ois.readObject();
- //output
- System.out.println(newInstance);
- } catch (IOException | ClassNotFoundException e ) {
- e.printStackTrace();
- }
- }
- }
- //OutPut:
- //User{name='null', age=0}
通過上面的實例的輸出結(jié)果可以發(fā)現(xiàn),對User1類進行序列化及反序列化之后得到的對象的所有屬性的值都變成了默認值。也就是說,之前的那個對象的狀態(tài)并沒有被持久化下來。這就是Externalizable接口和Serializable接口的區(qū)別:
Externalizable繼承了Serializable,該接口中定義了兩個抽象方法:writeExternal()與readExternal()。當(dāng)使用Externalizable接口來進行序列化與反序列化的時候需要開發(fā)人員重寫writeExternal()與readExternal()方法。
由于上面的代碼中,并沒有在這兩個方法中定義序列化實現(xiàn)細節(jié),所以輸出的內(nèi)容為空。還有一點值得注意:在使用Externalizable進行序列化的時候,在讀取對象時,會調(diào)用被序列化類的無參構(gòu)造器去創(chuàng)建一個新的對象,然后再將被保存對象的字段的值分別填充到新對象中。所以,實現(xiàn)Externalizable接口的類必須要提供一個public的無參的構(gòu)造器。
如果實現(xiàn)了Externalizable接口的類中沒有無參數(shù)的構(gòu)造函數(shù),在運行時會拋出異常:java.io.InvalidClassException。如果一個Java類沒有定義任何構(gòu)造函數(shù),編譯器會幫我們自動添加一個無參的構(gòu)造方法,可是,如果我們在類中定義了一個有參數(shù)的構(gòu)造方法了,編譯器便不會再幫我們創(chuàng)建無參構(gòu)造方法,這點需要注意。
按照要求修改之后代碼如下:
- package com.hollischaung.serialization.ExternalizableDemos;
- import java.io.Externalizable;
- import java.io.IOException;
- import java.io.ObjectInput;
- import java.io.ObjectOutput;
- /**
- * Created by hollis on 16/2/17.
- * 實現(xiàn)Externalizable接口,并實現(xiàn)writeExternal和readExternal方法
- */
- public class User2 implements Externalizable {
- private String name;
- private int 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;
- }
- public void writeExternal(ObjectOutput out) throws IOException {
- out.writeObject(name);
- out.writeInt(age);
- }
- public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
- name = (String) in.readObject();
- age = in.readInt();
- }
- @Override
- public String toString() {
- return "User{" +
- "name='" + name + '\'' +
- ", age=" + age +
- '}';
- }
- }
再執(zhí)行測試得到以下結(jié)果
- //OutPut:
- //User{name='hollis', age=23}
這次,就可以把之前的對象狀態(tài)持久化下來了。
ObjectOutput和ObjectInput 接口
上面的writeExternal方法和readExternal方法分別接收ObjectOutput和ObjectInput類型參數(shù)。這兩個類作用如下。
ObjectInput 擴展自 DataInput 接口以包含對象的讀操作。
DataInput 接口用于從二進制流中讀取字節(jié),并根據(jù)所有 Java 基本類型數(shù)據(jù)進行重構(gòu)。同時還提供根據(jù) UTF-8 修改版格式的數(shù)據(jù)重構(gòu) String 的工具。
對于此接口中的所有數(shù)據(jù)讀取例程來說,如果在讀取所需字節(jié)數(shù)之前已經(jīng)到達文件末尾 (end of file),則將拋出 EOFException(IOException 的一種)。如果因為到達文件末尾以外的其他原因無法讀取字節(jié),則將拋出 IOException 而不是 EOFException。尤其是,在輸入流已關(guān)閉的情況下,將拋出 IOException。
ObjectOutput 擴展 DataOutput 接口以包含對象的寫入操作。
DataOutput 接口用于將數(shù)據(jù)從任意 Java 基本類型轉(zhuǎn)換為一系列字節(jié),并將這些字節(jié)寫入二進制流。同時還提供了一個將 String 轉(zhuǎn)換成 UTF-8 修改版格式并寫入所得到的系列字節(jié)的工具。
對于此接口中寫入字節(jié)的所有方法,如果由于某種原因無法寫入某個字節(jié),則拋出 IOException。
ObjectOutputStream、ObjectInputStream類
通過前面的代碼片段中我們也能知道,我們一般使用ObjectOutputStream的writeObject方法把一個對象進行持久化。再使用ObjectInputStream的readObject從持久化存儲中把對象讀取出來。
更多關(guān)于ObjectInputStream和ObjectOutputStream的相關(guān)知識,我會單獨有一篇文章介紹,敬請期待。
transient 關(guān)鍵字
transient 關(guān)鍵字的作用是控制變量的序列化,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設(shè)為初始值,如 int 型的是 0,對象型的是 null。關(guān)于transient 關(guān)鍵字的拓展同樣下一篇文章介紹。
序列化ID
虛擬機是否允許反序列化,不僅取決于類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID)
序列化 ID 在 Eclipse 下提供了兩種生成策略,一個是固定的 1L,一個是隨機生成一個不重復(fù)的 long 類型數(shù)據(jù)(實際上是使用 JDK 工具生成),在這里有一個建議,如果沒有特殊需求,就是用默認的 1L 就可以,這樣可以確保代碼一致時反序列化成功。那么隨機生成的序列化 ID 有什么作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。