Java物件的序列化(Serialization)和反序列化詳解
1.序列化和反序列化
序列化(Serialization)是將物件的狀態資訊轉化為可以儲存或者傳輸的形式的過程,一般將一個物件儲存到一個儲存媒介,例如檔案或記憶體緩衝等,在網路傳輸過程中,可以是位元組或者XML等格式;而位元組或者XML格式的可以還原成完全相等的物件,這個相反的過程又稱為反序列化;
2.Java物件的序列化和反序列化
在Java中,我們可以通過多種方式來建立物件,並且只要物件沒有被回收我們都可以複用此物件。但是,我們創建出來的這些物件都存在於JVM中的堆(stack)記憶體中,只有JVM處於執行狀態的時候,這些物件才可能存在。一旦JVM停止,這些物件也就隨之消失;
但是在真實的應用場景中,我們需要將這些物件持久化下來,並且在需要的時候將物件重新讀取出來,Java的序列化可以幫助我們實現該功能。
物件序列化機制(object serialization)是java語言內建的一種物件持久化方式,通過物件序列化,可以將物件的狀態資訊儲存未位元組陣列,並且可以在有需要的時候將這個位元組陣列通過反序列化的方式轉換成物件,物件的序列化可以很容易的在JVM中的活動物件和位元組陣列(流)之間進行轉換。
在JAVA中,物件的序列化和反序列化被廣泛的應用到RMI(遠端方法呼叫)及網路傳輸中;
3.序列化及反序列化相關介面及類
Java為了方便開發人員將java物件序列化及反序列化提供了一套方便的API來支援,其中包括以下介面和類:
java.io.Serializable
java.io .Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
4.Serialization介面詳解
Java類通過實現java.io.Serialization介面來啟用序列化功能,未實現此介面的類將無法將其任何狀態或者資訊進行序列化或者反序列化。可序列化類的所有子型別都是可以序列化的。序列化介面沒有方法或者欄位,僅用於標識可序列化的語義。
當試圖對一個物件進行序列化時,如果遇到一個沒有實現java.io.Serialization介面的物件時,將丟擲NotSerializationException異常。
如果要序列化的類有父類,要想將在父類中定義過的變數序列化下來,那麼父類也應該實現java.io.Serialization介面。
下面是一個實現了java.io.Serialization介面的類:
package common.lang;
import java.io.Serializable;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
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 new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("name", name)
.append("age", age)
.toString();
}
}
通過下面的程式碼進行序列化及反序列化:
package common.lang;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializableDemo1 {
public static void main(String[] args) throws Exception, IOException {
//初始化物件
User1 user = new User1();
user.setName("yaomy");
user.setAge(23);
System.out.println(user);
//序列化物件到檔案中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("template"));
oos.writeObject(user);
oos.close();
//反序列化
File file = new File("template");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1)ois.readObject();
System.out.println(newUser.toString());
}
}
5.Java還提供了另一個序列化介面java.io.Externalizable
為了瞭解Externalizable介面和Serializable介面的區別先來看程式碼,我們將上面的User1類改為實現java.io.Externalization介面;
package common.lang;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
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;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("name", name)
.append("age", age)
.toString();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// TODO Auto-generated method stub
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// TODO Auto-generated method stub
}
}
package common.lang;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializableDemo1 {
public static void main(String[] args) throws Exception, IOException {
//初始化物件
User1 user = new User1();
user.setName("yaomy");
user.setAge(23);
System.out.println(user);
//序列化物件到檔案中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("template"));
oos.writeObject(user);
oos.close();
//反序列化
File file = new File("template");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1)ois.readObject();
System.out.println(newUser.toString());
ois.close();
}
}
輸出結構是:
common.lang.User1@6ef64f64[
name=yaomy
age=23
]
common.lang.User1@184c9860[
name=<null>
age=0
]
通過上面的例項可以發現,對User1進行序列化然後再進行反序列化之後物件的屬性都恢復成了預設值,也就是說之前的哪個物件的狀態並沒有被持久化下來,這就是Externalization和Serialization介面之間的區別;
Externalizable繼承了Serializable,該介面中定義了兩個抽象方法:writeExternal()與readExternal()。當使用Externalizable介面來進行序列化與反序列化的時候需要開發人員重寫writeExternal()與readExternal()方法。由於上面的程式碼中,並沒有在這兩個方法中定義序列化實現細節,所以輸出的內容為空。還有一點值得注意:在使用Externalizable進行序列化的時候,在讀取物件時,會呼叫被序列化類的無參構造器去建立一個新的物件,然後再將被儲存物件的欄位的值分別填充到新物件中。所以,實現Externalizable介面的類必須要提供一個public的無參的構造器。
按照要求修改之後的程式碼是:
package common.lang;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
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;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("name", name)
.append("age", age)
.toString();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String)in.readObject();
age = in.readInt();
}
}
package common.lang;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SerializableDemo1 {
public static void main(String[] args) throws Exception, IOException {
//初始化物件
User1 user = new User1();
user.setName("yaomy");
user.setAge(23);
System.out.println(user);
//序列化物件到檔案中
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("template"));
oos.writeObject(user);
oos.close();
//反序列化
File file = new File("template");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1)ois.readObject();
System.out.println(newUser.toString());
ois.close();
}
}
輸出結果是:
common.lang.User1@6cd66725[
name=yaomy
age=23
]
common.lang.User1@19160e64[
name=yaomy
age=23
]
這樣就可以將之前的物件狀態儲存下來了,如果User類中沒有無引數的建構函式,在執行時會丟擲異常:java.io.InvalidClassException;
6.靜態變數的序列化
靜態變數序列化程式碼:
public class Test implements Serializable {
private static final long serialVersionUID = 1L;
public static int staticVar = 5;
public static void main(String[] args) {
try {
//初始時staticVar為5
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("result.obj"));
out.writeObject(new Test());
out.close();
//序列化後修改為10
Test.staticVar = 10;
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"result.obj"));
Test t = (Test) oin.readObject();
oin.close();
//再讀取,通過t.staticVar列印新的值
System.out.println(t.staticVar);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
main 方法中,將物件序列化後,修改靜態變數的數值,再將序列化物件讀取出來,然後通過讀取出來的物件獲得靜態變數的數值並打印出來,打印出來的是10還是5?
最後的輸出是 10,對於無法理解的讀者認為,列印的 staticVar 是從讀取的物件裡獲得的,應該是儲存時的狀態才對。之所以列印 10 的原因在於序列化時,並不儲存靜態變數,這其實比較容易理解,序列化儲存的是物件的狀態,靜態變數屬於類的狀態,因此 序列化並不儲存靜態變數。
7.Transient 關鍵字使用
Transient 關鍵字的作用是控制變數的序列化,在變數宣告前加上該關鍵字,可以阻止該變數被序列化到檔案中,在被反序列化後,transient 變數的值被設為初始值,如 int 型的是 0,物件型的是 null。