2018年12月8日 星期六

Java SE 入門教學 - 物件序列化與反序列化

更新時間:12/08/2018

前言

一個類別只有實現了 Serializable 介面,它的物件才是可序列化的。因此如果要序列化某些類別的物件,這些類別就必須實作 Serializable 介面。而實際上,Serializable 是一個空介面,沒有什麼具體內容,它的目的只是簡單的標識一個類別的物件可以被序列化。好處:

(i) 比如說你的記憶體不夠用了,那計算機就要將內存裡面的一部分物件暫時的保存到硬碟中,等到要用的時候再讀入到記憶體中。在比如說你要將某個特定的物件保存到文件中,我隔幾天再把它拿出來用,那麼這時候就要實作 Serializable 介面。

(ii) 在進行 java 的 Socket 編程的時候,你有時候可能要傳輸某一類別的物件,那麼也就要實作 Serializable 介面。最常見的你傳輸一個字符串,它是 JDK 裡面的類別,也實作了 Serializable 介面,所以可以在網絡上傳輸。

(iii) 如果要通過遠程的方法(RMI)去調用一個遠程物件的方法,如在計算機 A 中調用另一台計算機 B 的物件的方法,那麼你需要通過 JNDI 服務獲取計算機 B 目標物件的引用,將物件從 B 傳送到 A,就需要實作 Serializable 介面。例如,在 web 開發中,如果物件被保存在了 Session 中,tomcat 在重啟時要把 Session 物件序列化到硬碟,這個物件就必須實作 Serializable 介面。如果物件要經過分佈式系統進行網絡傳輸或通過 rmi 等遠程調用,這就需要在網絡上傳輸物件,被傳輸的物件就必須實作 Serializable 介面。


一、序列化與反序列的概念

把物件轉換為字節(Byte)序列的過程稱為物件的序列化。
把字節(Byte)序列恢復為物件的過程稱為物件的反序列化。

物件的序列化主要有兩種用途:
1.1) 把物件的字節序列永久地保存到硬碟上,通常存放在一個文件中;
1.2) 在網絡上傳送物件的字節序列。

在很多應用中,需要對某些物件進行序列化,讓它們離開記憶體空間,入住物理硬碟,以便長期保存。比如最常見的是 Web 服務器中的會話物件(Session),當有10萬用戶並發訪問,就有可能出現10萬個會話物件,記憶體可能吃不消,於是網絡容器就會把一些 Session 先序列化到硬碟中,等要用了,再把保存在硬碟中的物件還原到記憶體中。

當兩線程(執行緒)在進行遠程通信時,彼此可以發送各種類型的數據。無論是何種類型的數據,都會以二進制序列的形式在網絡上傳送。發送方需要把這個 Java 的物件轉換為字節序列,才能在網絡上傳送;接收方則需要把字節序列再恢復為 Java 的物件。


二、Java API 中的序列化類別

在 IO 流的篇章 7.5 物件流--Object 中有稍微提到「永續化(Persistence)」、序列化的概念,因為要把可序列化的物件加載或儲存都要透過物件流的幫忙。

java.io.ObjectOutputStream 代表物件輸出流,它的 writeObject(Object obj) 方法可對參數指定的 obj 物件進行序列化,把得到的字節序列寫到一個目標輸出流中。

java.io.ObjectInputStream 代表物件輸入流,它的 readObject() 方法從一個源輸入流中讀取字節序列,再把它們反序列化為一個物件,並將其返回。

只有實作 Serializable 和 Externalizable 介面的類別的物件才能被序列化。Externalizable 介面繼承自 Serializable 介面,實作 Externalizable 介面的類別完全由自身來控制序列化的行為,而僅實作 Serializable 介面的類別可以採用默認的序列化方式。

物件序列化包括如下步驟:
(i) 創建一個物件輸出流,它可以包裝一個其他類型的目標輸出流,如文件輸出流。
(ii) 通過物件輸出流的 writeObject(Object obj) 方法寫出物件。

