Java程序員必備:序列化全方位解析
前言
相信大家日常開(kāi)發(fā)中,經(jīng)??吹絁ava對(duì)象“implements Serializable”。那么,它到底有什么用呢?本文從以下幾個(gè)角度來(lái)解析序列這一塊知識(shí)點(diǎn)~
- 什么是Java序列化?
 - 為什么需要序列化?
 - 序列化用途
 - Java序列化常用API
 - 序列化的使用
 - 序列化底層
 - 日常開(kāi)發(fā)序列化的注意點(diǎn)
 - 序列化常見(jiàn)面試題
 
一、什么是Java序列化?
- 序列化:把Java對(duì)象轉(zhuǎn)換為字節(jié)序列的過(guò)程
 - 反序列:把字節(jié)序列恢復(fù)為Java對(duì)象的過(guò)程
 
二、為什么需要序列化?
Java對(duì)象是運(yùn)行在JVM的堆內(nèi)存中的,如果JVM停止后,它的生命也就戛然而止。
如果想在JVM停止后,把這些對(duì)象保存到磁盤(pán)或者通過(guò)網(wǎng)絡(luò)傳輸?shù)搅硪贿h(yuǎn)程機(jī)器,怎么辦呢?磁盤(pán)這些硬件可不認(rèn)識(shí)Java對(duì)象,它們只認(rèn)識(shí)二進(jìn)制這些機(jī)器語(yǔ)言,所以我們就要把這些對(duì)象轉(zhuǎn)化為字節(jié)數(shù)組,這個(gè)過(guò)程就是序列化啦~
打個(gè)比喻,作為大城市漂泊的碼農(nóng),搬家是常態(tài)。當(dāng)我們搬書(shū)桌時(shí),桌子太大了就通不過(guò)比較小的門(mén),因此我們需要把它拆開(kāi)再搬過(guò)去,這個(gè)拆桌子的過(guò)程就是序列化。而我們把書(shū)桌復(fù)原回來(lái)(安裝)的過(guò)程就是反序列化啦。
三、序列化用途
序列化使得對(duì)象可以脫離程序運(yùn)行而獨(dú)立存在,它主要有兩種用途:
(1)序列化機(jī)制可以讓對(duì)象地保存到硬盤(pán)上,減輕內(nèi)存壓力的同時(shí),也起了持久化的作用;
比如 Web服務(wù)器中的Session對(duì)象,當(dāng)有 10+萬(wàn)用戶并發(fā)訪問(wèn)的,就有可能出現(xiàn)10萬(wàn)個(gè)Session對(duì)象,內(nèi)存可能消化不良,于是Web容器就會(huì)把一些seesion先序列化到硬盤(pán)中,等要用了,再把保存在硬盤(pán)中的對(duì)象還原到內(nèi)存中。
(2)序列化機(jī)制讓Java對(duì)象在網(wǎng)絡(luò)傳輸不再是天方夜譚。
我們?cè)谑褂肈ubbo遠(yuǎn)程調(diào)用服務(wù)框架時(shí),需要把傳輸?shù)腏ava對(duì)象實(shí)現(xiàn)Serializable接口,即讓Java對(duì)象序列化,因?yàn)檫@樣才能讓對(duì)象在網(wǎng)絡(luò)上傳輸。
四、Java序列化常用API
- java.io.ObjectOutputStream
 - java.io.ObjectInputStream
 - java.io.Serializable
 - java.io.Externalizable
 
Serializable 接口
Serializable接口是一個(gè)標(biāo)記接口,沒(méi)有方法或字段。一旦實(shí)現(xiàn)了此接口,就標(biāo)志該類的對(duì)象就是可序列化的。
- public interface Serializable {
 - }
 
Externalizable 接口
Externalizable繼承了Serializable接口,還定義了兩個(gè)抽象方法:writeExternal()和readExternal(),如果開(kāi)發(fā)人員使用Externalizable來(lái)實(shí)現(xiàn)序列化和反序列化,需要重寫(xiě)writeExternal()和readExternal()方法
- public interface Externalizable extends java.io.Serializable {
 - void writeExternal(ObjectOutput out) throws IOException;
 - void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
 - }
 
