一、概念
序列化:把創建出來的對象(new出來的對象),以及對象中的成員變量的數據轉化為字節數據,寫到流中,然后存儲到硬盤的文件中。
反序列化:可以把序列化后的對象(硬盤上的文件中的對象數據),讀取到內存中,然后就可以直接使用對象。這樣做的好處是不用再一次創建對象了,直接反序列化就可以了。
使用場景:
- 在創建對象並給所創建的對象賦予了值后,當前創建出來的對象是存放在堆內存中的,當JVM停止后,堆中的對象也被釋放了,如果下一次想要繼續使用之前的對象,需要再次創建對象並賦值。然而使用序列化對象,就可以把創建出來的對象及對象中數據存放到硬盤的文件中,下次使用的時候不用在重新賦值,而是直接讀取使用即可。
- 對象直接轉換為字節的形式進行網絡傳輸
二、相關API介紹
序列化
序列化對象所屬的類是ObjectOutputStream,如下圖所示:

說明:ObjectOutputStream類可以把對象及其數據寫入到流中或網絡中。
構造函數如下圖所示:

寫出功能:

反序列化
反序列化對象所屬的類是ObjectInputStream類:

說明:序列化輸出流對象和反序列化輸出流對象都不具備讀寫能力,分別依賴FileOutputStream和FileInputStream類來進行讀寫文件。
構造函數如下圖所示:

讀取功能:

三、實戰
序列化
需求:把Student類創建的對象持久化保存。
分析和步驟:
1)自定義一個Student類,並定義name和age屬性;
2)定義一個測試類,在這個測試類中創建Student類的對象s;
3)創建序列化對象objectOutputStream,同時創建輸出流並關聯硬盤上的文件;
4)使用序列化對象objectOutputStream調用writeObject()函數持久化保存學生對象s;
5)釋放序列化對象流的資源;
Student類如下:
需要實現序列化接口Serializable,它是一個標記性接口。這個接口中沒有任何的方法,這種接口稱為標記型接口!它僅僅是一個標識。只有具備了這個接口標識的類才能通過Java中的序列化和反序列化流操作這個對象。

/**
* 為了保證學生對象可以被序列化,我們讓Student類來實現Serializable接口
*/
public class Student implements Serializable {
private String name;
private Integer age;
//省略Getter and Setter、toString和構造方法
}
測試類如下:
/**
* 將學生對象序列化持久到硬盤文件中
*/
public class ObjectOutputStreamDemo {
public static void main(String[] args) throws IOException {
//創建學生對象
Student student = new Student("張三", 13);
//把創建出來的學生對象持久化保存在硬盤中
//創建序列化對象 創建輸出流對象並關聯目標文件
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\student.txt"));
//使用序列化對象中的方法持久化學生對象
objectOutputStream.writeObject(student);
//關閉資源
objectOutputStream.close();
}
}
序列化成功后在會在D盤生成一個student.txt文件。
注意:序列化對象如果沒有實現序列化接口Serializable,則會拋出如下異常

反序列化
需求:把硬盤文件中的序列化的對象,再進行反序列操作。
分析和步驟:
1)創建反序列化對象,指定一個字節輸入流,關聯硬盤上的文件;
2)使用反序列對象調用函數readObject()函數,進行反序列操作,獲得Student類的對象s;
3)使用對象s調用函數來獲取Student類的name和age屬性;
4)釋放資源;
測試類如下:
/**
* 演示反序列化操作
*/
public class ObjectInputStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//創建反序列化對象,指定一個字節輸入流用來讀取文件
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\student.txt"));
//使用反序列化對象調用函數來進行讀取數據
Student student = (Student) objectInputStream.readObject();
System.out.println(student.getName() + "," + student.getAge());
//關閉資源
objectInputStream.close();
}
}
結果:
張三,13
這里如果我們對Student類做了一些簡單的修改,無關緊要的修改。例如給Student類添加一個屬性字段或者函數都可以,再次反序列化,就出問題了,報如下圖所示的異常:


