Java 序列化與反序列化
上一小節(jié)我們學(xué)習(xí)了 Java 的輸入輸出流,有了這些前置知識(shí)點(diǎn),我們就可以學(xué)習(xí) Java 的序列化了。本小節(jié)將介紹什么是序列化、什么是反序列化、序列化有什么作用,如何實(shí)現(xiàn)序列化與反序列化,Serializable 接口介紹,常用序列化工具介紹等內(nèi)容。了解序列化的用途、學(xué)會(huì)如何進(jìn)行序列化和反序列化操作是本小節(jié)的重點(diǎn)內(nèi)容。
1. 序列化與反序列化
序列化在計(jì)算機(jī)科學(xué)的數(shù)據(jù)處理中,是指將數(shù)據(jù)結(jié)構(gòu)或?qū)ο鬆顟B(tài)轉(zhuǎn)換成可取用格式,以留待后續(xù)在相同或另一臺(tái)計(jì)算機(jī)環(huán)境中,能恢復(fù)原先狀態(tài)的過程。依照序列化格式重新獲取字節(jié)的結(jié)果時(shí),可以利用它來(lái)產(chǎn)生與原始對(duì)象相同語(yǔ)義的副本。
很多編程語(yǔ)言自身就支持序列化操作。Java 語(yǔ)言提供自動(dòng)序列化,序列化(serialize
)就是將對(duì)象轉(zhuǎn)換為字節(jié)流;與之相應(yīng)對(duì)的,反序列化(deserialize
)就是將字節(jié)流轉(zhuǎn)換為對(duì)象。
需要注意的是,Java 序列化對(duì)象時(shí),會(huì)把對(duì)象的狀態(tài)保存成字節(jié)序列,對(duì)象的狀態(tài)指的就是其成員變量,因此序列化的對(duì)象不會(huì)保存類的靜態(tài)變量。
在 Java 中,可通過對(duì)象輸出/輸入流來(lái)實(shí)現(xiàn)序列化/反序列化操作。 java.io
包中,提供了ObjectInputStream
類和ObjectOutputStream
用來(lái)序列化對(duì)象,這兩個(gè)類我們將在下面介紹。下面我們來(lái)介紹一下序列化的作用。
2. 序列化的作用
- 序列化可以將對(duì)象的字節(jié)序列存持久化:可以將其保存在內(nèi)存、文件、數(shù)據(jù)庫(kù)中(見下圖);
- 可以在網(wǎng)絡(luò)上傳輸對(duì)象字節(jié)序列;
- 可用于遠(yuǎn)端程序方法調(diào)用。

3. 實(shí)現(xiàn)序列化
ObjectOutputStream
類下的void writeObject(Object obj)
方法用于將一個(gè)對(duì)象寫入對(duì)象輸出流,也就是序列化;ObjectInputStream
類下的Object readObject()
方法用于讀取一個(gè)對(duì)象到輸入流,也就是反序列化。
實(shí)例代碼如下:
import java.io.*;
public class SerializeDemo1 {
static class Cat implements Serializable {
private static final long serialVersionUID = 1L;
private String nickname;
private Integer age;
public Cat() {}
public Cat(String nickname, Integer age) {
this.nickname = nickname;
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"nickname='" + nickname + '\'' +
", age=" + age +
'}';
}
}
/**
* 序列化方法
* @param filepath 文件路徑
* @param cat 要序列化的對(duì)象
* @throws IOException
*/
private static void serialize(String filepath, Cat cat) throws IOException {
// 實(shí)例化file對(duì)象
File file = new File(filepath);
// 實(shí)例化文件輸出流
FileOutputStream fileOutputStream = new FileOutputStream(file);
// 實(shí)例化對(duì)象輸出流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
// 保存cat對(duì)象
objectOutputStream.writeObject(cat);
// 關(guān)閉流
fileOutputStream.close();
objectOutputStream.close();
}
/**
* 反序列化方法
* @param filepath 文件路徑
* @throws IOException
* @throws ClassNotFoundException
*/
private static void deserialize(String filepath) throws IOException, ClassNotFoundException {
// 實(shí)例化file對(duì)象
File file = new File(filepath);
// 實(shí)例化文件輸入流
FileInputStream fileInputStream = new FileInputStream(file);
// 實(shí)例化對(duì)象輸入流
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Object o = objectInputStream.readObject();
System.out.println(o);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
String filename = "C:\\Users\\Colorful\\Desktop\\imooc\\Hello.txt";
Cat cat = new Cat("豬皮", 1);
serialize(filename, cat);
deserialize(filename);
}
}
運(yùn)行結(jié)果:
Cat{nickname='豬皮', age=1}
上述代碼中,我們定義了一個(gè)Cat
類,它實(shí)現(xiàn)了Serializable
接口,類內(nèi)部有一個(gè)private static final long serialVersionUID = 1L;
,關(guān)于這兩點(diǎn),我們下面緊接著就會(huì)介紹。
除了Cat
類的定義,我們還分別封裝了序列化與反序列化的方法,并在主方法中調(diào)用了這兩個(gè)方法,實(shí)現(xiàn)了cat
對(duì)象的序列化和反序列化操作。
在調(diào)用序列化方法后,你會(huì)發(fā)現(xiàn)磁盤中的Hello.txt
文件中被cat
對(duì)象寫入了序列化后的數(shù)據(jù):

