物件序列化控制輸入輸出

2020-07-16 10:05:18
前面學習了如何控制基本資料的輸入輸出,本節主要講解如何輸入輸出物件資料。物件資料是很複雜的,我們可以利用物件序列化來實現。

物件序列化是什麼

物件序列化(Serialize)指將一個 Java 物件寫入 IO 流中,與此對應的是,物件的反序列化(Deserialize)則指從 IO 流中恢復該 Java 物件。如果想讓某個 Java 物件能夠序列化,則必須讓它的類實現 java.io.Serializable 介面,介面定義如下:
public interface Serializable {
}
Serializable 介面是一個空介面,實現該介面無須實現任何方法,它只是告訴 JVM 該類可以被序列化機制處理。通常建議程式建立的每個 JavaBean 類都實現 Serializable。

ObjectInput 介面與 ObjectOutput 介面分別繼承了 DataInput 和 DataOutput  介面,主要提供用於讀寫基本資料和物件資料的方法。 ObjectInput 介面提供了 readObject() 方法,此方法用於將物件從流中讀出。ObjectOutput 提供了 writeObject() 方法,此方法用於將物件寫入流中。因為 ObjectInput 與 ObjectOutput 都是介面,所以不能建立物件,只能使用分別實現了這兩個介面的 ObjectInputStream 類和 ObjectOutputStream 類來建立物件。

下面講解如何使用 ObjectInputStream 類和 ObjectOutputStream 類來運算元據。

序列化

ObjectOutputStream 類繼承了 OutputStream 類,同時實現了 ObjectOutput 介面,提供將物件序列化並寫入流中的功能,該類的構造方法如下:

public ObjectOutputStream (OutputStream out)

該構造方法需要傳入一個 OutputStream 物件,用來表示將物件二進位制流寫入到指定的 OutputStream 中。 

程式通過以下兩個步驟來序列化物件:

1)建立一個 ObjectOutputStream 物件,如下程式碼所示。

// 建立個 ObjectOutputStream 輸出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

2)呼叫 ObjectOutputStream 物件的 writeObject() 方法輸出可序列化物件,如下程式碼所示。

// 將一個 Person 物件輸出到輸出流中
oos.writerObject(per);

例 1

下面程式定義了一個 Person 類,這個 Person 類就是一個普通的 Java 類,只是實現了 Serializable 介面,該介面表示該類的物件是可序列化的。
import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;

    // 注意此處沒有提供無引數的構造器
    public Person(String name, int age) {
        System.out.println("有引數的構造器");
        this.name = name;
        this.age = age;
    }
    // 省略 name 和 age的setter和getter方法
    ...
}
注意:Person 類的兩個成員變數分別是 String 型別和 int 型別的。如果某個類的成員變數的型別不是基本型別或 String 型別,而是另一個參照型別,那麼這個參照型別必須是可序列化的,否則擁有該型別成員變數的類也是不可序列化的。

下面程式使用 ObjectOutputStream 將一個 Person 物件寫入到磁碟檔案。
public class WriteObject {
    public static void main(String[] args) throws Exception {
        // 建立一個 ObjectOutputStream 輸出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
        Person per = new Person("C語言中文網", 7);
        // 將 Per物件寫入輸出流
        oos.writeObject(per);
    }
}
上面程式中的第 4 行程式碼建立了一個 ObjectOutputStream 輸出流,這個 ObjectOutputStream 輸出流建立在一個檔案輸出流的基礎之上。程式第 7 行程式碼使用 writeObject() 方法將一個 Person 物件寫入輸出流。執行上面程式,將會看到生成了一個 object.txt 檔案,該檔案的內容就是 Person 物件。

反序列化

ObjectInputStream 類繼承了 InputStream 類,同時實現了 ObjectInput 介面,提供了將物件序列化並從流中讀取出來的功能。該類的構造方法如下:

public ObjectInputStream(InputStream out)

該構造方法需要傳入一個 InputStream 物件,用來建立從指定 InputStream 讀取的 ObjectInputStream。

反序列化的步驟如下所示:

1)建立一個 ObjectInputStream 輸入流,這個輸入流是一個處理流,所以必須建立在其他節點流的基礎之上。如下程式碼所示。

// 建立一個ObjectInputStream輸入流
ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("object. txt"));

2)呼叫 ObjectInputStream 物件的 readObject() 方法讀取流中的物件,該方法返回一個 Object 型別的 Java 物件,如果程式知道該 Java 物件的型別,則可以將該物件強制型別轉換成其真實的型別。如下程式碼所示。

// 從輸入流中讀取一個Java物件,並將其強制型別轉換為Person類
Person P = (Person)ois.readObject();

例 2

下面程式是從例 1 中生成的 object.txt 檔案來讀取 Person 物件的步驟。
public class ReadObject {
    public static void main(String[] args) throws Exception {
        // 建立一個ObjectInputStream輸入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
        // 從輸入流中讀取一個 Java物件,並將其強制型別轉換為Person類
        Person p = (Person) ois.readObject();
        System.out.println("名字為:" + p.getName() + "n年齡為:" + p.getAge());
    }
}
上面程式中第 4 行粗體字程式碼將一個檔案輸入流包裝成 ObjectInputStream 輸入流,第 6 行程式碼使用 readObject() 讀取了檔案中的 Java 物件,這就完成了反序列化過程。

反序列化讀取的僅僅是 Java 物件的資料,而不是 Java 類,因此採用反序列化恢復 Java 物件時,必須提供該 Java 物件所屬類的 class 檔案,否則將會引發 ClassNotFoundException 異常。

Person 類只有一個有引數的構造器,沒有無引數的構造器,而且該構造器內有一個普通的列印語句。當反序列化讀取 Java 物件時,並沒有看到程式呼叫該構造器,這表明反序列化機制無須通過構造器來初始化 Java 物件。

如果使用序列化機制向檔案中寫入了多個 Java 物件,使用反序列化機制恢復物件時必須按實際寫入的順序讀取。

當一個可序列化類有多個父類別時(包括直接父類別和間接父類別),這些父類別要麼有無引數的構造方法,要麼也是可序列化的,否則反序列化時將丟擲 InvalidClassException 異常。如果父類別是不可序列化的,只是帶有無引數的構造方法,則該父類別中定義的成員變數值不會序列化到 IO 流中。

Java序列化編號

Java 序列化機制是通過類的序列化編號(serialVersionUID)來驗證版本一致性的。在反序列化時,JVM 會把傳來位元組流中的序列化編號和本地相應實體類的序列化編號進行比較,如果相同就認為一致,可以進行反序列化,否則會丟擲 InvalidCastException 異常

序列化編號有兩種顯式生成方式:
  1. 預設的1L,比如:private static final long serialVersionUID = 1L。
  2. 根據類名、介面名、成員方法及屬性等來生成一個 64 位的雜湊欄位。

當實現 Serializable 介面的物件沒有顯式定義一個序列化編號時,Java 序列化會根據編譯的 Class 自動生成一個序列化編號,這種情況下只要 class 檔案發生變化,序列化號就會改變,否則一直不變。