java.io.ObjectOutputStream類
表示對(duì)象輸出流,它的writeObject(Object obj)方法可以對(duì)指定obj對(duì)象參數(shù)進(jìn)行序列化,再把得到的字節(jié)序列寫(xiě)到一個(gè)目標(biāo)輸出流中。
java.io.ObjectInputStream
表示對(duì)象輸入流,它的readObject()方法,從輸入流中讀取到字節(jié)序列,反序列化成為一個(gè)對(duì)象,最后將其返回。
五、序列化的使用
序列化如何使用?來(lái)看一下,序列化的使用的幾個(gè)關(guān)鍵點(diǎn)吧:
- 聲明一個(gè)實(shí)體類,實(shí)現(xiàn)Serializable接口
 - 使用ObjectOutputStream類的writeObject方法,實(shí)現(xiàn)序列化
 - 使用ObjectInputStream類的readObject方法,實(shí)現(xiàn)反序列化
 
聲明一個(gè)Student類,實(shí)現(xiàn)Serializable
- public class Student implements Serializable {
 - private Integer age;
 - private String name;
 - public Integer getAge() {
 - return age;
 - }
 - public void setAge(Integer age) {
 - this.age = age;
 - }
 - public String getName() {
 - return name;
 - }
 - public void setName(String name) {
 - this.name = name;
 - }
 - }
 
使用ObjectOutputStream類的writeObject方法,對(duì)Student對(duì)象實(shí)現(xiàn)序列化
把Student對(duì)象設(shè)置值后,寫(xiě)入一個(gè)文件,即序列化,哈哈
- ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:\\text.out"));
 - Student student = new Student();
 - student.setAge(25);
 - student.setName("jayWei");
 - objectOutputStream.writeObject(student);
 - objectOutputStream.flush();
 - objectOutputStream.close();
 
看看序列化的可愛(ài)模樣吧,test.out文件內(nèi)容如下(使用UltraEdit打開(kāi)):
使用ObjectInputStream類的readObject方法,實(shí)現(xiàn)反序列化,重新生成student對(duì)象
再把test.out文件讀取出來(lái),反序列化為Student對(duì)象
- ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
 - Student student = (Student) objectInputStream.readObject();
 - System.out.println("name="+student.getName());
 
六、序列化底層
Serializable底層
Serializable接口,只是一個(gè)空的接口,沒(méi)有方法或字段,為什么這么神奇,實(shí)現(xiàn)了它就可以讓對(duì)象序列化了?
- public interface Serializable {
 - }
 
為了驗(yàn)證Serializable的作用,把以上demo的Student對(duì)象,去掉實(shí)現(xiàn)Serializable接口,看序列化過(guò)程怎樣吧。
序列化過(guò)程中拋出異常啦,堆棧信息如下:
- Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student
 - at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
 - at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
 - at com.example.demo.Test.main(Test.java:13)
 
順著堆棧信息看一下,原來(lái)有重大發(fā)現(xiàn),如下:
原來(lái)底層是這樣:ObjectOutputStream 在序列化的時(shí)候,會(huì)判斷被序列化的Object是哪一種類型,String?array?enum?還是 Serializable,如果都不是的話,拋出 NotSerializableException異常。所以呀,Serializable真的只是一個(gè)標(biāo)志,一個(gè)序列化標(biāo)志。
writeObject(Object)
序列化的方法就是writeObject,基于以上的demo,我們來(lái)分析一波它的核心方法調(diào)用鏈吧~(建議大家也去debug看一下這個(gè)方法,感興趣的話)
writeObject直接調(diào)用的就是writeObject0()方法,
- public final void writeObject(Object obj) throws IOException {
 - ......
 - writeObject0(obj, false);
 - ......
 - }
 