4. Seralizable 接口
被序列化的類必須是Enum
、Array
或Serializable
中的任意一種類型。
如果要序列化的類不是枚舉類型和數(shù)組類型的話,則必須實(shí)現(xiàn)java.io.Seralizable
接口,否則直接序列化將拋出NotSerializableException
異常。
4.1 serialVersionUID
serialVersionUID
是 Java 為每個(gè)序列化類產(chǎn)生的版本標(biāo)識(shí)。它可以用來(lái)保證在反序列化時(shí),發(fā)送方發(fā)送的和接受方接收的是可兼容的對(duì)象。如果接收方接收的類的 serialVersionUID
與發(fā)送方發(fā)送的 serialVersionUID
不一致,會(huì)拋出 InvalidClassException
。
4.2 默認(rèn)序列化機(jī)制
如果僅僅只是讓某個(gè)類實(shí)現(xiàn) Serializable
接口,而沒有其它任何處理的話,那么就會(huì)使用默認(rèn)序列化機(jī)制。
使用默認(rèn)機(jī)制,在序列化對(duì)象時(shí),不僅會(huì)序列化當(dāng)前對(duì)象本身,還會(huì)對(duì)其父類的字段以及該對(duì)象引用的其它對(duì)象也進(jìn)行序列化。同樣地,這些其它對(duì)象引用的另外對(duì)象也將被序列化,以此類推。所以,如果一個(gè)對(duì)象包含的成員變量是容器類對(duì)象,而這些容器所含有的元素也是容器類對(duì)象,那么這個(gè)序列化的過程就會(huì)較復(fù)雜,開銷也較大。
4.3 transient 關(guān)鍵字
在現(xiàn)實(shí)應(yīng)用中,有些時(shí)候不能使用默認(rèn)序列化機(jī)制。比如,希望在序列化過程中忽略掉敏感數(shù)據(jù),或者簡(jiǎn)化序列化過程。下面將介紹若干影響序列化的方法。
當(dāng)某個(gè)字段被聲明為 transient
后,默認(rèn)序列化機(jī)制就會(huì)忽略該字段。
可以嘗試將實(shí)例代碼中Cat
類的成員變量age
聲明為transient
:
// 僅部分代碼
static class Cat implements Serializable {
transient private Integer age;
}
運(yùn)行程序,我們會(huì)發(fā)現(xiàn)成員變量age
沒有被序列化。
5. 常用序列化工具
Java 官方的序列化存在很多缺點(diǎn),因此,開發(fā)者們更傾向于使用優(yōu)秀的第三方序列化工具來(lái)替代 Java 自身的序列化機(jī)制。
Java 官方的序列化主要體現(xiàn)在以下方面:
- 性能問題:序列化后的數(shù)據(jù)相對(duì)于一些優(yōu)秀的序列化的工具,還是要大不少,這大大影響存儲(chǔ)和傳輸?shù)男剩?/li>
- 繁瑣的步驟:Java 官方的序列化一定需要實(shí)現(xiàn)
Serializable
接口,略顯繁瑣,而且需要關(guān)注serialVersionUID
; - 無(wú)法跨語(yǔ)言使用:序列化的很大一個(gè)目的就是用于不同語(yǔ)言來(lái)讀寫數(shù)據(jù)。
下面列舉了一些優(yōu)秀的序列化工具:
6. 小結(jié)
通過本小節(jié)的學(xué)習(xí),我們知道了序列化(serialize
)就是將對(duì)象轉(zhuǎn)換為字節(jié)流,反序列化(deserialize
)就是將字節(jié)流轉(zhuǎn)換為對(duì)象。想要實(shí)現(xiàn)序列化,就必須繼承Seralizable
接口,serialVersionUID
是 Java 為每個(gè)序列化類產(chǎn)生的版本標(biāo)識(shí)。當(dāng)某個(gè)字段被聲明為 transient
后,默認(rèn)序列化機(jī)制就會(huì)忽略該字段。學(xué)會(huì)根據(jù)自己的應(yīng)用場(chǎng)景選擇使用序列化工具。