首頁 > 軟體

Java 內建介面 Serializable範例詳解

2022-11-03 14:02:20

引言

上一部分我們著重講了 Java 集合框架中在開發專案時經常會被用到的資料容器,在講解、演示使用實踐的同時,把這個過程中遇到的各種相關知識點:泛型、LambadaStream 操作,一併給大家做了梳理。

從這篇開始我們進入下一部分,用三到五部分給大家梳理一下,在用 Java 程式設計時,那些我們繞不開的 interface;從最基本的 Serializable ComparableIterator 這些,再到 Java 為了支援函數語言程式設計而提供的 FunctionPredicateinterface

這些 Java 內建提供的 interface 或多或少我們在寫 Java 程式碼的時候都見過,有的甚至是潛移默化地在日常編碼中已經實現過其中的一些 interface,只不過我們沒有察覺到罷了。相信通過閱讀著幾篇文章,一定會讓你在寫 Java 程式碼時更清楚自己是在做什麼,不會再被這些個似曾相識的 interface 困擾到。

本文大綱如下:

Serializable 介面

作為 Java 中那些繞不開的內建介面 這個小系列的開篇文章,首先要給大家介紹的 interface 是 Serializable

Serializable這個介面的全限定名(包名 + 介面名)是 java.io.Serializable,這裡給大家說個小技巧,當你看到一個類或者介面的包名字首裡包含java.io那就證明這個類 / 介面它跟資料的傳輸有關。

Serializable 是 Java 中非常重要的一個介面,如果一個類的物件是可序列化的,即物件在程式裡可以進行序列化和反序列化,物件的類就一定要實現Serializable介面。那麼為什麼要進行序列化和反序列化呢?

序列化的意思是將物件的狀態轉換為位元組流;反序列化則相反。換句話說,序列化是將 Java 物件轉換為靜態位元組流(序列),然後我們可以將其儲存到檔案、資料庫或者是通過通過網路傳輸,反序列化則是在我們讀取到位元組流後再轉換成 Java 物件的過程;這也正好解釋了為什麼Serializable 介面會歸屬到java.io包下面。

Serializable 是一個標記型介面

雖說需要進行序列化的物件,它們的類都需要實現 Serializable 介面,但其實你會發現,我們在讓一個類實現 Serializable 介面時,並沒有額外實現過什麼抽線方法。

import java.io.Serializable;
public class Person implements Serializable {
    private String name;
    private int age;
}

比如向上面個類檔案裡的內容,Person 類宣告實現 Serializable 介面後,並沒有去實現什麼抽象方法,IDE 也不會用紅線警告提示我們:“你有一個抽象方法需要實現” ,原因是 Serializable 介面裡並沒有宣告抽象方法。

public interface Serializable {
}

這種不包含任何方法的 interface 被稱為標記型介面,類實現 Serializable介面不必實現任何特定方法,它只起標記作用,讓 Java 知道該類可以用於物件序列化。

serializable Version UID

雖說一個類實現了 Serializable 介面的時候不需要實現特定的方法,但是經常會看到一些實現了Serializable的類中,都有一個名為serialVersionUID型別為long的私有靜態 屬性。

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 在從讀取到的位元組序列中反序列化出物件時,使用 serialVersionUID 這個靜態類屬性來判斷:是否序列化物件時使用了當前相同版本的類進行的序列化。Java 使用它來驗證儲存和載入的物件是否具有相同的屬性,確保在序列化上是相容的。

大多數的 IDE 都可以自動生成這個 serialVersionUID靜態屬性的值,規則是基於類名、屬性和相關的存取修飾符。任何更改都會導致不同的數位,並可能導致 InvalidClassException。 如果一個實現 Serializable 的類沒有宣告 serialVersionUID,JVM 會在執行時自動生成一個。但是,強烈建議每個可序列化類都宣告 serialVersionUID,因為預設生成的serialVersionUID依賴於編譯器,因此可能會導致意外的InvalidClassExceptions

我上面那個例子裡,Person 類的serialVersionUID是用 Intelij IDEA 自動生成的,所以值看起來一大串,不是我自己些的。IDEA 預設不會給可序列化類自動生成 serialVersionUID 需要安裝一個外掛。

這裡給大家放一個截圖,外掛的安裝和使用,網上有很多例子,大家需要的話動手搜一下,這裡就不再佔用太多篇幅講怎麼安裝和使用這個外掛了。

