帶你瞭解Java的序列化(Serializable)與反序列化

這篇可幫助你大體瞭解Java中的序列化(Serializable)。包括爲什麼需要它,如何工作,何時使用它,相關概念(serialVersionUID和transient)以及有關序列化和反序列化的其他必要信息。本教程中的序列化示例保持簡單,以幫助你理解要點。

1.爲什麼要進行Java序列化

序列化過程:
是指把一個Java對象變成二進制內容,實質上就是一個byte[]數組。
因爲序列化後可以把byte[]保存到文件中,或者把byte[]通過網絡傳輸到遠程(IO),這樣,就相當於把Java對象存儲到文件或者通過網絡傳輸出去了。

反序列化過程:
把一個二進制內容(也就是byte[]數組)變回Java對象。有了反序列化,保存到文件中的byte[]數組又可以“變回”Java對象,或者從網絡上讀取byte[]並把它“變回”Java對象。

以下是一些使用序列化的示例:

-以面向對象的方式將數據存儲到磁盤上的文件,例如,Redis存儲Student對象的列表。

-將程序的狀態保存在磁盤上,例如,保存遊戲狀態。

-通過網絡以表單對象形式發送數據,例如,在聊天應用程序中以對象形式發送消息。

一個Java對象要能序列化,必須實現一個特殊的java.io.Serializable接口,它的定義如下:

public interface Serializable {
}

Serializable接口沒有定義任何方法,它是一個空接口。我們把這樣的空接口稱爲“標記接口”(Marker Interface),實現了標記接口的類僅僅是給自身貼了個“標記”,並沒有增加任何方法。

2.Java中的序列化如何工作

當且僅當對象的類實現java.io.Serializable 接口時,該對象纔有資格進行序列化。可序列化 是一個標記接口(不包含任何方法),該接口告訴Java虛擬機(JVM)該類的對象已準備好寫入持久性存儲或通過網絡進行讀取。

默認情況下,JVM負責編寫和讀取可序列化對象的過程。序列化/反序列化功能通過對象流類的以下兩種方法公開:

  • ObjectOutputStream。writeObject(Object):將可序列化的對象寫入輸出流。如果要序列化的某些對象未實現Serializable接口,則此方法將引發NotSerializableException

  • ObjectInputStream。readObject():從輸入流讀取,構造並返回一個對象。如果找不到序列化對象的類,則此方法將引發ClassNotFoundException
    如果序列化使用的類有問題,則這兩種方法都將引發InvalidClassException,如果發生I / O錯誤,則將引發IOException。無論NotSerializableExceptionInvalidClassException是子類IOException異常。

讓我們來看一個簡單的例子。以下代碼將String對象序列化爲名爲“ data.ser”的文件。字符串對象是可序列化的,因爲String類實現了Serializable 接口:

 
String filePath = "data.ser";
String message = "Java Serialization is Cool";
 
try (
    FileOutputStream fos = new FileOutputStream(filePath);
    ObjectOutputStream outputStream = new ObjectOutputStream(fos);
) {
 
    outputStream.writeObject(message);
 
} catch (IOException ex) {
    System.err.println(ex);
}
 

以下代碼反序列化文件“ data.ser”中的String對象:

String filePath = "data.ser";
 
try (
    FileInputStream fis = new FileInputStream(filePath);
    ObjectInputStream inputStream = new ObjectInputStream(fis);
) {
 
    String message = (String) inputStream.readObject();
 
    System.out.println("Message: " + message);
 
} catch (ClassNotFoundException ex) {
    System.err.println("Class not found: " + ex);
} catch (IOException ex) {
    System.err.println("IO error: " + ex);
}

請注意,readObject()返回一個Object類型的對象,因此您需要將其強制轉換爲可序列化的類,在這種情況下爲String類。

讓我們看一個涉及使用自定義類的更復雜的示例。

給定以下學生班

import java.io.*;
import java.util.*;
 
/**
 * Student.java
 * @author chenhh
 */