writeObject0 主要實(shí)現(xiàn)是對(duì)象的不同類型,調(diào)用不同的方法寫(xiě)入序列化數(shù)據(jù),這里面如果對(duì)象實(shí)現(xiàn)了Serializable接口,就調(diào)用writeOrdinaryObject()方法。
- private void writeObject0(Object obj, boolean unshared)
 - throws IOException
 - {
 - ......
 - //String類型
 - if (obj instanceof String) {
 - writeString((String) obj, unshared);
 - //數(shù)組類型
 - } else if (cl.isArray()) {
 - writeArray(obj, desc, unshared);
 - //枚舉類型
 - } else if (obj instanceof Enum) {
 - writeEnum((Enum<?>) obj, desc, unshared);
 - //Serializable實(shí)現(xiàn)序列化接口
 - } else if (obj instanceof Serializable) {
 - writeOrdinaryObject(obj, desc, unshared);
 - } else{
 - //其他情況會(huì)拋異常~
 - if (extendedDebugInfo) {
 - throw new NotSerializableException(
 - cl.getName() + "\n" + debugInfoStack.toString());
 - } else {
 - throw new NotSerializableException(cl.getName());
 - }
 - }
 - ......
 
writeOrdinaryObject()會(huì)先調(diào)用writeClassDesc(desc),寫(xiě)入該類的生成信息,然后調(diào)用writeSerialData方法,寫(xiě)入序列化數(shù)據(jù)。
- private void writeSerialData(Object obj, ObjectStreamClass desc)
 - throws IOException
 - {
 - for (int i = 0; i < slots.length; i++) {
 - if (slotDesc.hasWriteObjectMethod()) {
 - //如果被序列化的對(duì)象自定義實(shí)現(xiàn)了writeObject()方法,則執(zhí)行這個(gè)代碼塊
 - slotDesc.invokeWriteObject(obj, this);
 - } else {
 - // 調(diào)用默認(rèn)的方法寫(xiě)入實(shí)例數(shù)據(jù)
 - defaultWriteFields(obj, slotDesc);
 - }
 - }
 - }
 
writeSerialData()實(shí)現(xiàn)的就是寫(xiě)入被序列化對(duì)象的字段數(shù)據(jù),它會(huì)調(diào)用defaultWriteFields方法。
- private void writeSerialData(Object obj, ObjectStreamClass desc)
 - throws IOException
 - {
 - for (int i = 0; i < slots.length; i++) {
 - if (slotDesc.hasWriteObjectMethod()) {
 - //如果被序列化的對(duì)象自定義實(shí)現(xiàn)了writeObject()方法,則執(zhí)行這個(gè)代碼塊
 - slotDesc.invokeWriteObject(obj, this);
 - } else {
 - // 調(diào)用默認(rèn)的方法寫(xiě)入實(shí)例數(shù)據(jù)
 - defaultWriteFields(obj, slotDesc);
 - }
 - }
 - }
 
defaultWriteFields()方法,獲取類的基本數(shù)據(jù)類型數(shù)據(jù),直接寫(xiě)入底層字節(jié)容器;獲取類的obj類型數(shù)據(jù),循環(huán)遞歸調(diào)用writeObject0()方法,寫(xiě)入數(shù)據(jù)。
- private void defaultWriteFields(Object obj, ObjectStreamClass desc)
 - throws IOException
 - {
 - // 獲取類的基本數(shù)據(jù)類型數(shù)據(jù),保存到primVals字節(jié)數(shù)組
 - desc.getPrimFieldValues(obj, primVals);
 - //primVals的基本類型數(shù)據(jù)寫(xiě)到底層字節(jié)容器
 - bout.write(primVals, 0, primDataSize, false);
 - // 獲取對(duì)應(yīng)類的所有字段對(duì)象
 - ObjectStreamField[] fields = desc.getFields(false);
 - Object[] objVals = new Object[desc.getNumObjFields()];
 - int numPrimFields = fields.length - objVals.length;
 - // 獲取類的obj類型數(shù)據(jù),保存到objVals字節(jié)數(shù)組
 - desc.getObjFieldValues(obj, objVals);
 - //對(duì)所有Object類型的字段,循環(huán)
 - for (int i = 0; i < objVals.length; i++) {
 - ......
 - //遞歸調(diào)用writeObject0()方法,寫(xiě)入對(duì)應(yīng)的數(shù)據(jù)
 - writeObject0(objVals[i],
 - fields[numPrimFields + i].isUnshared());
 - ......
 - }
 - }
 
七、日常開(kāi)發(fā)序列化的一些注意點(diǎn)
- static靜態(tài)變量和transient 修飾的字段是不會(huì)被序列化的
 - serialVersionUID問(wèn)題
 - 如果某個(gè)序列化類的成員變量是對(duì)象類型,則該對(duì)象類型的類必須實(shí)現(xiàn)序列化
 - 子類實(shí)現(xiàn)了序列化,父類沒(méi)有實(shí)現(xiàn)序列化,父類中的字段會(huì)丟失~
 
static靜態(tài)變量和transient 修飾的字段是不會(huì)被序列化的
static靜態(tài)變量和transient 修飾的字段是不會(huì)被序列化的,我們來(lái)看例子分析一波~ Student類加了一個(gè)類變量gender和一個(gè)transient修飾的字段specialty
- public class Student implements Serializable {
 - private Integer age;
 - private String name;
 - public static String gender = "男";
 - transient String specialty = "計(jì)算機(jī)專業(yè)";
 - public String getSpecialty() {
 - return specialty;
 - }
 - public void setSpecialty(String specialty) {
 - this.specialty = specialty;
 - }
 - @Override
 - public String toString() {
 - return "Student{" +"age=" + age + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", specialty='" + specialty + '\'' +
 - '}';
 - }
 - ......
 
打印學(xué)生對(duì)象,序列化到文件,接著修改靜態(tài)變量的值,再反序列化,輸出反序列化后的對(duì)象~
運(yùn)行結(jié)果:
- 序列化前Student{age=25, name='jayWei', gender='男', specialty='計(jì)算機(jī)專業(yè)'}
 - 序列化后Student{age=25, name='jayWei', gender='女', specialty='null'}
 
對(duì)比結(jié)果可以發(fā)現(xiàn):
(1)序列化前的靜態(tài)變量性別明明是‘男’,序列化后再在程序中修改,反序列化后卻變成‘女’了,what?顯然這個(gè)靜態(tài)屬性并沒(méi)有進(jìn)行序列化。其實(shí),靜態(tài)(static)成員變量是屬于類級(jí)別的,而序列化是針對(duì)對(duì)象的~所以不能序列化哦。
(2)經(jīng)過(guò)序列化和反序列化過(guò)程后,specialty字段變量值由'計(jì)算機(jī)專業(yè)'變?yōu)榭樟?,為什么?其實(shí)是因?yàn)閠ransient關(guān)鍵字,它可以阻止修飾的字段被序列化到文件中,在被反序列化后,transient 字段的值被設(shè)為初始值,比如int型的值會(huì)被設(shè)置為 0,對(duì)象型初始值會(huì)被設(shè)置為null。
serialVersionUID問(wèn)題
serialVersionUID 表面意思就是序列化版本號(hào)ID,其實(shí)每一個(gè)實(shí)現(xiàn)Serializable接口的類,都有一個(gè)表示序列化版本標(biāo)識(shí)符的靜態(tài)變量,或者默認(rèn)等于1L,或者等于對(duì)象的哈希碼。
- private static final long serialVersionUID = -6384871967268653799L;
 
