Java 內(nèi)置接口 Serializable示例詳解
引言
上一部分我們著重講了 Java 集合框架中在開發(fā)項(xiàng)目時(shí)經(jīng)常會(huì)被用到的數(shù)據(jù)容器,在講解、演示使用實(shí)踐的同時(shí),把這個(gè)過程中遇到的各種相關(guān)知識(shí)點(diǎn):泛型、Lambada
、Stream
操作,一并給大家做了梳理。
從這篇開始我們進(jìn)入下一部分,用三到五部分給大家梳理一下,在用 Java 編程時(shí),那些我們繞不開的 interface
;從最基本的 Serializable
到 Comparable
和 Iterator
這些,再到 Java 為了支持函數(shù)式編程而提供的 Function
、Predicate
等 interface
。
這些 Java 內(nèi)置提供的 interface 或多或少我們?cè)趯?Java 代碼的時(shí)候都見過,有的甚至是潛移默化地在日常編碼中已經(jīng)實(shí)現(xiàn)過其中的一些 interface,只不過我們沒有察覺到罷了。相信通過閱讀著幾篇文章,一定會(huì)讓你在寫 Java 代碼時(shí)更清楚自己是在做什么,不會(huì)再被這些個(gè)似曾相識(shí)的 interface 困擾到。
本文大綱如下:
Serializable 接口
作為 Java 中那些繞不開的內(nèi)置接口 這個(gè)小系列的開篇文章,首先要給大家介紹的 interface 是 Serializable
。
Serializable
這個(gè)接口的全限定名(包名 + 接口名)是 java.io.Serializable
,這里給大家說個(gè)小技巧,當(dāng)你看到一個(gè)類或者接口的包名前綴里包含java.io
那就證明這個(gè)類 / 接口它跟數(shù)據(jù)的傳輸有關(guān)。
Serializable
是 Java 中非常重要的一個(gè)接口,如果一個(gè)類的對(duì)象是可序列化的,即對(duì)象在程序里可以進(jìn)行序列化和反序列化,對(duì)象的類就一定要實(shí)現(xiàn)Serializable
接口。那么為什么要進(jìn)行序列化和反序列化呢?
序列化的意思是將對(duì)象的狀態(tài)轉(zhuǎn)換為字節(jié)流;反序列化則相反。換句話說,序列化是將 Java 對(duì)象轉(zhuǎn)換為靜態(tài)字節(jié)流(序列),然后我們可以將其保存到文件、數(shù)據(jù)庫或者是通過通過網(wǎng)絡(luò)傳輸,反序列化則是在我們讀取到字節(jié)流后再轉(zhuǎn)換成 Java 對(duì)象的過程;這也正好解釋了為什么Serializable
接口會(huì)歸屬到java.io
包下面。
Serializable 是一個(gè)標(biāo)記型接口
雖說需要進(jìn)行序列化的對(duì)象,它們的類都需要實(shí)現(xiàn) Serializable
接口,但其實(shí)你會(huì)發(fā)現(xiàn),我們?cè)谧屢粋€(gè)類實(shí)現(xiàn) Serializable
接口時(shí),并沒有額外實(shí)現(xiàn)過什么抽線方法。
import java.io.Serializable; public class Person implements Serializable { private String name; private int age; }
比如向上面?zhèn)€類文件里的內(nèi)容,Person 類聲明實(shí)現(xiàn) Serializable 接口后,并沒有去實(shí)現(xiàn)什么抽象方法,IDE 也不會(huì)用紅線警告提示我們:“你有一個(gè)抽象方法需要實(shí)現(xiàn)” ,原因是 Serializable 接口里并沒有聲明抽象方法。
public interface Serializable { }
這種不包含任何方法的 interface 被稱為標(biāo)記型接口,類實(shí)現(xiàn) Serializable
接口不必實(shí)現(xiàn)任何特定方法,它只起標(biāo)記作用,讓 Java 知道該類可以用于對(duì)象序列化。
serializable Version UID
雖說一個(gè)類實(shí)現(xiàn)了 Serializable
接口的時(shí)候不需要實(shí)現(xiàn)特定的方法,但是經(jīng)常會(huì)看到一些實(shí)現(xiàn)了Serializable
的類中,都有一個(gè)名為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 的對(duì)象序列化 API 在從讀取到的字節(jié)序列中反序列化出對(duì)象時(shí),使用 serialVersionUID 這個(gè)靜態(tài)類屬性來判斷:是否序列化對(duì)象時(shí)使用了當(dāng)前相同版本的類進(jìn)行的序列化。Java 使用它來驗(yàn)證保存和加載的對(duì)象是否具有相同的屬性,確保在序列化上是兼容的。
大多數(shù)的 IDE 都可以自動(dòng)生成這個(gè) serialVersionUID
靜態(tài)屬性的值,規(guī)則是基于類名、屬性和相關(guān)的訪問修飾符。任何更改都會(huì)導(dǎo)致不同的數(shù)字,并可能導(dǎo)致 InvalidClassException。 如果一個(gè)實(shí)現(xiàn) Serializable
的類沒有聲明 serialVersionUID
,JVM 會(huì)在運(yùn)行時(shí)自動(dòng)生成一個(gè)。但是,強(qiáng)烈建議每個(gè)可序列化類都聲明 serialVersionUID,因?yàn)槟J(rèn)生成的serialVersionUID
依賴于編譯器,因此可能會(huì)導(dǎo)致意外的InvalidClassExceptions
。
我上面那個(gè)例子里,Person
類的serialVersionUID
是用 Intelij IDEA 自動(dòng)生成的,所以值看起來一大串,不是我自己些的。IDEA 默認(rèn)不會(huì)給可序列化類自動(dòng)生成 serialVersionUID
需要安裝一個(gè)插件。
這里給大家放一個(gè)截圖,插件的安裝和使用,網(wǎng)上有很多例子,大家需要的話動(dòng)手搜一下,這里就不再占用太多篇幅講怎么安裝和使用這個(gè)插件了。
Java 序列化與JSON序列化的區(qū)別
Java 的序列化與現(xiàn)在互聯(lián)網(wǎng)上 Web 應(yīng)用交互數(shù)據(jù)常用的 JSON 序列化并不是一回事兒,這是咱們需要注意的,像 Java、C#、PHP 這些編程語言,都有自己的序列化機(jī)制把自家的對(duì)象序列化成字節(jié)然后進(jìn)行傳輸或者保存,但是這些語言的序列化機(jī)制之間并不能互認(rèn),即用 Java 把對(duì)象序列化成字節(jié)、通過網(wǎng)絡(luò) RESTful API 傳給一個(gè) PHP 開發(fā)的服務(wù),PHP 是沒辦法反序列化還原出這個(gè)對(duì)象的。這樣才有了 JSON、XML、Protocol Buffer 這樣的更通用的序列化標(biāo)準(zhǔn)。
例如在實(shí)際項(xiàng)目開發(fā)的時(shí)候,Java 對(duì)象往往被序列化為 JSON、XML 后再在網(wǎng)絡(luò)上傳輸,如果對(duì)數(shù)據(jù)大小敏感的場(chǎng)景,會(huì)把 Java 對(duì)象序列化成空間占用更小的一些二進(jìn)制格式,比如 Protocol Buffer ( 分布式 RPC 框架 gRPC 的數(shù)據(jù)交換格式)。這樣做的好處是序列化后的數(shù)據(jù)可以被非 Java 應(yīng)用程序讀取和反序列化,例如,在 Web 瀏覽器中運(yùn)行的 JavaScript 可以在本地將對(duì)象序列化成 JSON 傳輸給 Java 寫的 API 接口,也可以從 Java API接口返回響應(yīng)中的 JSON 數(shù)據(jù),反序列化成 JavaScript 本地的對(duì)象 。
像上面列舉的這些對(duì)象序列化機(jī)制,是不需要我們的 Java 類實(shí)現(xiàn) Serializable 接口的。這些 JSON、XML 等格式的序列化類,通常使用 Java 反射來檢查類,配合一些特定的注解完成序列化。
Java序列化相較于 JSON 的優(yōu)勢(shì)
上面介紹了 JSON 這樣的通用序列化格式的優(yōu)勢(shì),有的可能會(huì)問了,那還用 Java 序列化干啥。這里再給大家分析一下,Java 對(duì)象序列化雖然在通用性上不如 JSON 那些序列化格式,但是在 Java 生態(tài)內(nèi)部卻是十分好用的,其最聰明的一點(diǎn)是,它不僅能保存對(duì)象的副本,而且還會(huì)跟著對(duì)象里面的reference,把它所引用的對(duì)象也保存起來,然后再繼續(xù)跟蹤那些對(duì)象的reference,以此類推。
這個(gè)機(jī)制所涵蓋的范圍不僅包括對(duì)象的成員數(shù)據(jù),而且還包含數(shù)組里面的reference。如果你要自己實(shí)現(xiàn)對(duì)象序列化的話,那么編寫跟蹤這些鏈接的程序?qū)?huì)是一件非常痛苦的任務(wù)。但是,Java的對(duì)象序列化就能精確無誤地做到這一點(diǎn),毫無疑問,它的遍歷算法是做過優(yōu)化的。
另外你們?cè)谝恍┵Y料里看過 Java Bean 的定義
1、所有屬性為private
2、提供默認(rèn)構(gòu)造方法
3、提供getter和setter
4、實(shí)現(xiàn)java.io.Serializable接口
那么問題來了,為什么要進(jìn)行序列化?每個(gè)實(shí)體bean都必須實(shí)現(xiàn)serializabel接口嗎?以及我做項(xiàng)目的時(shí)候,沒有實(shí)現(xiàn)序列化,同樣沒什么影響,到底什么時(shí)候應(yīng)該進(jìn)行序列化操作呢?
這里轉(zhuǎn)載一個(gè)網(wǎng)上大佬對(duì)這個(gè)問題的解釋
首先第一個(gè)問題,實(shí)現(xiàn)序列化的兩個(gè)原因:
1、將對(duì)象的狀態(tài)保存在存儲(chǔ)媒體中以便可以在以后重新創(chuàng)建出完全相同的副本;
2、按值將對(duì)象從一個(gè)應(yīng)用程序域發(fā)送至另一個(gè)應(yīng)用程序域。實(shí)現(xiàn)serializabel接口的作用是就是可以把對(duì)象存到字節(jié)流,然后可以恢復(fù),所以你想如果你的對(duì)象沒實(shí)現(xiàn)序列化怎么才能進(jìn)行持久化和網(wǎng)絡(luò)傳輸呢,要持久化和網(wǎng)絡(luò)傳輸就得轉(zhuǎn)為字節(jié)流,所以在分布式應(yīng)用中及設(shè)計(jì)數(shù)據(jù)持久化的場(chǎng)景中,你就得實(shí)現(xiàn)序列化。
第二個(gè)問題,是不是每個(gè)實(shí)體bean都要實(shí)現(xiàn)序列化,答案其實(shí)還要回歸到第一個(gè)問題,那就是你的bean是否需要持久化存儲(chǔ)媒體中以及是否需要傳輸給另一個(gè)應(yīng)用,沒有的話就不需要,例如我們利用fastjson將實(shí)體類轉(zhuǎn)化成json字符串時(shí),并不涉及到轉(zhuǎn)化為字節(jié)流,所以其實(shí)跟序列化沒有關(guān)系。
第三個(gè)問題,有的時(shí)候并沒有實(shí)現(xiàn)序列化,依然可以持久化到數(shù)據(jù)庫。這個(gè)其實(shí)我們可以看看實(shí)體類中常用的數(shù)據(jù)類型,例如Date、String等等,它們已經(jīng)實(shí)現(xiàn)了序列化,而一些基本類型,數(shù)據(jù)庫里面有與之對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),從我們的類聲明來看,我們沒有實(shí)現(xiàn)serializabel接口,其實(shí)是在聲明的各個(gè)不同變量的時(shí)候,由具體的數(shù)據(jù)類型幫助我們實(shí)現(xiàn)了序列化操作。
另外需要注意的是,在NoSql數(shù)據(jù)庫中,并沒有與我們Java基本類型對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),所以在往nosql數(shù)據(jù)庫中存儲(chǔ)時(shí),我們就必須將對(duì)象進(jìn)行序列化,同時(shí)在網(wǎng)絡(luò)傳輸中我們要注意到兩個(gè)應(yīng)用中javabean的serialVersionUID要保持一致,不然就不能正常的進(jìn)行反序列化。
Java 類對(duì)象的序列化代碼演示
到這里 Serializable 需要了解的基礎(chǔ)知識(shí)就都給大家梳理出來了,這塊屬于選讀,用 Java 編程寫序列化代碼的場(chǎng)景并不是太多,不過有興趣就再接著往下看吧,有個(gè)印象,這樣以后寫代碼的時(shí)候,哪天用上了,還能快速想起來在哪看過,再回來翻看。
Java 對(duì)象序列化(寫入)由 ObjectOutputStream 完成,反序列化(讀?。┯?ObjectInputStream 完成。ObjectInputStream 和 ObjectOutputStream 是分別繼承了 java.io.InputStream 和 java.io.OutputStream 抽象的實(shí)體類。 ObjectOutputStream 可以將對(duì)象的原型作為字節(jié)流寫入 OutputStream。然后我們可以使用 ObjectInputStream 讀取這些流。 ObjectOutputStream 中最重要的方法是:
public final void writeObject(Object o) throws IOException;
這個(gè)方法接收一個(gè)可序列化對(duì)象(實(shí)現(xiàn)了 Serializable 接口的類的對(duì)象)并將其轉(zhuǎn)換為字節(jié)序列。同樣,在ObjectInputStream 中最重要的方法是:
public final Object readObject() throws IOException, ClassNotFoundException;
此方法可以讀取字節(jié)流并將其轉(zhuǎn)換回 Java 對(duì)象。然后我們可以再使用類型轉(zhuǎn)換(Type Cast)將其轉(zhuǎn)換回原始的類型對(duì)象。
下面我們使用文章示例里的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)屬性是類屬性,并不屬于對(duì)象,所以在序列化對(duì)象時(shí)不會(huì)把類中的靜態(tài)屬性序列化了,另外我們也可以使用 transient
關(guān)鍵字修飾那些我們想在序列化過程中忽略調(diào)的對(duì)象屬性。
@Test public void serializingAndDeserializing_ThenObjectIsTheSame() () throws IOException, ClassNotFoundException { Person person = new Person(); person.setAge(20); person.setName("Joe"); // 用指定文件路徑--當(dāng)前目錄的 test_serialization.txt 文件創(chuàng)建 FileOutputStream。 // 在寫入 FileOutputStream 時(shí), FileOutputStream 會(huì)在在項(xiàng)目目錄中創(chuàng)建文件 // “test_serialization.txt” FileOutputStream fileOutputStream = new FileOutputStream("./test_serialization.txt"); // 以 FileOutputStream 為底層輸出流創(chuàng)建對(duì)象輸出流 ObjectOutputStream ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); // 向 ObjectOutputStream 中寫入 person 對(duì)象 objectOutputStream.writeObject(person); // 把數(shù)據(jù)從流中刷到磁盤上 objectOutputStream.flush(); objectOutputStream.close(); // 用上面的文件路徑,創(chuàng)建文件輸入流 FileInputStream fileInputStream = new FileInputStream("./test_serialization.txt"); // 以文件輸入流創(chuàng)建對(duì)象輸入流 ObjectInputStream ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); // 用對(duì)象輸入流讀取到文件中保存的序列化對(duì)象,反序列化成 Java Object 再轉(zhuǎn)換成 Person 對(duì)象 Person p2 = (Person) objectInputStream.readObject(); objectInputStream.close(); assertTrue(p2.getAge() == person.getAge()); assertTrue(p2.getName().equals(person.getName())); }
上面這個(gè)單元測(cè)試?yán)锏拇a演示了,怎么把 Person 類的對(duì)象進(jìn)行 Java 序列化保存到文件中,再從文件中讀取對(duì)象被序列化后的字節(jié)序列,然后還原成Person
類的對(duì)象。
因?yàn)槲覀兊膶谶€沒有設(shè)計(jì)到 Java IO 這塊的內(nèi)容,所以各種輸入輸出流就不過多進(jìn)行講解了,為了方便大家閱讀時(shí)理解上面的程序,我在上面程序注釋里已經(jīng)詳細(xì)注釋了每一步完成的操作,這些輸入輸出流我們等到講到 Java IO 體系的時(shí)候再詳細(xì)進(jìn)行講解。
總結(jié)
今天給大家梳理了 Java Serializable 接口的一些必須要了解的知識(shí),Serializable 接口在我們用 Java 編程的時(shí)候經(jīng)常見,但是很多人并不了解它的作用,因?yàn)樗闹饕饔眠€是用于標(biāo)記類是否是可序列化類,這樣 Java 的 ObjectOutputStream 和 ObjectInputStream 才能對(duì)類的對(duì)象進(jìn)行序列化和反序列化。
下一篇我們分享 Iterable 和 Iterator 這兩個(gè)名字看起差不多的 Java 內(nèi)置接口,請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring boot2X負(fù)載均衡和反向代理實(shí)現(xiàn)過程解析
這篇文章主要介紹了Spring boot2X負(fù)載均衡和反向代理實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12IntelliJ Idea SpringBoot 數(shù)據(jù)庫增刪改查實(shí)例詳解
SpringBoot 是 SpringMVC 的升級(jí),對(duì)于編碼、配置、部署和監(jiān)控,更加簡(jiǎn)單。這篇文章主要介紹了IntelliJ Idea SpringBoot 數(shù)據(jù)庫增刪改查實(shí)例,需要的朋友可以參考下2018-02-02java連接zookeeper實(shí)現(xiàn)zookeeper教程
這篇文章主要介紹了java連接zookeeper實(shí)現(xiàn)zookeeper教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Java中的Semaphore信號(hào)量使用方法代碼實(shí)例
這篇文章主要介紹了Java中的Semaphore信號(hào)量使用方法代碼實(shí)例,Semaphore是一種基于計(jì)數(shù)的信號(hào)量,它可以設(shè)定一個(gè)閾值,基于此,多個(gè)線程競(jìng)爭(zhēng)獲取許可信號(hào),做自己的申請(qǐng)后歸還,超過閾值后,線程申請(qǐng)?jiān)S可信號(hào)將會(huì)被阻塞,需要的朋友可以參考下2023-11-11解決使用stream將list轉(zhuǎn)map時(shí),key重復(fù)導(dǎo)致報(bào)錯(cuò)的問題
這篇文章主要介紹了解決使用stream將list轉(zhuǎn)map時(shí),key重復(fù)導(dǎo)致報(bào)錯(cuò)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06springcloud整合到項(xiàng)目中無法啟動(dòng)報(bào)錯(cuò)Failed to start bean&n
這篇文章主要介紹了springcloud整合到項(xiàng)目中無法啟動(dòng)報(bào)錯(cuò)Failed to start bean 'eurekaAutoServiceRegistration'問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01