物件反序列化的步驟如下:
(i) 創建一個物件輸入流,它可以包裝一個其他類型的源輸入流,如文件輸入流。
(ii) 通過物件輸入流的 readObject() 方法讀取物件。

物件序列化與反序列化範例

定義一個 Person 類別,實現 Serializable 介面。

序列化和反序列化 Person 類物件。


三、serialVersionUID 的作用

serialVersionUID: 字面意思上是序列化的版本號,型態為 long,凡是實現 Serializable 介面的類別都有一個表示序列化版本標識符的靜態變數。

Serial ID 有什麼作用?直接看範例

執行結果:序列化與反序列化成功。

我們修改一下 Customer 類別,添加多一個 sex 屬性,如下所示(紅色部分為新增加的代碼):

class Customer implements Serializable {
  //Customer 中沒有定義 serialVersionUID
  private String name;
  private int age;

  // 添加新的 sex 屬性
  private String sex;


  public Customer(String name, int age) {
    this.name = name;
    this.age = age;
  }

  public Customer(String name, int age, String sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
  }


  /*
   * @MethodName toString
   * @Description 多載 Object 的 toString() 方法
   * @return string
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    return "name=" + name + ", age=" + age;
  }
}

修改一下主程式 main 中的代碼,註解產生序列化物件的程式碼,直接讀取舊的,已經序列化好的資料,如下所示(紅色部分為新增加的代碼):

public static void main(String[] args){
  //SerializeCustomer();// 序列化 Customer 物件
  Customer customer = DeserializeCustomer();// 反序列 Customer 物件
  System.out.println(customer);
}

然後執行反序列化的代碼,此時會拋出如下的異常訊息:

意思就是說,文件流中的 class 和 classpath 中的 class,也就是修改過後的 class,不兼容了,處於安全機制考慮,程序拋出了錯誤,並且拒絕載入。那麼如果我們真的有需求要在序列化後添加一個字段或者方法呢?應該怎麼辦?那就是自己去指定 serialVersionUID。在 TestSerialversionUID 例子中,沒有指定 Customer 類別的 serialVersionUID,那麼 Java 編譯器會自動給這個 class 進行一個摘要算法,類似於指紋算法,只要這個文件多一個空格,得到的 UID 就會截然不同的,可以保證在這麼多類中,這個編號是唯一的。所以,添加了一個字段後,由於沒有指定 serialVersionUID,編譯器又為我們生成了另一個 UID,當然和前面保存在文件中的那個不會一樣了,於是就出現了2個序列化版本號不一致的錯誤。因此,只要我們自己指定了serialVersionUID,就可以在序列化後,去添加一個字段,或者方法,而不會影響到後期的還原,還原後的對象照樣可以使用,而且還多了方法或者屬性可以用。

我們只要在 Customer 類別中指定 serialVersionUID 後,重新編譯執行,再去新增或刪除 Customer 類別中的屬性就不會發生上述的情況了!完整代碼如下:

編譯執行完後,重新打開新添加的屬性,並把 main 中的這一行「SerializeCustomer();// 序列化 Customer 物件」註解,再次編譯執行依舊正常運作可以取得先前已經序列化的資料。


四、serialVersionUID 的取值

serialVersionUID 的取值是 Java 執行時環境根據類別的內部細節自動生成的。如果對類別的源代碼作了修改,再重新編譯,新生成的類別文件的 serialVersionUID 的取值有可能也會發生變化。

類別的 serialVersionUID 的默認值完全依賴於 Java 編譯器的實現,對於同一個類別,用不同的 Java 編譯器編譯,有可能會導致不同的 serialVersionUID,也有可能相同。為了提高 serialVersionUID 的獨立性和確定性,強烈建議在一個可序列化類別中明確的定義 serialVersionUID,為它賦予明確的值。

明確地定義 serialVersionUID 有兩種用途:
 (i) 在某些場合,希望類別的不同版本對序列化兼容,因此需要確保類別的不同版本具有相同的 serialVersionUID。
 (ii) 在某些場合,不希望類別的不同版本對序列化兼容,因此需要確保類別的不同版本具有不同的 serialVersionUID。





沒有留言:

張貼留言