serialVersionUID有什么用?
JAVA序列化的機(jī)制是通過(guò)判斷類的serialVersionUID來(lái)驗(yàn)證版本是否一致的。在進(jìn)行反序列化時(shí),JVM會(huì)把傳來(lái)的字節(jié)流中的serialVersionUID和本地相應(yīng)實(shí)體類的serialVersionUID進(jìn)行比較,如果相同,反序列化成功,如果不相同,就拋出InvalidClassException異常。
接下來(lái),我們來(lái)驗(yàn)證一下吧,修改一下Student類,再反序列化操作:
- Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student;
 - local class incompatible: stream classdesc serialVersionUID = 3096644667492403394,
 - local class serialVersionUID = 4429793331949928814
 - at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
 - at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876)
 - at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745)
 - at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)
 - at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)
 - at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
 - at com.example.demo.Test.main(Test.java:20)
 
從日志堆棧異常信息可以看到,文件流中的class和當(dāng)前類路徑中的class不同了,它們的serialVersionUID不相同,所以反序列化拋出InvalidClassException異常。那么,如果確實(shí)需要修改Student類,又想反序列化成功,怎么辦呢?可以手動(dòng)指定serialVersionUID的值,一般可以設(shè)置為1L,或者讓我們的編輯器IDE生成。
- private static final long serialVersionUID = -6564022808907262054L;
 
實(shí)際上,阿里開(kāi)發(fā)手冊(cè),強(qiáng)制要求序列化類新增屬性時(shí),不能修改serialVersionUID字段。
如果某個(gè)序列化類的成員變量是對(duì)象類型,則該對(duì)象類型的類必須實(shí)現(xiàn)序列化。
給Student類添加一個(gè)Teacher類型的成員變量,其中Teacher是沒(méi)有實(shí)現(xiàn)序列化接口的。
- public class Student implements Serializable {
 - private Integer age;
 - private String name;
 - private Teacher teacher;
 - ...
 - }
 - //Teacher 沒(méi)有實(shí)現(xiàn)
 - public class Teacher {
 - ......
 - }
 
序列化運(yùn)行,就報(bào)NotSerializableException異常啦。
- Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher
 - at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
 - at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
 - at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
 - at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
 - at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
 - at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
 - at com.example.demo.Test.main(Test.java:16)
 
