Java 內(nèi)置接口 Serializable示例詳解
引言
上一部分我們著重講了 Java 集合框架中在開發(fā)項目時經(jīng)常會被用到的數(shù)據(jù)容器,在講解、演示使用實踐的同時,把這個過程中遇到的各種相關(guān)知識點:泛型、Lambada、Stream 操作,一并給大家做了梳理。
從這篇開始我們進入下一部分,用三到五部分給大家梳理一下,在用 Java 編程時,那些我們繞不開的 interface;從最基本的 Serializable 到 Comparable 和 Iterator 這些,再到 Java 為了支持函數(shù)式編程而提供的 Function、Predicate 等 interface。
這些 Java 內(nèi)置提供的 interface 或多或少我們在寫 Java 代碼的時候都見過,有的甚至是潛移默化地在日常編碼中已經(jīng)實現(xiàn)過其中的一些 interface,只不過我們沒有察覺到罷了。相信通過閱讀著幾篇文章,一定會讓你在寫 Java 代碼時更清楚自己是在做什么,不會再被這些個似曾相識的 interface 困擾到。
本文大綱如下:

Serializable 接口
作為 Java 中那些繞不開的內(nèi)置接口 這個小系列的開篇文章,首先要給大家介紹的 interface 是 Serializable。
Serializable這個接口的全限定名(包名 + 接口名)是 java.io.Serializable,這里給大家說個小技巧,當你看到一個類或者接口的包名前綴里包含java.io那就證明這個類 / 接口它跟數(shù)據(jù)的傳輸有關(guān)。
Serializable 是 Java 中非常重要的一個接口,如果一個類的對象是可序列化的,即對象在程序里可以進行序列化和反序列化,對象的類就一定要實現(xiàn)Serializable接口。那么為什么要進行序列化和反序列化呢?
序列化的意思是將對象的狀態(tài)轉(zhuǎn)換為字節(jié)流;反序列化則相反。換句話說,序列化是將 Java 對象轉(zhuǎn)換為靜態(tài)字節(jié)流(序列),然后我們可以將其保存到文件、數(shù)據(jù)庫或者是通過通過網(wǎng)絡(luò)傳輸,反序列化則是在我們讀取到字節(jié)流后再轉(zhuǎn)換成 Java 對象的過程;這也正好解釋了為什么Serializable 接口會歸屬到java.io包下面。
Serializable 是一個標記型接口
雖說需要進行序列化的對象,它們的類都需要實現(xiàn) Serializable 接口,但其實你會發(fā)現(xiàn),我們在讓一個類實現(xiàn) Serializable 接口時,并沒有額外實現(xiàn)過什么抽線方法。
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
}
比如向上面?zhèn)€類文件里的內(nèi)容,Person 類聲明實現(xiàn) Serializable 接口后,并沒有去實現(xiàn)什么抽象方法,IDE 也不會用紅線警告提示我們:“你有一個抽象方法需要實現(xiàn)” ,原因是 Serializable 接口里并沒有聲明抽象方法。
public interface Serializable {
}
這種不包含任何方法的 interface 被稱為標記型接口,類實現(xiàn) Serializable接口不必實現(xiàn)任何特定方法,它只起標記作用,讓 Java 知道該類可以用于對象序列化。
serializable Version UID
雖說一個類實現(xiàn)了 Serializable 接口的時候不需要實現(xiàn)特定的方法,但是經(jīng)常會看到一些實現(xiàn)了Serializable的類中,都有一個名為serialVersionUID類型為long的私有靜態(tài) 屬性。
import java.io.Serializable;
public static class Person implements Serializable {
private static final long serialVersionUID = -7792628363939354385L;
public String name;
public int age;
}
該屬性修飾符里使用了final即賦值后不可更改。Java 的對象序列化 API 在從讀取到的字節(jié)序列中反序列化出對象時,使用 serialVersionUID 這個靜態(tài)類屬性來判斷:是否序列化對象時使用了當前相同版本的類進行的序列化。Java 使用它來驗證保存和加載的對象是否具有相同的屬性,確保在序列化上是兼容的。
大多數(shù)的 IDE 都可以自動生成這個 serialVersionUID靜態(tài)屬性的值,規(guī)則是基于類名、屬性和相關(guān)的訪問修飾符。任何更改都會導(dǎo)致不同的數(shù)字,并可能導(dǎo)致 InvalidClassException。 如果一個實現(xiàn) Serializable 的類沒有聲明 serialVersionUID,JVM 會在運行時自動生成一個。但是,強烈建議每個可序列化類都聲明 serialVersionUID,因為默認生成的serialVersionUID依賴于編譯器,因此可能會導(dǎo)致意外的InvalidClassExceptions。
我上面那個例子里,Person 類的serialVersionUID是用 Intelij IDEA 自動生成的,所以值看起來一大串,不是我自己些的。IDEA 默認不會給可序列化類自動生成 serialVersionUID 需要安裝一個插件。