Java 序列化與JSON序列化的區別

Java 的序列化與現在網際網路上 Web 應用互動資料常用的 JSON 序列化並不是一回事兒,這是咱們需要注意的,像 Java、C#、PHP 這些程式語言,都有自己的序列化機制把自家的物件序列化成位元組然後進行傳輸或者儲存,但是這些語言的序列化機制之間並不能互認,即用 Java 把物件序列化成位元組、通過網路 RESTful API 傳給一個 PHP 開發的服務,PHP 是沒辦法反序列化還原出這個物件的。這樣才有了 JSON、XML、Protocol Buffer 這樣的更通用的序列化標準。

例如在實際專案開發的時候,Java 物件往往被序列化為 JSON、XML 後再在網路上傳輸,如果對資料大小敏感的場景,會把 Java 物件序列化成空間佔用更小的一些二進位制格式,比如 Protocol Buffer ( 分散式 RPC 框架 gRPC 的資料交換格式)。這樣做的好處是序列化後的資料可以被非 Java 應用程式讀取和反序列化,例如,在 Web 瀏覽器中執行的 JavaScript 可以在本地將物件序列化成 JSON 傳輸給 Java 寫的 API 介面,也可以從 Java API介面返回響應中的 JSON 資料,反序列化成 JavaScript 原生的物件 。

像上面列舉的這些物件序列化機制,是不需要我們的 Java 類實現 Serializable 介面的。這些 JSON、XML 等格式的序列化類,通常使用 Java 反射來檢查類,配合一些特定的註解完成序列化。

Java序列化相較於 JSON 的優勢

上面介紹了 JSON 這樣的通用序列化格式的優勢,有的可能會問了,那還用 Java 序列化幹啥。這裡再給大家分析一下,Java 物件序列化雖然在通用性上不如 JSON 那些序列化格式,但是在 Java 生態內部卻是十分好用的,其最聰明的一點是,它不僅能儲存物件的副本,而且還會跟著物件裡面的reference,把它所參照的物件也儲存起來,然後再繼續跟蹤那些物件的reference,以此類推。

這個機制所涵蓋的範圍不僅包括物件的成員資料,而且還包含陣列裡面的reference。如果你要自己實現物件序列化的話,那麼編寫跟蹤這些連結的程式將會是一件非常痛苦的任務。但是,Java的物件序列化就能精確無誤地做到這一點,毫無疑問,它的遍歷演演算法是做過優化的。

另外你們在一些資料裡看過 Java Bean 的定義

1、所有屬性為private

2、提供預設構造方法

3、提供getter和setter

4、實現java.io.Serializable介面

那麼問題來了,為什麼要進行序列化?每個實體bean都必須實現serializabel介面嗎?以及我做專案的時候,沒有實現序列化,同樣沒什麼影響,到底什麼時候應該進行序列化操作呢?

這裡轉載一個網上大佬對這個問題的解釋

首先第一個問題,實現序列化的兩個原因:

1、將物件的狀態儲存在儲存媒體中以便可以在以後重新建立出完全相同的副本;

2、按值將物件從一個應用程式域傳送至另一個應用程式域。實現serializabel介面的作用是就是可以把物件存到位元組流,然後可以恢復,所以你想如果你的物件沒實現序列化怎麼才能進行持久化和網路傳輸呢,要持久化和網路傳輸就得轉為位元組流,所以在分散式應用中及設計資料持久化的場景中,你就得實現序列化。

第二個問題,是不是每個實體bean都要實現序列化,答案其實還要回歸到第一個問題,那就是你的bean是否需要持久化儲存媒體中以及是否需要傳輸給另一個應用,沒有的話就不需要,例如我們利用fastjson將實體類轉化成json字串時,並不涉及到轉化為位元組流,所以其實跟序列化沒有關係。

第三個問題,有的時候並沒有實現序列化,依然可以持久化到資料庫。這個其實我們可以看看實體類中常用的資料型別,例如Date、String等等,它們已經實現了序列化,而一些基本型別,資料庫裡面有與之對應的資料結構,從我們的類宣告來看,我們沒有實現serializabel介面,其實是在宣告的各個不同變數的時候,由具體的資料型別幫助我們實現了序列化操作。

另外需要注意的是,在NoSql資料庫中,並沒有與我們Java基本型別對應的資料結構,所以在往nosql資料庫中儲存時,我們就必須將物件進行序列化,同時在網路傳輸中我們要注意到兩個應用中javabean的serialVersionUID要保持一致,不然就不能正常的進行反序列化。