其實(shí)這個(gè)可以在上小節(jié)的底層源碼分析找到答案,一個(gè)對(duì)象序列化過(guò)程,會(huì)循環(huán)調(diào)用它的Object類型字段,遞歸調(diào)用序列化的,也就是說(shuō),序列化Student類的時(shí)候,會(huì)對(duì)Teacher類進(jìn)行序列化,但是對(duì)Teacher沒(méi)有實(shí)現(xiàn)序列化接口,因此拋出NotSerializableException異常。所以如果某個(gè)序列化類的成員變量是對(duì)象類型,則該對(duì)象類型的類必須實(shí)現(xiàn)序列化。
子類實(shí)現(xiàn)了Serializable,父類沒(méi)有實(shí)現(xiàn)Serializable接口的話,父類不會(huì)被序列化。
子類Student實(shí)現(xiàn)了Serializable接口,父類User沒(méi)有實(shí)現(xiàn)Serializable接口。
- //父類實(shí)現(xiàn)了Serializable接口
 - public class Student extends User implements Serializable {
 - private Integer age;
 - private String name;
 - }
 - //父類沒(méi)有實(shí)現(xiàn)Serializable接口
 - public class User {
 - String userId;
 - }
 - Student student = new Student();
 - student.setAge(25);
 - student.setName("jayWei");
 - student.setUserId("1");
 - ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\text.out"));
 - objectOutputStream.writeObject(student);
 - objectOutputStream.flush();
 - objectOutputStream.close();
 - //反序列化結(jié)果
 - ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
 - Student student1 = (Student) objectInputStream.readObject();
 - System.out.println(student1.getUserId());
 - //output
 - /**
 - * null
 - */
 
從反序列化結(jié)果,可以發(fā)現(xiàn),父類屬性值丟失了。因此子類實(shí)現(xiàn)了Serializable接口,父類沒(méi)有實(shí)現(xiàn)Serializable接口的話,父類不會(huì)被序列化。
八、序列化常見(jiàn)面試題
- 序列化的底層是怎么實(shí)現(xiàn)的?
 - 序列化時(shí),如何讓某些成員不要序列化?
 - 在 Java 中,Serializable 和 Externalizable 有什么區(qū)別
 - serialVersionUID有什么用?
 - 是否可以自定義序列化過(guò)程, 或者是否可以覆蓋 Java 中的默認(rèn)序列化過(guò)程?
 - 在 Java 序列化期間,哪些變量未序列化?
 
1.序列化的底層是怎么實(shí)現(xiàn)的?
本文第六小節(jié)可以回答這個(gè)問(wèn)題,如回答Serializable關(guān)鍵字作用,序列化標(biāo)志啦,源碼中,它的作用啦~還有,可以回答writeObject幾個(gè)核心方法,如直接寫(xiě)入基本類型,獲取obj類型數(shù)據(jù),循環(huán)遞歸寫(xiě)入,哈哈~
2.序列化時(shí),如何讓某些成員不要序列化?
可以用transient關(guān)鍵字修飾,它可以阻止修飾的字段被序列化到文件中,在被反序列化后,transient 字段的值被設(shè)為初始值,比如int型的值會(huì)被設(shè)置為 0,對(duì)象型初始值會(huì)被設(shè)置為null。
3.在Java中,Serializable 和 Externalizable 有什么區(qū)別
Externalizable繼承了Serializable,給我們提供 writeExternal() 和 readExternal() 方法, 讓我們可以控制 Java的序列化機(jī)制, 不依賴于Java的默認(rèn)序列化。正確實(shí)現(xiàn) Externalizable 接口可以顯著提高應(yīng)用程序的性能。
4.serialVersionUID有什么用?
可以看回本文第七小節(jié)哈,JAVA序列化的機(jī)制是通過(guò)判斷類的serialVersionUID來(lái)驗(yàn)證版本是否一致的。在進(jìn)行反序列化時(shí),JVM會(huì)把傳來(lái)的字節(jié)流中的serialVersionUID和本地相應(yīng)實(shí)體類的serialVersionUID進(jìn)行比較,如果相同,反序列化成功,如果不相同,就拋出InvalidClassException異常。
5.是否可以自定義序列化過(guò)程, 或者是否可以覆蓋 Java 中的默認(rèn)序列化過(guò)程?
可以的。我們都知道,對(duì)于序列化一個(gè)對(duì)象需調(diào)用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 讀取對(duì)象, 但 Java 虛擬機(jī)為你提供的還有一件事, 是定義這兩個(gè)方法。如果在類中定義這兩種方法, 則 JVM 將調(diào)用這兩種方法, 而不是應(yīng)用默認(rèn)序列化機(jī)制。同時(shí),可以聲明這些方法為私有方法,以避免被繼承、重寫(xiě)或重載。
6.在Java序列化期間,哪些變量未序列化?
static靜態(tài)變量和transient 修飾的字段是不會(huì)被序列化的。靜態(tài)(static)成員變量是屬于類級(jí)別的,而序列化是針對(duì)對(duì)象的。transient關(guān)鍵字修飾字段,可以阻止該字段被序列化到文件中。



























 
 
 




 
 
 
 