問題:什么是該類的序列版本號呢?
在序列化時將類的各個方面(如類的成員變量、成員函數、修飾符、函數返回值類型等)計算成為該類的默認 serialVersionUID值(版本號)。然后在序列化的時候,這個版本號會隨着對象一起被序列化到本地文件中。serialVersionUID 稱為序列版本編號(標記值)。類要進行序列化操作時,需要實現Serializable接口(Serializable接口也稱為標記接口),實現了標記接口的類,該類會存在一個標記值。在反序列化的時候,從流中也就是硬盤文件中讀取數據及原來存儲的版本號。同時,再次根據類的內容計算當前的版本號。然后這兩個版本號進行對比,如果不一致,認為類發生了改變,則拋出InvalidClassException異常。上述代碼如果在反序列化之前修改Student類時,會在報的異常中出現如下圖所示的提示信息:
local class incompatible: stream classdesc serialVersionUID = 8434462772094727595, local class serialVersionUID = 2117687252335805572
stream classdesc serialVersionUID = 8434462772094727595, 表示從流中讀取的版本號(反序列化時)
local class serialVersionUID = 2117687252335805572 ,表示Student類的序列版本號(序列化時)
上述兩個版本號不一致,所以報異常。
通過以上分析,可以得到一個結論:
如果可以保證反序列化對象和序列化對象的標記值相同,就可以避免異常的發生。
那么我們如何做才能保證反序列化對象和序列化對象的標記值相同呢?
我們修改Student類是無關緊要的。在我們修改Student類的時候,我們不希望它拋異常。我們可以給類定義一個默認的版本號,即給Student類添加標記值也就是版本號serialVersionUID。這樣一來,添加的標記值即版本號會隨着對象的序列化持久保存。無論是序列化,還是反序列化,都不會再根據類的各個方面計算版本號了。序列化和反序列化的版本號會永遠一致,所以不會拋出異常,這樣就可以避免InvalidClassException異常的發生了。
但是,這樣一來,類的安全問題,只能自己來維護。因為已經將類的對象序列化之后,由於類中已經顯示定義了版本號,那么反序列化的時候即使修改了Student類,也不會報異常了。
使用idea給自定義類Student添加版本號方法如下圖所示:

注意:
在使用序列化操作時,不是所有的成員都可以進行序列化操作:
1)靜態成員不會進行序列化操作;
2)瞬態成員也不會進行序列化操作;
瞬態成員:在進行序列化操作時,如果希望某些成員不被序列化,而該成員又不能是靜態成員(不希望隨着類加載而存在,和對象有關系),就使用關鍵字transient把成員變為瞬態成員。
代碼如下所示:

反序列化的結果:
null,null
說明:由於name和age屬性分別被靜態和瞬態修飾了,所以都不能被序列化到硬盤上,所以反序列化都是默認值。
記住:
1、當一個對象需要被序列化 或 反序列化的時候對象所屬的類需要實現Serializable接口。
2、被序列化的類中需要添加一個serialVersionUID。
序列化的細節:
序列化的時候,只能把對象在堆中的所有數據持久保存到持久設備上。靜態的成員變量不會被序列化。
有時我們在序列化的時候某些非靜態成員變量也不想被序列化的時候,我們可以使用瞬態關鍵字(transient)修飾。
四、serialVersionUID的取值
serialVersionUID的取值是Java運行時環境根據類的內部細節自動生成的。如果對類的源代碼作了修改,再重新編譯,新生成的類文件的serialVersionUID的取值有可能也會發生變化。
類的serialVersionUID的默認值完全依賴於Java編譯器的實現,對於同一個類,用不同的Java編譯器編譯,有可能會導致不同的 serialVersionUID,也有可能相同。為了提高serialVersionUID的獨立性和確定性,強烈建議在一個可序列化類中顯示的定義serialVersionUID,為它賦予明確的值。
顯式地定義serialVersionUID有兩種用途:
- 在某些場合,希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有相同的serialVersionUID;
- 在某些場合,不希望類的不同版本對序列化兼容,因此需要確保類的不同版本具有不同的serialVersionUID。
五、什么時候需要序列化對象
一:對象序列化可以實現分布式對象。
主要應用例如:RMI(即遠程調用Remote Method Invocation)要利用對象序列化運行遠程主機上的服務,就像在本地機上運行對象時一樣。
二:java對象序列化不僅保留一個對象的數據,而且遞歸保存對象引用的每個對象的數據。
可以將整個對象層次寫入字節流中,可以保存在文件中或在網絡連接上傳遞。利用對象序列化可以進行對象的"深復制",即復制對象本身及引用的對象本身。序列化一個對象可能得到整個對象序列。
三:序列化可以將內存中的類寫入文件或數據庫中。
比如:將某個類序列化后存為文件,下次讀取時只需將文件中的數據反序列化就可以將原先的類還原到內存中。也可以將類序列化為流數據進行傳輸。總的來說就是將一個已經實例化的類轉成文件存儲,下次需要實例化的時候只要反序列化即可將類實例化到內存中並保留序列化時類中的所有變量和狀態。
四: 對象、文件、數據,有許多不同的格式,很難統一傳輸和保存。
序列化以后就都是字節流了,無論原來是什么東西,都能變成一樣的東西,就可以進行通用的格式傳輸或保存,傳輸結束以后,要再次使用,就進行反序列化還原,這樣對象還是對象,文件還是文件
正常來說是很少需要用到的,大部分情況下我們都是使用json或者xml格式來進行數據傳輸,如果只是轉換為字符串的形式與網絡打交道,那么就不需要實現Serializable接口。