Java 類物件的序列化程式碼演示

到這裡 Serializable 需要了解的基礎知識就都給大家梳理出來了,這塊屬於選讀,用 Java 程式設計寫序列化程式碼的場景並不是太多,不過有興趣就再接著往下看吧,有個印象,這樣以後寫程式碼的時候,哪天用上了,還能快速想起來在哪看過,再回來翻看。

Java 物件序列化(寫入)由 ObjectOutputStream 完成,反序列化(讀取)由 ObjectInputStream 完成。ObjectInputStream 和 ObjectOutputStream 是分別繼承了 java.io.InputStream 和 java.io.OutputStream 抽象的實體類。 ObjectOutputStream 可以將物件的原型作為位元組流寫入 OutputStream。然後我們可以使用 ObjectInputStream 讀取這些流。 ObjectOutputStream 中最重要的方法是:

public final void writeObject(Object o) throws IOException;

這個方法接收一個可序列化物件(實現了 Serializable 介面的類的物件)並將其轉換為位元組序列。同樣,在ObjectInputStream 中最重要的方法是:

public final Object readObject() throws IOException, ClassNotFoundException;

此方法可以讀取位元組流並將其轉換回 Java 物件。然後我們可以再使用型別轉換(Type Cast)將其轉換回原始的型別物件。

下面我們使用文章範例裡的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 修飾的靜態屬性是類屬性,並不屬於物件,所以在序列化物件時不會把類中的靜態屬性序列化了,另外我們也可以使用 transient關鍵字修飾那些我們想在序列化過程中忽略調的物件屬性。

@Test 
public void serializingAndDeserializing_ThenObjectIsTheSame() () 
  throws IOException, ClassNotFoundException { 
    Person person = new Person();
    person.setAge(20);
    person.setName("Joe");
    // 用指定檔案路徑--當前目錄的 test_serialization.txt 檔案建立 FileOutputStream。
    // 在寫入 FileOutputStream 時, FileOutputStream 會在在專案目錄中建立檔案
    // 「test_serialization.txt」
    FileOutputStream fileOutputStream
      = new FileOutputStream("./test_serialization.txt");
    // 以 FileOutputStream 為底層輸出流建立物件輸出流 ObjectOutputStream
    ObjectOutputStream objectOutputStream 
      = new ObjectOutputStream(fileOutputStream);
    // 向 ObjectOutputStream 中寫入 person 物件
    objectOutputStream.writeObject(person);
    // 把資料從流中刷到磁碟上
    objectOutputStream.flush();
    objectOutputStream.close();
    // 用上面的檔案路徑,建立檔案輸入流
    FileInputStream fileInputStream
      = new FileInputStream("./test_serialization.txt");
    // 以檔案輸入流建立物件輸入流 ObjectInputStream
    ObjectInputStream objectInputStream
      = new ObjectInputStream(fileInputStream);
    // 用物件輸入流讀取到檔案中儲存的序列化物件,反序列化成 Java Object 再轉換成 Person 物件
    Person p2 = (Person) objectInputStream.readObject();
    objectInputStream.close(); 
    assertTrue(p2.getAge() == person.getAge());
    assertTrue(p2.getName().equals(person.getName()));
}

上面這個單元測試裡的程式碼演示了,怎麼把 Person 類的物件進行 Java 序列化儲存到檔案中,再從檔案中讀取物件被序列化後的位元組序列,然後還原成Person類的物件。

因為我們的專欄還沒有設計到 Java IO 這塊的內容,所以各種輸入輸出流就不過多進行講解了,為了方便大家閱讀時理解上面的程式,我在上面程式註釋裡已經詳細註釋了每一步完成的操作,這些輸入輸出流我們等到講到 Java IO 體系的時候再詳細進行講解。

總結

今天給大家梳理了 Java Serializable 介面的一些必須要了解的知識,Serializable 介面在我們用 Java 程式設計的時候經常見,但是很多人並不瞭解它的作用,因為它的主要作用還是用於標記類是否是可序列化類,這樣 Java 的 ObjectOutputStream 和 ObjectInputStream 才能對類的物件進行序列化和反序列化。

下一篇我們分享 Iterable 和 Iterator 這兩個名字看起差不多的 Java 內建介面,請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com