public class Student extends Person implements Serializable {
    public static final long serialVersionUID = 1234L;
 
    private long studentId;
    private String name;
    private transient int age;
 
    public Student(long studentId, String name, int age) {
        super();
        this.studentId = studentId;
        this.name = name;
        this.age = age;
 
        System.out.println("Constructor");
    }
 
    public String toString() {
        return String.format("%d - %s - %d", studentId, name, age);
    }
}

如上面代碼,你會發現兩點:

-long serialVersionUID類型的常量。

-成員變量age被標記爲transient。

下面讓我解釋一下它們。

2-1.什麼是serialVersionUID常數

serialVersionUID是一個常數,用於唯一標識可序列化類的版本。從輸入流構造對象時,JVM在反序列化過程中檢查此常數。如果正在讀取的對象的serialVersionUID與類中指定的序列號不同,則JVM拋出InvalidClassException。這是爲了確保正在構造的對象與具有相同serialVersionUID的類兼容。
請注意,serialVersionUID是可選的。這意味着如果您不顯式聲明Java編譯器,它將生成一個。
那麼,爲什麼要顯式聲明serialVersionUID呢?
原因是:自動生成的serialVersionUID是基於類的元素(成員變量,方法,構造函數等)計算的。如果這些元素之一發生更改,serialVersionUID也將更改。想象一下這種情況:

-您編寫了一個程序,將Student類的某些對象存儲到文件中。Student類沒有顯式聲明的serialVersionUID。

-有時,您更新了Student類(例如,添加了一個新的私有方法),現在自動生成的serialVersionUID也被更改了。

-您的程序無法反序列化先前編寫的Student對象,因爲那裏的serialVersionUID不同。JVM拋出InvalidClassException。

這就是爲什麼建議爲可序列化類顯式添加serialVersionUID的原因。

2-2.什麼是瞬時變量?

在上面的Student類中,您看到成員變量age被標記爲transient,對嗎?JVM 在序列化過程中跳過瞬態變量。這意味着在序列化對象時不會存儲age變量的值。
因此,如果成員變量不需要序列化,則可以將其標記爲瞬態。
以下代碼將Student對象序列化爲名爲“ students.ser”的文件:

String filePath = "students.ser";
Student student = new Student(123, "John", 22);
 
try (
    FileOutputStream fos = new FileOutputStream(filePath);
    ObjectOutputStream outputStream = new ObjectOutputStream(fos);
) {
 
    outputStream.writeObject(student);
 
} catch (IOException ex) {
    System.err.println(ex);
}

請注意,在序列化對象之前,變量age的值爲22。
下面的代碼從文件中反序列化Student對象:

String filePath = "students.ser";
 
try (
    FileInputStream fis = new FileInputStream(filePath);
    ObjectInputStream inputStream = new ObjectInputStream(fis);
) {
 
    Student student = (Student) inputStream.readObject();
 
    System.out.println(student);
 
} catch (ClassNotFoundException ex) {
    System.err.println("Class not found: " + ex);
} catch (IOException ex) {
    System.err.println("IO error: " + ex);
}

此代碼將輸出以下輸出:

1個
123 - John - 0

3.有關Java序列化的更多信息

你應該瞭解一些有關序列化的重要信息:

  • 序列化一個對象時,它所引用的所有其他對象也會被序列化,依此類推,直到序列化完整的對象樹爲止。
  • 如果超類實現Serializable,則其子類會自動執行。
  • 反序列化可序列化類的實例時,構造函數將不會運行。
  • 如果超類未實現Serializable,則在反序列化子類對象時,超類構造函數將運行。
  • 靜態變量未序列化,因爲它們不是對象本身的一部分。
  • 如果序列化集合或數組,則每個元素都必須可序列化。單個不可序列化的元素將導致序列化失敗(NotSerializableException)。
  • JDK中的可序列化類包括原始包裝器(Integer,Long,Double等),String,Date,collection類…對於其他類,請查閱相關的Javadoc來了解它們是否可序列化。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章