序列化與反序列化
序列化 (Serialization)是將對(duì)象的狀態(tài)信息轉(zhuǎn)換為可以存儲(chǔ)或傳輸?shù)男问降倪^(guò)程瘫辩。一般將一個(gè)對(duì)象存儲(chǔ)至一個(gè)儲(chǔ)存媒介仲闽,例如檔案或是記億體緩沖等棋傍。在網(wǎng)絡(luò)傳輸過(guò)程中倔毙,可以是字節(jié)或是XML等格式冻璃。而字節(jié)的或XML編碼格式可以還原完全相等的對(duì)象侮穿。這個(gè)相反的過(guò)程又稱為反序列化歌径。
Java對(duì)象的序列化與反序列化
在Java中,我們可以通過(guò)多種方式來(lái)創(chuàng)建對(duì)象亲茅,并且只要對(duì)象沒(méi)有被回收我們都可以復(fù)用該對(duì)象回铛。但是,我們創(chuàng)建出來(lái)的這些Java對(duì)象都是存在于JVM的堆內(nèi)存中的克锣。只有JVM處于運(yùn)行狀態(tài)的時(shí)候茵肃,這些對(duì)象才可能存在。一旦JVM停止運(yùn)行袭祟,這些對(duì)象的狀態(tài)也就隨之而丟失了验残。
但是在真實(shí)的應(yīng)用場(chǎng)景中,我們需要將這些對(duì)象持久化下來(lái)巾乳,并且能夠在需要的時(shí)候把對(duì)象重新讀取出來(lái)您没。Java的對(duì)象序列化可以幫助我們實(shí)現(xiàn)該功能。
對(duì)象序列化機(jī)制(object serialization)是Java語(yǔ)言內(nèi)建的一種對(duì)象持久化方式胆绊,通過(guò)對(duì)象序列化氨鹏,可以把對(duì)象的狀態(tài)保存為字節(jié)數(shù)組,并且可以在有需要的時(shí)候?qū)⑦@個(gè)字節(jié)數(shù)組通過(guò)反序列化的方式再轉(zhuǎn)換成對(duì)象压状。對(duì)象序列化可以很容易的在JVM中的活動(dòng)對(duì)象和字節(jié)數(shù)組(流)之間進(jìn)行轉(zhuǎn)換喻犁。
在Java中,對(duì)象的序列化與反序列化被廣泛應(yīng)用到RMI(遠(yuǎn)程方法調(diào)用)及網(wǎng)絡(luò)傳輸中何缓。
相關(guān)接口及類
Java為了方便開(kāi)發(fā)人員將Java對(duì)象進(jìn)行序列化及反序列化提供了一套方便的API來(lái)支持。其中包括以下接口和類:
java.io.Serializable
java.io.Externalizable
ObjectOutput
ObjectInput
ObjectOutputStream
ObjectInputStream
Serializable 接口
類通過(guò)實(shí)現(xiàn) java.io.Serializable 接口以啟用其序列化功能还栓。未實(shí)現(xiàn)此接口的類將無(wú)法使其任何狀態(tài)序列化或反序列化碌廓。可序列化類的所有子類型本身都是可序列化的剩盒。序列化接口沒(méi)有方法或字段谷婆,僅用于標(biāo)識(shí)可序列化的語(yǔ)義。
當(dāng)試圖對(duì)一個(gè)對(duì)象進(jìn)行序列化的時(shí)候辽聊,如果遇到不支持 Serializable 接口的對(duì)象纪挎。在此情況下,將拋出 NotSerializableException跟匆。
如果要序列化的類有父類异袄,要想同時(shí)將在父類中定義過(guò)的變量持久化下來(lái),那么父類也應(yīng)該集成java.io.Serializable接口玛臂。
下面是一個(gè)實(shí)現(xiàn)了java.io.Serializable接口的類
import java.io.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 +
'}';
}
}
通過(guò)下面的代碼進(jìn)行序列化及反序列化
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
/**
*
* SerializableDemo1 結(jié)合SerializableDemo2說(shuō)明 一個(gè)類要想被序列化必須實(shí)現(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
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(oos);
}
//Read Obj from File
File file = new File("tempFile");
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(file));
User1 newUser = (User1) ois.readObject();
System.out.println(newUser);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//OutPut:
//User{name='hollis', age=23}
//User{name='hollis', age=23}
Externalizable接口
除了Serializable 之外烤蜕,java中還提供了另一個(gè)序列化接口Externalizable封孙,為了了解Externalizable接口和Serializable接口的區(qū)別,先來(lái)看代碼讽营,我們把上面的代碼改成使用Externalizable的形式虎忌。
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
*
* 實(shí)現(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 +
'}';
}
}
import java.io.*;
public class ExternalizableDemo1 {
//為了便于理解和節(jié)省篇幅,忽略關(guān)閉流操作及刪除文件操作橱鹏。真正編碼時(shí)千萬(wàn)不要忘記
//IOException直接拋出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
User1 user = new User1();
user.setName("hollis");
user.setAge(23);
oos.writeObject(user);
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User1 newInstance = (User1) ois.readObject();
//output
System.out.println(newInstance);
}
}
//OutPut:
//User{name='null', age=0}
通過(guò)上面的實(shí)例可以發(fā)現(xiàn)膜蠢,對(duì)User1類進(jìn)行序列化及反序列化之后得到的對(duì)象的所有屬性的值都變成了默認(rèn)值。也就是說(shuō)莉兰,之前的那個(gè)對(duì)象的狀態(tài)并沒(méi)有被持久化下來(lái)挑围。這就是Externalizable接口和Serializable接口的區(qū)別:Externalizable繼承了Serializable,該接口中定義了兩個(gè)抽象方法:
writeExternal()與readExternal()贮勃。當(dāng)使用Externalizable接口來(lái)進(jìn)行序列化與反序列化的時(shí)候需要開(kāi)發(fā)人員重寫(xiě)writeExternal()與readExternal()方法贪惹。由于上面的代碼中,并沒(méi)有在這兩個(gè)方法中定義序列化實(shí)現(xiàn)細(xì)節(jié)寂嘉,所以輸出的內(nèi)容為空奏瞬。還有一點(diǎn)值得注意:在使用Externalizable進(jìn)行序列化的時(shí)候,在讀取對(duì)象時(shí)泉孩,會(huì)調(diào)用被序列化類的無(wú)參構(gòu)造器去創(chuàng)建一個(gè)新的對(duì)象硼端,然后再將被保存對(duì)象的字段的值分別填充到新對(duì)象中。所以寓搬,實(shí)現(xiàn)Externalizable接口的類必須要提供一個(gè)public的無(wú)參的構(gòu)造器珍昨。
按照要求修改之后代碼如下:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
*
* 實(shí)現(xiàn)Externalizable接口,并實(shí)現(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 +
'}';
}
}
import java.io.*;
public class ExternalizableDemo2 {
//為了便于理解和節(jié)省篇幅,忽略關(guān)閉流操作及刪除文件操作句喷。真正編碼時(shí)千萬(wàn)不要忘記
//IOException直接拋出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
User2 user = new User2();
user.setName("hollis");
user.setAge(23);
oos.writeObject(user);
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
User2 newInstance = (User2) ois.readObject();
//output
System.out.println(newInstance);
}
}
//OutPut:
//User{name='hollis', age=23}
這次镣典,就可以把之前的對(duì)象狀態(tài)持久化下來(lái)了。
如果User類中沒(méi)有無(wú)參數(shù)的構(gòu)造函數(shù)唾琼,在運(yùn)行時(shí)會(huì)拋出異常:java.io.InvalidClassException
ObjectOutput 和 ObjectInput 接口
ObjectInput接口 擴(kuò)展自 DataInput 接口以包含對(duì)象的讀操作兄春。
DataInput 接口用于從二進(jìn)制流中讀取字節(jié),并根據(jù)所有 Java 基本類型數(shù)據(jù)進(jìn)行重構(gòu)锡溯。同時(shí)還提供根據(jù) UTF-8 修改版格式的數(shù)據(jù)重構(gòu) String 的工具赶舆。
對(duì)于此接口中的所有數(shù)據(jù)讀取例程來(lái)說(shuō),如果在讀取所需字節(jié)數(shù)之前已經(jīng)到達(dá)文件末尾 (end of file)祭饭,則將拋出 EOFException(IOException 的一種)芜茵。如果因?yàn)榈竭_(dá)文件末尾以外的其他原因無(wú)法讀取字節(jié),則將拋出 IOException 而不是 EOFException倡蝙。尤其是九串,在輸入流已關(guān)閉的情況下,將拋出 IOException寺鸥。
ObjectOutput 擴(kuò)展 DataOutput 接口以包含對(duì)象的寫(xiě)入操作蒸辆。
DataOutput 接口用于將數(shù)據(jù)從任意 Java 基本類型轉(zhuǎn)換為一系列字節(jié)征炼,并將這些字節(jié)寫(xiě)入二進(jìn)制流。同時(shí)還提供了一個(gè)將 String 轉(zhuǎn)換成 UTF-8 修改版格式并寫(xiě)入所得到的系列字節(jié)的工具躬贡。
對(duì)于此接口中寫(xiě)入字節(jié)的所有方法谆奥,如果由于某種原因無(wú)法寫(xiě)入某個(gè)字節(jié),則拋出 IOException拂玻。
ObjectOutputStream 類和 ObjectInputStream 類
通過(guò)前面的代碼片段中我們也能知道酸些,我們一般使用ObjectOutputStream的writeObject
方法把一個(gè)對(duì)象進(jìn)行持久化。再使用ObjectInputStream的readObject
從持久化存儲(chǔ)中把對(duì)象讀取出來(lái)檐蚜。
更多關(guān)于ObjectInputStream和ObjectOutputStream的相關(guān)知識(shí)請(qǐng)閱讀另外兩篇博文:深入分析Java的序列化與反序列化魄懂、單例與序列化的那些事兒
序列化ID
虛擬機(jī)是否允許反序列化啼染,不僅取決于類路徑和功能代碼是否一致韧骗,一個(gè)非常重要的一點(diǎn)是兩個(gè)類的序列化 ID 是否一致(就是 private static final long serialVersionUID)
序列化 ID 在 Eclipse 下提供了兩種生成策略,一個(gè)是固定的 1L织阳,一個(gè)是隨機(jī)生成一個(gè)不重復(fù)的 long 類型數(shù)據(jù)(實(shí)際上是使用 JDK 工具生成)咳短,在這里有一個(gè)建議填帽,如果沒(méi)有特殊需求,就是用默認(rèn)的 1L 就可以咙好,這樣可以確保代碼一致時(shí)反序列化成功篡腌。那么隨機(jī)生成的序列化 ID 有什么作用呢,有些時(shí)候勾效,通過(guò)改變序列化 ID 可以用來(lái)限制某些用戶的使用嘹悼。