這里給大家放一個截圖,插件的安裝和使用,網(wǎng)上有很多例子,大家需要的話動手搜一下,這里就不再占用太多篇幅講怎么安裝和使用這個插件了。
Java 序列化與JSON序列化的區(qū)別
Java 的序列化與現(xiàn)在互聯(lián)網(wǎng)上 Web 應(yīng)用交互數(shù)據(jù)常用的 JSON 序列化并不是一回事兒,這是咱們需要注意的,像 Java、C#、PHP 這些編程語言,都有自己的序列化機制把自家的對象序列化成字節(jié)然后進行傳輸或者保存,但是這些語言的序列化機制之間并不能互認,即用 Java 把對象序列化成字節(jié)、通過網(wǎng)絡(luò) RESTful API 傳給一個 PHP 開發(fā)的服務(wù),PHP 是沒辦法反序列化還原出這個對象的。這樣才有了 JSON、XML、Protocol Buffer 這樣的更通用的序列化標準。
例如在實際項目開發(fā)的時候,Java 對象往往被序列化為 JSON、XML 后再在網(wǎng)絡(luò)上傳輸,如果對數(shù)據(jù)大小敏感的場景,會把 Java 對象序列化成空間占用更小的一些二進制格式,比如 Protocol Buffer ( 分布式 RPC 框架 gRPC 的數(shù)據(jù)交換格式)。這樣做的好處是序列化后的數(shù)據(jù)可以被非 Java 應(yīng)用程序讀取和反序列化,例如,在 Web 瀏覽器中運行的 JavaScript 可以在本地將對象序列化成 JSON 傳輸給 Java 寫的 API 接口,也可以從 Java API接口返回響應(yīng)中的 JSON 數(shù)據(jù),反序列化成 JavaScript 本地的對象 。
像上面列舉的這些對象序列化機制,是不需要我們的 Java 類實現(xiàn) Serializable 接口的。這些 JSON、XML 等格式的序列化類,通常使用 Java 反射來檢查類,配合一些特定的注解完成序列化。
Java序列化相較于 JSON 的優(yōu)勢
上面介紹了 JSON 這樣的通用序列化格式的優(yōu)勢,有的可能會問了,那還用 Java 序列化干啥。這里再給大家分析一下,Java 對象序列化雖然在通用性上不如 JSON 那些序列化格式,但是在 Java 生態(tài)內(nèi)部卻是十分好用的,其最聰明的一點是,它不僅能保存對象的副本,而且還會跟著對象里面的reference,把它所引用的對象也保存起來,然后再繼續(xù)跟蹤那些對象的reference,以此類推。
這個機制所涵蓋的范圍不僅包括對象的成員數(shù)據(jù),而且還包含數(shù)組里面的reference。如果你要自己實現(xiàn)對象序列化的話,那么編寫跟蹤這些鏈接的程序?qū)且患浅M纯嗟娜蝿?wù)。但是,Java的對象序列化就能精確無誤地做到這一點,毫無疑問,它的遍歷算法是做過優(yōu)化的。
另外你們在一些資料里看過 Java Bean 的定義
1、所有屬性為private
2、提供默認構(gòu)造方法
3、提供getter和setter
4、實現(xiàn)java.io.Serializable接口
那么問題來了,為什么要進行序列化?每個實體bean都必須實現(xiàn)serializabel接口嗎?以及我做項目的時候,沒有實現(xiàn)序列化,同樣沒什么影響,到底什么時候應(yīng)該進行序列化操作呢?
這里轉(zhuǎn)載一個網(wǎng)上大佬對這個問題的解釋
首先第一個問題,實現(xiàn)序列化的兩個原因:
1、將對象的狀態(tài)保存在存儲媒體中以便可以在以后重新創(chuàng)建出完全相同的副本;
2、按值將對象從一個應(yīng)用程序域發(fā)送至另一個應(yīng)用程序域。實現(xiàn)serializabel接口的作用是就是可以把對象存到字節(jié)流,然后可以恢復(fù),所以你想如果你的對象沒實現(xiàn)序列化怎么才能進行持久化和網(wǎng)絡(luò)傳輸呢,要持久化和網(wǎng)絡(luò)傳輸就得轉(zhuǎn)為字節(jié)流,所以在分布式應(yīng)用中及設(shè)計數(shù)據(jù)持久化的場景中,你就得實現(xiàn)序列化。
第二個問題,是不是每個實體bean都要實現(xiàn)序列化,答案其實還要回歸到第一個問題,那就是你的bean是否需要持久化存儲媒體中以及是否需要傳輸給另一個應(yīng)用,沒有的話就不需要,例如我們利用fastjson將實體類轉(zhuǎn)化成json字符串時,并不涉及到轉(zhuǎn)化為字節(jié)流,所以其實跟序列化沒有關(guān)系。
第三個問題,有的時候并沒有實現(xiàn)序列化,依然可以持久化到數(shù)據(jù)庫。這個其實我們可以看看實體類中常用的數(shù)據(jù)類型,例如Date、String等等,它們已經(jīng)實現(xiàn)了序列化,而一些基本類型,數(shù)據(jù)庫里面有與之對應(yīng)的數(shù)據(jù)結(jié)構(gòu),從我們的類聲明來看,我們沒有實現(xiàn)serializabel接口,其實是在聲明的各個不同變量的時候,由具體的數(shù)據(jù)類型幫助我們實現(xiàn)了序列化操作。
另外需要注意的是,在NoSql數(shù)據(jù)庫中,并沒有與我們Java基本類型對應(yīng)的數(shù)據(jù)結(jié)構(gòu),所以在往nosql數(shù)據(jù)庫中存儲時,我們就必須將對象進行序列化,同時在網(wǎng)絡(luò)傳輸中我們要注意到兩個應(yīng)用中javabean的serialVersionUID要保持一致,不然就不能正常的進行反序列化。
Java 類對象的序列化代碼演示
到這里 Serializable 需要了解的基礎(chǔ)知識就都給大家梳理出來了,這塊屬于選讀,用 Java 編程寫序列化代碼的場景并不是太多,不過有興趣就再接著往下看吧,有個印象,這樣以后寫代碼的時候,哪天用上了,還能快速想起來在哪看過,再回來翻看。
Java 對象序列化(寫入)由 ObjectOutputStream 完成,反序列化(讀?。┯?ObjectInputStream 完成。ObjectInputStream 和 ObjectOutputStream 是分別繼承了 java.io.InputStream 和 java.io.OutputStream 抽象的實體類。 ObjectOutputStream 可以將對象的原型作為字節(jié)流寫入 OutputStream。然后我們可以使用 ObjectInputStream 讀取這些流。 ObjectOutputStream 中最重要的方法是:
public final void writeObject(Object o) throws IOException;
這個方法接收一個可序列化對象(實現(xiàn)了 Serializable 接口的類的對象)并將其轉(zhuǎn)換為字節(jié)序列。同樣,在ObjectInputStream 中最重要的方法是:
public final Object readObject() throws IOException, ClassNotFoundException;
此方法可以讀取字節(jié)流并將其轉(zhuǎn)換回 Java 對象。然后我們可以再使用類型轉(zhuǎn)換(Type Cast)將其轉(zhuǎn)換回原始的類型對象。
下面我們使用文章示例里的Person類再給大家演示一下 Java 的序列化代碼。
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
static String country = "ITALY";
private int age;
private String name;
transient int height;
// 省略 getter 和 setter
}
這里要注意一下, static 修飾的靜態(tài)屬性是類屬性,并不屬于對象,所以在序列化對象時不會把類中的靜態(tài)屬性序列化了,另外我們也可以使用 transient關(guān)鍵字修飾那些我們想在序列化過程中忽略調(diào)的對象屬性。
@Test
public void serializingAndDeserializing_ThenObjectIsTheSame() ()
throws IOException, ClassNotFoundException {
Person person = new Person();
person.setAge(20);
person.setName("Joe");
// 用指定文件路徑--當前目錄的 test_serialization.txt 文件創(chuàng)建 FileOutputStream。
// 在寫入 FileOutputStream 時, FileOutputStream 會在在項目目錄中創(chuàng)建文件
// “test_serialization.txt”
FileOutputStream fileOutputStream
= new FileOutputStream("./test_serialization.txt");
// 以 FileOutputStream 為底層輸出流創(chuàng)建對象輸出流 ObjectOutputStream
ObjectOutputStream objectOutputStream
= new ObjectOutputStream(fileOutputStream);
// 向 ObjectOutputStream 中寫入 person 對象
objectOutputStream.writeObject(person);
// 把數(shù)據(jù)從流中刷到磁盤上
objectOutputStream.flush();
objectOutputStream.close();
// 用上面的文件路徑,創(chuàng)建文件輸入流
FileInputStream fileInputStream
= new FileInputStream("./test_serialization.txt");
// 以文件輸入流創(chuàng)建對象輸入流 ObjectInputStream
ObjectInputStream objectInputStream
= new ObjectInputStream(fileInputStream);
// 用對象輸入流讀取到文件中保存的序列化對象,反序列化成 Java Object 再轉(zhuǎn)換成 Person 對象
Person p2 = (Person) objectInputStream.readObject();
objectInputStream.close();
assertTrue(p2.getAge() == person.getAge());
assertTrue(p2.getName().equals(person.getName()));
}
上面這個單元測試里的代碼演示了,怎么把 Person 類的對象進行 Java 序列化保存到文件中,再從文件中讀取對象被序列化后的字節(jié)序列,然后還原成Person類的對象。
因為我們的專欄還沒有設(shè)計到 Java IO 這塊的內(nèi)容,所以各種輸入輸出流就不過多進行講解了,為了方便大家閱讀時理解上面的程序,我在上面程序注釋里已經(jīng)詳細注釋了每一步完成的操作,這些輸入輸出流我們等到講到 Java IO 體系的時候再詳細進行講解。
總結(jié)
今天給大家梳理了 Java Serializable 接口的一些必須要了解的知識,Serializable 接口在我們用 Java 編程的時候經(jīng)常見,但是很多人并不了解它的作用,因為它的主要作用還是用于標記類是否是可序列化類,這樣 Java 的 ObjectOutputStream 和 ObjectInputStream 才能對類的對象進行序列化和反序列化。
下一篇我們分享 Iterable 和 Iterator 這兩個名字看起差不多的 Java 內(nèi)置接口,請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring boot2X負載均衡和反向代理實現(xiàn)過程解析
這篇文章主要介紹了Spring boot2X負載均衡和反向代理實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12
IntelliJ Idea SpringBoot 數(shù)據(jù)庫增刪改查實例詳解
SpringBoot 是 SpringMVC 的升級,對于編碼、配置、部署和監(jiān)控,更加簡單。這篇文章主要介紹了IntelliJ Idea SpringBoot 數(shù)據(jù)庫增刪改查實例,需要的朋友可以參考下2018-02-02
java連接zookeeper實現(xiàn)zookeeper教程
這篇文章主要介紹了java連接zookeeper實現(xiàn)zookeeper教程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
解決使用stream將list轉(zhuǎn)map時,key重復(fù)導(dǎo)致報錯的問題
這篇文章主要介紹了解決使用stream將list轉(zhuǎn)map時,key重復(fù)導(dǎo)致報錯的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
springcloud整合到項目中無法啟動報錯Failed to start bean&n
這篇文章主要介紹了springcloud整合到項目中無法啟動報錯Failed to start bean 'eurekaAutoServiceRegistration'問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01

