Java對(duì)象的序列化與反序列化
序列化與反序列化
序列化 (Serialization)是將對(duì)象的狀態(tài)信息轉(zhuǎn)換為可以存儲(chǔ)或傳輸?shù)男问降倪^程。一般將一個(gè)對(duì)象存儲(chǔ)至一個(gè)儲(chǔ)存媒介,例如檔案或是記億體緩沖等。在網(wǎng)絡(luò)傳輸過程中,可以是字節(jié)或是XML等格式。而字節(jié)的或XML編碼格式可以還原完全相等的對(duì)象。這個(gè)相反的過程又稱為反序列化。
Java對(duì)象的序列化與反序列化
在Java中,我們可以通過多種方式來創(chuàng)建對(duì)象,并且只要對(duì)象沒有被回收我們都可以復(fù)用該對(duì)象。但是,我們創(chuàng)建出來的這些Java對(duì)象都是存在于JVM的堆內(nèi)存中的。只有JVM處于運(yùn)行狀態(tài)的時(shí)候,這些對(duì)象才可能存在。一旦JVM停止運(yùn)行,這些對(duì)象的狀態(tài)也就隨之而丟失了。
但是在真實(shí)的應(yīng)用場(chǎng)景中,我們需要將這些對(duì)象持久化下來,并且能夠在需要的時(shí)候把對(duì)象重新讀取出來。Java的對(duì)象序列化可以幫助我們實(shí)現(xiàn)該功能。
對(duì)象序列化機(jī)制(object serialization)是Java語言內(nèi)建的一種對(duì)象持久化方式,通過對(duì)象序列化,可以把對(duì)象的狀態(tài)保存為字節(jié)數(shù)組,并且可以在有需要的時(shí)候?qū)⑦@個(gè)字節(jié)數(shù)組通過反序列化的方式再轉(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為了方便開發(fā)人員將Java對(duì)象進(jìn)行序列化及反序列化提供了一套方便的API來支持。其中包括以下接口和類:
- java.io.Serializable
- java.io.Externalizable
- ObjectOutput
- ObjectInput
- ObjectOutputStream
- ObjectInputStream
Serializable 接口
類通過實(shí)現(xiàn) java.io.Serializable 接口以啟用其序列化功能。未實(shí)現(xiàn)此接口的類將無法使其任何狀態(tài)序列化或反序列化??尚蛄谢惖乃凶宇愋捅旧矶际强尚蛄谢?。序列化接口沒有方法或字段,僅用于標(biāo)識(shí)可序列化的語義。
當(dāng)試圖對(duì)一個(gè)對(duì)象進(jìn)行序列化的時(shí)候,如果遇到不支持 Serializable 接口的對(duì)象。在此情況下,將拋出 NotSerializableException。
雖然Serializable接口中并沒有定義任何屬性和方法,但是如果一個(gè)類想要具備序列化能力也比必須要實(shí)現(xiàn)它。其實(shí),主要是因?yàn)樾蛄谢谡嬲膱?zhí)行過程中會(huì)使用instanceof判斷一個(gè)類是否實(shí)現(xiàn)類Serializable,如果未實(shí)現(xiàn)則直接拋出異常。關(guān)于這部分內(nèi)容,我會(huì)單開一篇文章講解。
如果要序列化的類有父類,要想同時(shí)將在父類中定義過的變量持久化下來,那么父類也應(yīng)該集成java.io.Serializable接口。
下面是一個(gè)實(shí)現(xiàn)了java.io.Serializable接口的類
- package com.hollischaung.serialization.SerializableDemos;
- import java.io.Serializable;
- /**
- * Created by hollis on 16/2/17.
- * 實(shí)現(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 +
- '}';
- }
- }
通過下面的代碼進(jìn)行序列化及反序列化
- 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說明 一個(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
- 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}
如果你觀察夠細(xì)微的話,你可能會(huì)發(fā)現(xiàn),我在上面的測(cè)試代碼中使用了IO流,但是我并沒有顯示的關(guān)閉他。這其實(shí)是Java 7中的新特性try-with-resources。這其實(shí)是Java中的一個(gè)語法糖,背后原理其實(shí)是編譯器幫我們做了關(guān)閉IO流的工作。后面我會(huì)單獨(dú)出一篇文章介紹下如何使用語法糖提高代碼質(zhì)量。
上面的代碼中,我們將代碼中定義出來的User對(duì)象通過序列化的方式保存到文件中,然后再從文件中將他到序列化成Java對(duì)象。結(jié)果是我們的對(duì)象的屬性均被持久化了下來。
Externalizable接口
除了Serializable 之外,java中還提供了另一個(gè)序列化接口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.
- * 實(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 +
- '}';
- }
- }
- package com.hollischaung.serialization.ExternalizableDemos;
- import java.io.*;
- /**
- * Created by hollis on 16/2/17.
- * 對(duì)一個(gè)實(shí)現(xiàn)了Externalizable接口的類進(jìn)行序列化及反序列化
- */
- 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}
通過上面的實(shí)例的輸出結(jié)果可以發(fā)現(xiàn),對(duì)User1類進(jìn)行序列化及反序列化之后得到的對(duì)象的所有屬性的值都變成了默認(rèn)值。也就是說,之前的那個(gè)對(duì)象的狀態(tài)并沒有被持久化下來。這就是Externalizable接口和Serializable接口的區(qū)別:
Externalizable繼承了Serializable,該接口中定義了兩個(gè)抽象方法:writeExternal()與readExternal()。當(dāng)使用Externalizable接口來進(jìn)行序列化與反序列化的時(shí)候需要開發(fā)人員重寫writeExternal()與readExternal()方法。
由于上面的代碼中,并沒有在這兩個(gè)方法中定義序列化實(shí)現(xiàn)細(xì)節(jié),所以輸出的內(nèi)容為空。還有一點(diǎn)值得注意:在使用Externalizable進(jìn)行序列化的時(shí)候,在讀取對(duì)象時(shí),會(huì)調(diào)用被序列化類的無參構(gòu)造器去創(chuàng)建一個(gè)新的對(duì)象,然后再將被保存對(duì)象的字段的值分別填充到新對(duì)象中。所以,實(shí)現(xiàn)Externalizable接口的類必須要提供一個(gè)public的無參的構(gòu)造器。
如果實(shí)現(xiàn)了Externalizable接口的類中沒有無參數(shù)的構(gòu)造函數(shù),在運(yùn)行時(shí)會(huì)拋出異常:java.io.InvalidClassException。如果一個(gè)Java類沒有定義任何構(gòu)造函數(shù),編譯器會(huì)幫我們自動(dòng)添加一個(gè)無參的構(gòu)造方法,可是,如果我們?cè)陬愔卸x了一個(gè)有參數(shù)的構(gòu)造方法了,編譯器便不會(huì)再幫我們創(chuàng)建無參構(gòu)造方法,這點(diǎn)需要注意。
按照要求修改之后代碼如下:
- 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.
- * 實(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 +
- '}';
- }
- }
再執(zhí)行測(cè)試得到以下結(jié)果
- //OutPut:
- //User{name='hollis', age=23}
這次,就可以把之前的對(duì)象狀態(tài)持久化下來了。
ObjectOutput和ObjectInput 接口
上面的writeExternal方法和readExternal方法分別接收ObjectOutput和ObjectInput類型參數(shù)。這兩個(gè)類作用如下。
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ù)讀取例程來說,如果在讀取所需字節(jié)數(shù)之前已經(jīng)到達(dá)文件末尾 (end of file),則將拋出 EOFException(IOException 的一種)。如果因?yàn)榈竭_(dá)文件末尾以外的其他原因無法讀取字節(jié),則將拋出 IOException 而不是 EOFException。尤其是,在輸入流已關(guān)閉的情況下,將拋出 IOException。
ObjectOutput 擴(kuò)展 DataOutput 接口以包含對(duì)象的寫入操作。
DataOutput 接口用于將數(shù)據(jù)從任意 Java 基本類型轉(zhuǎn)換為一系列字節(jié),并將這些字節(jié)寫入二進(jìn)制流。同時(shí)還提供了一個(gè)將 String 轉(zhuǎn)換成 UTF-8 修改版格式并寫入所得到的系列字節(jié)的工具。
對(duì)于此接口中寫入字節(jié)的所有方法,如果由于某種原因無法寫入某個(gè)字節(jié),則拋出 IOException。
ObjectOutputStream、ObjectInputStream類
通過前面的代碼片段中我們也能知道,我們一般使用ObjectOutputStream的writeObject方法把一個(gè)對(duì)象進(jìn)行持久化。再使用ObjectInputStream的readObject從持久化存儲(chǔ)中把對(duì)象讀取出來。
更多關(guān)于ObjectInputStream和ObjectOutputStream的相關(guān)知識(shí),我會(huì)單獨(dú)有一篇文章介紹,敬請(qǐng)期待。
transient 關(guān)鍵字
transient 關(guān)鍵字的作用是控制變量的序列化,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設(shè)為初始值,如 int 型的是 0,對(duì)象型的是 null。關(guān)于transient 關(guān)鍵字的拓展同樣下一篇文章介紹。
序列化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è)建議,如果沒有特殊需求,就是用默認(rèn)的 1L 就可以,這樣可以確保代碼一致時(shí)反序列化成功。那么隨機(jī)生成的序列化 ID 有什么作用呢,有些時(shí)候,通過改變序列化 ID 可以用來限制某些用戶的使用。