是時候放棄Java序列化了?。?/h1>
基本概念
Java 序列化和反序列化三連問:
-
什么是 Java 序列化和反序列化?
-
為什么需要 Java 序列化和反序列化?
-
如何實(shí)現(xiàn) Java 序列化和反序列化?
是什么
一句話就能夠說明白什么是 Java 序列化和反序列化?Java 序列化是將 Java 對象轉(zhuǎn)換為字節(jié)序列的過程,而 Java 反序列化則是將字節(jié)序列恢復(fù)為 Java 對象的過程。
-
序列化:任何需要保存到磁盤或者在網(wǎng)絡(luò)進(jìn)行傳輸?shù)?Java 對象都需要支持序列化,序列化后的字節(jié)流保存了 Java 對象的狀態(tài)及相關(guān)的描述信息,反序列化能夠根據(jù)這些信息“復(fù)刻”出一個一模一樣的對象。序列化的核心作用就是對象狀態(tài)的保存。
-
反序列化:反序列化就是根據(jù)磁盤中保存的或者網(wǎng)絡(luò)上傳輸?shù)淖止?jié)流中所保存的對象狀態(tài)和相關(guān)描述信息,通過反序列化重建對象。
所以,從本質(zhì)上來說,序列化就是將對象的狀態(tài)和相關(guān)描述信息按照一定的格式寫入到字節(jié)流中,而反序列化則是從字節(jié)流中重建這個對象。
為什么
為什么需要 Java 序列化和反序列化呢?有兩個原因:
-
持久化。即將該對象保存到磁盤中。一般來說我們是不需要持久化 Java 對象的,但是如果遇到特殊情況,我們需要將 Java 對象持久化到磁盤中,以便于我們在重啟 JVM 時可以重建這些 Java 對象。所以我們可以通過序列化的方式將 Java 對象轉(zhuǎn)換成字節(jié)流,然后將這些字節(jié)流保存到磁盤中實(shí)現(xiàn)持久化。在我們應(yīng)用程序重啟時,可以讀取這些字節(jié)流進(jìn)行反序列化還原 Java 對象。
-
網(wǎng)絡(luò)傳輸:我們都知道網(wǎng)絡(luò)上傳輸?shù)膶ο笫嵌M(jìn)制字節(jié)流,我們是無法傳輸一個 Java 對象給一個應(yīng)用的,所以在傳輸前我們需要對 Java 對象進(jìn)行序列化將其轉(zhuǎn)換為字節(jié)流。而接收方則根據(jù)字節(jié)流中所包含的信息重建該 Java 對象。
怎么做?
在 Java 中,如果一個對象要想實(shí)現(xiàn)序列化,它有兩種方式:
-
實(shí)現(xiàn) Serializable 接口
-
實(shí)現(xiàn) Externalizable 接口
這兩個接口是如何工作的呢?又有什么區(qū)別呢?下面我們分別介紹。
Java 如何實(shí)現(xiàn)序列化和反序列化
Serializable 接口
Serializable 接口只是一個標(biāo)記接口,不用實(shí)現(xiàn)任何方法。一個對象只要實(shí)現(xiàn)了該接口,就意味著該對象是可序列化的。
序列化
Java 對象序列化的步驟如下:
-
對象實(shí)現(xiàn) Serializable 接口
-
創(chuàng)建一個 ObjectOutputStream 輸出流
-
調(diào)用 ObjectOutputStream 對象的 writeObject() 輸出可序列化對象
如下:
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
private String name;
private Integer age;
private Float height;
}
public class Serializable01 {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person01.txt"));
Person person01 = new Person("張三",35,175.4F);
oos.writeObject(person01);
}
}
用 idea 打開 person01.txt 文件就可以得到如下內(nèi)容:
從這個文件中我們基本上可以看清楚 Person01 對象的字節(jié)流的輪廓。
反序列化
Java 反序列化步驟如下:
-
對象實(shí)現(xiàn) Serializable 接口
-
創(chuàng)建一個 ObjectInputStream 對象
-
調(diào)用 ObjectInputStream 對象的 readObject()
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person01.txt"));
Person person011 = (Person01) ois.readObject();
System.out.println("person01.txt 反序列化內(nèi)容:" + person011.toString());
運(yùn)行結(jié)果
person01.txt 反序列化內(nèi)容:Person01(name=張三, age=35, height=175.4)
反序列化生成的對象和序列化的對象內(nèi)容一模一樣,完全還原了序列化時的對象。
成員為引用的序列化
上面的例子 Person 的成員變量都是基本類型,如果成員變量為引用類型呢?
我們?nèi)サ?Person 類實(shí)現(xiàn)的 Serializable 接口,然后定義一個 Women 類。
public class Person {
private String name;
private Integer age;
private Float height;
}
public class Woman implements Serializable {
private String hairColor;
private Person person;
}
我們再來序列化 Woman 這類
public class Serializable02 {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("woman.txt"));
Person person = new Person("李四",30,180F);
Woman woman = new Woman("黃顏色",person);
oos.writeObject(woman);
}
}
執(zhí)行時,你會發(fā)現(xiàn)程序會拋出異常:
java.io.NotSerializableException: com.sike.javacore.serializer.serializable.dto.Person
...
所以,一個可序列化的類,如果它含有引用類型的成員變量,那么這個引用類型也必須是可序列化的。
自定義序列化
有些時候我們并不需要將一個對象的所有屬性全部序列化,這個時候我們可以使用 transient 關(guān)鍵字來選擇不需要序列化的字段。
transient 的作用就是用來標(biāo)識一個成員變量在序列化應(yīng)該被忽略。
public class Person_1 implements Serializable {
private String name;
// 標(biāo)識為 transient
private transient Integer age;
private Float height;
}
將 age 屬性標(biāo)識為 transient。
public class Serializable03 {
public static void main(String[] args) throws Exception {
// 先序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person_1.txt"));
Person_1 person = new Person_1("王五",32,180F);
oos.writeObject(person);
System.out.println("原對象:" + person);
// 再反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person_1.txt"));
Person_1 person1 = (Person_1) ois.readObject();
System.out.println("序列化后對象:" + person1);
}
}
運(yùn)行結(jié)果:
原對象:Person_1(name=王五, age=32, height=180.0)
序列化后對象:Person_1(name=王五, age=null, height=180.0)
從運(yùn)行結(jié)果我們可以看出,用 transient 標(biāo)識的屬性,在進(jìn)行序列化時會將該字段忽略,然后在反序列化的時候,被 transient 標(biāo)識的屬性會被設(shè)置為默認(rèn)值。
Externalizable 接口
一個類除了實(shí)現(xiàn) Serializable 接口外來實(shí)現(xiàn)序列化,還有一種更加靈活的方式來實(shí)現(xiàn)序列化:實(shí)現(xiàn) Externalizable 接口。
Externalizable 接口是 Serializable 的子類,它提供了 writeExternal() 和 readExternal() 方法讓類能夠更加靈活地實(shí)現(xiàn)序列化。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
一個類如果實(shí)現(xiàn)了 Externalizable 接口,即必須要實(shí)現(xiàn) writeExternal() 和 readExternal() 兩個方法。在這兩個方法里面你可以做自己任何想做的事情。
public class Student implements Externalizable {
private String name;
private int age;
private int grade;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age - 2); // 年齡我虛報(bào) 2 歲
// 成績我不報(bào)了
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = (String) in.readObject();
this.age = in.readInt();
}
}
public class Serializable04 {
public static void main(String[] args) throws Exception {
// 先序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.txt"));
Student student = new Student("小明",15,55);
oos.writeObject(student);
System.out.println("序列化對象內(nèi)容:" + student);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.txt"));
Student student1 = (Student) ois.readObject();
System.out.println("序列化后的內(nèi)容:" + student1);
}
}
運(yùn)行結(jié)果:
序列化對象內(nèi)容:Student(name=小明, age=15, grade=55)
序列化后的內(nèi)容:Student(name=小明, age=13, grade=0)
根據(jù)運(yùn)行結(jié)果我們看到,Externalizable 接口可以實(shí)現(xiàn)自定義的序列化和反序列化。
但是使用 Externalizable 接口時要注意,writeExternal() 方法和 readExternal() 的順序要一致,即 writeExternal() 是按照怎么樣的順序來 write 值的,readExternal() 就必須嚴(yán)格按照這個順序來 read ,否則會報(bào)錯。有興趣的小伙伴可以 name 和 age 的順序調(diào)整下,就知道了。
Serializable 和 Externalizable 對比
serializable
Externalizable
系統(tǒng)自動存儲 Java 對象必要的信息
程序員自己來實(shí)現(xiàn) Java 對象的序列化,靈活度更加高
不需要的屬性使用 transient 修飾
不需要的屬性可以不寫入對象
在反序列化的時候不走構(gòu)造方法
反序列化時,先走無參構(gòu)造方法得到一個空對象,在調(diào)用 readExternal() 方法來讀取序列化文件中的內(nèi)容給該空對象賦值
serialVersionUID 版本號
我們先看一個例子。
我們先將 Student 對象序列化到本地磁盤 student.txt 文件中,然后在 Student 類里面增加一個字段,比如 className,用來表示所在的班級,然后再用剛剛已經(jīng)序列化的 student.txt 來反序列化試圖還原 Student 對象,這個時候你會發(fā)現(xiàn)運(yùn)行報(bào)錯,拋出下面的異常:
Exception in thread "main" java.io.InvalidClassException: com.sike.javacore.serializer.serializable.dto.Student; local class incompatible: stream classdesc serialVersionUID = -1065600830313514941, local class serialVersionUID = 2126309100823681
異常信息說明:序列化前后的 serialVersionUID 不一致。一個是 serialVersionUID = -1065600830313514941,另外一個是 serialVersionUID = 2126309100823681。
為什么兩個 serialVersionUID 會不一樣呢?因?yàn)槲覀儗?Student 類做了變更,即所謂的升級。
在我們實(shí)際開發(fā)中,我們的 Class 文件不可能一成不變,它是隨著項(xiàng)目的升級,Class 文件也會 升級,但是我們不能因?yàn)樯壛?Class 類就導(dǎo)致之前的序列化對象無法還原了,我們需要做到升級前后的兼容性。怎么保證呢?顯示聲明 serialVersionUID。
Java 序列化提供了一個 private static final long serialVersionUID = xxxx 的序列化版本號,只要版本號相同,就可以將原來的序列化對象還原。
類的序列化版本號 serialVersionUID 可以隨意指定,如果不指定,則 JVM 會根據(jù)類信息自己生成一個版本號,但是這樣就會無法保證類升級后的序列化了。同時,不指定版本號也不利于 JVM 間的移植,因?yàn)榭赡懿煌?JVM 版本計(jì)算規(guī)則可能就不一樣了,這樣也會導(dǎo)致無法反序列化。所以,凡是實(shí)現(xiàn) Serializable 接口的類,我們都需要顯示聲明一個 serialVersionUID 版本號。
缺點(diǎn)
說實(shí)在話,現(xiàn)在幾乎不會有人使用 Java 原生的序列化了,有如下幾個原因使得我們不得不嫌棄他。
無法跨語言
通過 Java 原生 Serializable 接口與 ObjectOutputStream 實(shí)現(xiàn)的序列化,只能通過 Java 語言自己的ObjectInputStream 來反序列化,其他語言,如 C、Python、Go 等等都無法對其進(jìn)行反序列化,這不很坑么?
同時,跨平臺支持也不是很好,客戶端與服務(wù)端如果因?yàn)?JDK 的版本不同都有可能導(dǎo)致無法進(jìn)行反序列化,這個就更加坑了。
序列化字節(jié)流太大
Java 序列化它需要將類的描述信息和屬性進(jìn)行序列化,如果不這樣做,它根本無法還原,這就會導(dǎo)致序列化字節(jié)流變得很大。我們來做一個比較,一個是 Java 原生序列化,一個是通用的二進(jìn)制編碼。
public class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String userName;
private String nickName;
public byte[] codeC() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] userNameBytes = this.userName.getBytes();
buffer.putInt(userNameBytes.length);
buffer.put(userNameBytes);
byte[] nickNameBytes = this.nickName.getBytes();
buffer.putInt(nickNameBytes.length);
buffer.put(nickNameBytes);
buffer.putLong(this.id);
buffer.flip();
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
}
UserInfo 類有一個 codeC() 方法,該方法返回 UserInfo 的字節(jié)流。
public class Serializable01 {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person01.txt"));
Person person01 = new Person("張三",35,175.4F);
oos.writeObject(person01);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person01.txt"));
Person person011 = (Person) ois.readObject();
System.out.println("person01.txt 反序列化內(nèi)容:" + person011.toString());
}
}
運(yùn)行結(jié)果:
原生 JDK 序列化生成的字節(jié)流大?。?span style="line-height: 26px;">246
UserInfo 對象字節(jié)流大?。?span style="line-height: 26px;">31
有 8 倍的差距,這差距還是有點(diǎn)兒大的。
序列化時間太長
還是上面那個類,我們把上面的程序改下:
public class Serializable05 {
public static void main(String[] args) throws Exception {
UserInfo userInfo = new UserInfo(1001L,"zhangshan","張三");
// 序列化
long startTime = System.currentTimeMillis();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject(userInfo);
out.flush();
out.close();
System.out.println("原生 JDK 序列化消耗時間:" + (System.currentTimeMillis() - startTime));
bout.close();
// 原生字節(jié)碼
startTime = System.currentTimeMillis();
userInfo.codeC();
System.out.println("UserInfo#codeC 消耗時間:" + (System.currentTimeMillis() - startTime));
}
}
運(yùn)行結(jié)果:
原生 JDK 序列化消耗時間:9
UserInfo#codeC 消耗時間:1
這差距依然很巨大?。?/p>
所以,Java 原生序列化這么弱,也不能不讓我們嫌棄他啊?。?!
總結(jié)
下面對 Java 序列化做一個總結(jié)。
-
序列化的目的是為了將 Java 對象的狀態(tài)持久化存儲起來或者在網(wǎng)絡(luò)上傳輸。
-
對象的類名、實(shí)例變量(包括基本類型,數(shù)組,對其他對象的引用)都會被序列化;方法、類變量、transient實(shí)例變量都不會被序列化。
-
如果要序列化的類中包含有引用類型的成員變量,那么該成員變量也需要支持序列化。
-
反序列化時必須要有序列化對象的 Class 文件(這里埋坑了)。
-
對于 Serializable 接口而言,它只是起到一個標(biāo)識作用。實(shí)現(xiàn)了該接口就意味著該類支持序列化。
-
如果我們不想要某個變量被序列化,使用 transient 修飾。
-
對于 Externalizable 接口
-
Externalizable 接口是 Serializable 的子類,它提供了 writeExternal() 和 readExternal() 方法類實(shí)現(xiàn)自定義的序列化和反序列化。
-
writeExternal() 和 readExternal() 兩個方法對屬性的加工順序要一致。
-
建議所有實(shí)現(xiàn)了 Serializable 接口的類都顯示申明 serialVersionUID 版本號。
瀏覽
1195
基本概念
Java 序列化和反序列化三連問:
-
什么是 Java 序列化和反序列化? -
為什么需要 Java 序列化和反序列化? -
如何實(shí)現(xiàn) Java 序列化和反序列化?
是什么
一句話就能夠說明白什么是 Java 序列化和反序列化?Java 序列化是將 Java 對象轉(zhuǎn)換為字節(jié)序列的過程,而 Java 反序列化則是將字節(jié)序列恢復(fù)為 Java 對象的過程。
-
序列化:任何需要保存到磁盤或者在網(wǎng)絡(luò)進(jìn)行傳輸?shù)?Java 對象都需要支持序列化,序列化后的字節(jié)流保存了 Java 對象的狀態(tài)及相關(guān)的描述信息,反序列化能夠根據(jù)這些信息“復(fù)刻”出一個一模一樣的對象。序列化的核心作用就是對象狀態(tài)的保存。 -
反序列化:反序列化就是根據(jù)磁盤中保存的或者網(wǎng)絡(luò)上傳輸?shù)淖止?jié)流中所保存的對象狀態(tài)和相關(guān)描述信息,通過反序列化重建對象。
所以,從本質(zhì)上來說,序列化就是將對象的狀態(tài)和相關(guān)描述信息按照一定的格式寫入到字節(jié)流中,而反序列化則是從字節(jié)流中重建這個對象。
為什么
為什么需要 Java 序列化和反序列化呢?有兩個原因:
-
持久化。即將該對象保存到磁盤中。一般來說我們是不需要持久化 Java 對象的,但是如果遇到特殊情況,我們需要將 Java 對象持久化到磁盤中,以便于我們在重啟 JVM 時可以重建這些 Java 對象。所以我們可以通過序列化的方式將 Java 對象轉(zhuǎn)換成字節(jié)流,然后將這些字節(jié)流保存到磁盤中實(shí)現(xiàn)持久化。在我們應(yīng)用程序重啟時,可以讀取這些字節(jié)流進(jìn)行反序列化還原 Java 對象。 -
網(wǎng)絡(luò)傳輸:我們都知道網(wǎng)絡(luò)上傳輸?shù)膶ο笫嵌M(jìn)制字節(jié)流,我們是無法傳輸一個 Java 對象給一個應(yīng)用的,所以在傳輸前我們需要對 Java 對象進(jìn)行序列化將其轉(zhuǎn)換為字節(jié)流。而接收方則根據(jù)字節(jié)流中所包含的信息重建該 Java 對象。
怎么做?
在 Java 中,如果一個對象要想實(shí)現(xiàn)序列化,它有兩種方式:
-
實(shí)現(xiàn) Serializable 接口 -
實(shí)現(xiàn) Externalizable 接口
這兩個接口是如何工作的呢?又有什么區(qū)別呢?下面我們分別介紹。
Java 如何實(shí)現(xiàn)序列化和反序列化
Serializable 接口
Serializable 接口只是一個標(biāo)記接口,不用實(shí)現(xiàn)任何方法。一個對象只要實(shí)現(xiàn)了該接口,就意味著該對象是可序列化的。
序列化
Java 對象序列化的步驟如下:
-
對象實(shí)現(xiàn) Serializable 接口 -
創(chuàng)建一個 ObjectOutputStream 輸出流 -
調(diào)用 ObjectOutputStream 對象的 writeObject()輸出可序列化對象
如下:
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Person implements Serializable {
private String name;
private Integer age;
private Float height;
}
public class Serializable01 {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person01.txt"));
Person person01 = new Person("張三",35,175.4F);
oos.writeObject(person01);
}
}
用 idea 打開 person01.txt 文件就可以得到如下內(nèi)容:
從這個文件中我們基本上可以看清楚 Person01 對象的字節(jié)流的輪廓。
反序列化
Java 反序列化步驟如下:
-
對象實(shí)現(xiàn) Serializable 接口 -
創(chuàng)建一個 ObjectInputStream 對象 -
調(diào)用 ObjectInputStream 對象的 readObject()
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person01.txt"));
Person person011 = (Person01) ois.readObject();
System.out.println("person01.txt 反序列化內(nèi)容:" + person011.toString());
運(yùn)行結(jié)果
person01.txt 反序列化內(nèi)容:Person01(name=張三, age=35, height=175.4)
反序列化生成的對象和序列化的對象內(nèi)容一模一樣,完全還原了序列化時的對象。
成員為引用的序列化
上面的例子 Person 的成員變量都是基本類型,如果成員變量為引用類型呢?
我們?nèi)サ?Person 類實(shí)現(xiàn)的 Serializable 接口,然后定義一個 Women 類。
public class Person {
private String name;
private Integer age;
private Float height;
}
public class Woman implements Serializable {
private String hairColor;
private Person person;
}
我們再來序列化 Woman 這類
public class Serializable02 {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("woman.txt"));
Person person = new Person("李四",30,180F);
Woman woman = new Woman("黃顏色",person);
oos.writeObject(woman);
}
}
執(zhí)行時,你會發(fā)現(xiàn)程序會拋出異常:
java.io.NotSerializableException: com.sike.javacore.serializer.serializable.dto.Person
...
所以,一個可序列化的類,如果它含有引用類型的成員變量,那么這個引用類型也必須是可序列化的。
自定義序列化
有些時候我們并不需要將一個對象的所有屬性全部序列化,這個時候我們可以使用 transient 關(guān)鍵字來選擇不需要序列化的字段。
transient 的作用就是用來標(biāo)識一個成員變量在序列化應(yīng)該被忽略。
public class Person_1 implements Serializable {
private String name;
// 標(biāo)識為 transient
private transient Integer age;
private Float height;
}
將 age 屬性標(biāo)識為 transient。
public class Serializable03 {
public static void main(String[] args) throws Exception {
// 先序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person_1.txt"));
Person_1 person = new Person_1("王五",32,180F);
oos.writeObject(person);
System.out.println("原對象:" + person);
// 再反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person_1.txt"));
Person_1 person1 = (Person_1) ois.readObject();
System.out.println("序列化后對象:" + person1);
}
}
運(yùn)行結(jié)果:
原對象:Person_1(name=王五, age=32, height=180.0)
序列化后對象:Person_1(name=王五, age=null, height=180.0)
從運(yùn)行結(jié)果我們可以看出,用 transient 標(biāo)識的屬性,在進(jìn)行序列化時會將該字段忽略,然后在反序列化的時候,被 transient 標(biāo)識的屬性會被設(shè)置為默認(rèn)值。
Externalizable 接口
一個類除了實(shí)現(xiàn) Serializable 接口外來實(shí)現(xiàn)序列化,還有一種更加靈活的方式來實(shí)現(xiàn)序列化:實(shí)現(xiàn) Externalizable 接口。
Externalizable 接口是 Serializable 的子類,它提供了 writeExternal() 和 readExternal() 方法讓類能夠更加靈活地實(shí)現(xiàn)序列化。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
一個類如果實(shí)現(xiàn)了 Externalizable 接口,即必須要實(shí)現(xiàn) writeExternal() 和 readExternal() 兩個方法。在這兩個方法里面你可以做自己任何想做的事情。
public class Student implements Externalizable {
private String name;
private int age;
private int grade;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age - 2); // 年齡我虛報(bào) 2 歲
// 成績我不報(bào)了
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = (String) in.readObject();
this.age = in.readInt();
}
}
public class Serializable04 {
public static void main(String[] args) throws Exception {
// 先序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.txt"));
Student student = new Student("小明",15,55);
oos.writeObject(student);
System.out.println("序列化對象內(nèi)容:" + student);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.txt"));
Student student1 = (Student) ois.readObject();
System.out.println("序列化后的內(nèi)容:" + student1);
}
}
運(yùn)行結(jié)果:
序列化對象內(nèi)容:Student(name=小明, age=15, grade=55)
序列化后的內(nèi)容:Student(name=小明, age=13, grade=0)
根據(jù)運(yùn)行結(jié)果我們看到,Externalizable 接口可以實(shí)現(xiàn)自定義的序列化和反序列化。
但是使用 Externalizable 接口時要注意,writeExternal() 方法和 readExternal() 的順序要一致,即 writeExternal() 是按照怎么樣的順序來 write 值的,readExternal() 就必須嚴(yán)格按照這個順序來 read ,否則會報(bào)錯。有興趣的小伙伴可以 name 和 age 的順序調(diào)整下,就知道了。
Serializable 和 Externalizable 對比
| serializable | Externalizable |
|---|---|
| 系統(tǒng)自動存儲 Java 對象必要的信息 | 程序員自己來實(shí)現(xiàn) Java 對象的序列化,靈活度更加高 |
| 不需要的屬性使用 transient 修飾 | 不需要的屬性可以不寫入對象 |
| 在反序列化的時候不走構(gòu)造方法 | 反序列化時,先走無參構(gòu)造方法得到一個空對象,在調(diào)用 readExternal() 方法來讀取序列化文件中的內(nèi)容給該空對象賦值 |
serialVersionUID 版本號
我們先看一個例子。
我們先將 Student 對象序列化到本地磁盤 student.txt 文件中,然后在 Student 類里面增加一個字段,比如 className,用來表示所在的班級,然后再用剛剛已經(jīng)序列化的 student.txt 來反序列化試圖還原 Student 對象,這個時候你會發(fā)現(xiàn)運(yùn)行報(bào)錯,拋出下面的異常:
Exception in thread "main" java.io.InvalidClassException: com.sike.javacore.serializer.serializable.dto.Student; local class incompatible: stream classdesc serialVersionUID = -1065600830313514941, local class serialVersionUID = 2126309100823681
異常信息說明:序列化前后的 serialVersionUID 不一致。一個是 serialVersionUID = -1065600830313514941,另外一個是 serialVersionUID = 2126309100823681。
為什么兩個 serialVersionUID 會不一樣呢?因?yàn)槲覀儗?Student 類做了變更,即所謂的升級。
在我們實(shí)際開發(fā)中,我們的 Class 文件不可能一成不變,它是隨著項(xiàng)目的升級,Class 文件也會 升級,但是我們不能因?yàn)樯壛?Class 類就導(dǎo)致之前的序列化對象無法還原了,我們需要做到升級前后的兼容性。怎么保證呢?顯示聲明 serialVersionUID。
Java 序列化提供了一個 private static final long serialVersionUID = xxxx 的序列化版本號,只要版本號相同,就可以將原來的序列化對象還原。
類的序列化版本號 serialVersionUID 可以隨意指定,如果不指定,則 JVM 會根據(jù)類信息自己生成一個版本號,但是這樣就會無法保證類升級后的序列化了。同時,不指定版本號也不利于 JVM 間的移植,因?yàn)榭赡懿煌?JVM 版本計(jì)算規(guī)則可能就不一樣了,這樣也會導(dǎo)致無法反序列化。所以,凡是實(shí)現(xiàn) Serializable 接口的類,我們都需要顯示聲明一個 serialVersionUID 版本號。
缺點(diǎn)
說實(shí)在話,現(xiàn)在幾乎不會有人使用 Java 原生的序列化了,有如下幾個原因使得我們不得不嫌棄他。
無法跨語言
通過 Java 原生 Serializable 接口與 ObjectOutputStream 實(shí)現(xiàn)的序列化,只能通過 Java 語言自己的ObjectInputStream 來反序列化,其他語言,如 C、Python、Go 等等都無法對其進(jìn)行反序列化,這不很坑么?
同時,跨平臺支持也不是很好,客戶端與服務(wù)端如果因?yàn)?JDK 的版本不同都有可能導(dǎo)致無法進(jìn)行反序列化,這個就更加坑了。
序列化字節(jié)流太大
Java 序列化它需要將類的描述信息和屬性進(jìn)行序列化,如果不這樣做,它根本無法還原,這就會導(dǎo)致序列化字節(jié)流變得很大。我們來做一個比較,一個是 Java 原生序列化,一個是通用的二進(jìn)制編碼。
public class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String userName;
private String nickName;
public byte[] codeC() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] userNameBytes = this.userName.getBytes();
buffer.putInt(userNameBytes.length);
buffer.put(userNameBytes);
byte[] nickNameBytes = this.nickName.getBytes();
buffer.putInt(nickNameBytes.length);
buffer.put(nickNameBytes);
buffer.putLong(this.id);
buffer.flip();
byte[] result = new byte[buffer.remaining()];
buffer.get(result);
return result;
}
}
UserInfo 類有一個 codeC() 方法,該方法返回 UserInfo 的字節(jié)流。
public class Serializable01 {
public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person01.txt"));
Person person01 = new Person("張三",35,175.4F);
oos.writeObject(person01);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person01.txt"));
Person person011 = (Person) ois.readObject();
System.out.println("person01.txt 反序列化內(nèi)容:" + person011.toString());
}
}
運(yùn)行結(jié)果:
原生 JDK 序列化生成的字節(jié)流大?。?span style="line-height: 26px;">246
UserInfo 對象字節(jié)流大?。?span style="line-height: 26px;">31
有 8 倍的差距,這差距還是有點(diǎn)兒大的。
序列化時間太長
還是上面那個類,我們把上面的程序改下:
public class Serializable05 {
public static void main(String[] args) throws Exception {
UserInfo userInfo = new UserInfo(1001L,"zhangshan","張三");
// 序列化
long startTime = System.currentTimeMillis();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bout);
out.writeObject(userInfo);
out.flush();
out.close();
System.out.println("原生 JDK 序列化消耗時間:" + (System.currentTimeMillis() - startTime));
bout.close();
// 原生字節(jié)碼
startTime = System.currentTimeMillis();
userInfo.codeC();
System.out.println("UserInfo#codeC 消耗時間:" + (System.currentTimeMillis() - startTime));
}
}
運(yùn)行結(jié)果:
原生 JDK 序列化消耗時間:9
UserInfo#codeC 消耗時間:1
這差距依然很巨大?。?/p>
所以,Java 原生序列化這么弱,也不能不讓我們嫌棄他啊?。?!
總結(jié)
下面對 Java 序列化做一個總結(jié)。
-
序列化的目的是為了將 Java 對象的狀態(tài)持久化存儲起來或者在網(wǎng)絡(luò)上傳輸。 -
對象的類名、實(shí)例變量(包括基本類型,數(shù)組,對其他對象的引用)都會被序列化;方法、類變量、transient實(shí)例變量都不會被序列化。 -
如果要序列化的類中包含有引用類型的成員變量,那么該成員變量也需要支持序列化。 -
反序列化時必須要有序列化對象的 Class 文件(這里埋坑了)。 -
對于 Serializable 接口而言,它只是起到一個標(biāo)識作用。實(shí)現(xiàn)了該接口就意味著該類支持序列化。 -
如果我們不想要某個變量被序列化,使用 transient 修飾。 -
對于 Externalizable 接口 -
Externalizable 接口是 Serializable 的子類,它提供了 writeExternal()和readExternal()方法類實(shí)現(xiàn)自定義的序列化和反序列化。 -
writeExternal()和readExternal()兩個方法對屬性的加工順序要一致。 -
建議所有實(shí)現(xiàn)了 Serializable 接口的類都顯示申明 serialVersionUID版本號。
