CodeGym /課程 /JAVA 25 SELF /ObjectOutputStream、ObjectInputStream:操作串流

ObjectOutputStream、ObjectInputStream:操作串流

JAVA 25 SELF
等級 42 , 課堂 3
開放

1. 前言

在 Java 中,只有明確允許的物件才能被序列化。為此,類別必須實作特殊的介面 — java.io.Serializable

import java.io.Serializable;

public class Person implements Serializable {
    // 欄位、建構子、方法
}

Serializable 是標記介面:它沒有任何方法,只是告訴 JVM — 「這個類別可以被序列化,別擔心!」。 如果嘗試序列化一個未實作 Serializable 的類別的物件,會得到例外 NotSerializableException。哪怕只有一個欄位(或巢狀物件)不可序列化,序列化也會失敗。

ObjectOutputStream 與 ObjectInputStream

  • ObjectOutputStream — 將物件寫入串流的類別(例如寫入檔案或透過網路)。
  • ObjectInputStream — 從串流讀取物件的類別。

它們成對工作:一個序列化物件,另一個 — 反序列化。

主要方法

  • writeObject(Object obj) — 將物件序列化並寫入串流。
  • readObject() — 從串流讀取物件、反序列化並回傳。

重要:兩個類別都建構在一般 I/O 串流之上(OutputStreamInputStream)。最常與檔案串流 — FileOutputStreamFileInputStream 搭配使用,也可以用於網路串流。

2. 序列化範例

來看一個簡單範例:將 Person 類別的物件序列化與反序列化。

步驟 1. 定義類別

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;
    // transient 欄位不會被序列化
    private transient String secret;

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

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", secret='" + secret + "'}";
    }
}

步驟 2. 將物件序列化到檔案

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializeDemo {
    public static void main(String[] args) throws Exception {
        Person person = new Person("Alice", 30, "likes pizza");

        // 建立寫入檔案的串流
        FileOutputStream fileOut = new FileOutputStream("person.bin");
        ObjectOutputStream out = new ObjectOutputStream(fileOut);

        // 儲存物件
        out.writeObject(person);

        // 關閉串流
        out.close();
        fileOut.close();

        System.out.println("物件已序列化至檔案 person.bin");
    }
}

步驟 3. 從檔案反序列化物件

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializeDemo {
    public static void main(String[] args) throws Exception {
        // 開啟從檔案讀取的串流
        FileInputStream fileIn = new FileInputStream("person.bin");
        ObjectInputStream in = new ObjectInputStream(fileIn);

        // 還原物件
        Person person = (Person) in.readObject();

        in.close();
        fileIn.close();

        System.out.println("物件已反序列化: " + person);
    }
}

預期輸出

物件已序列化至檔案 person.bin
物件已反序列化: Person{name='Alice', age=30, secret='null'}

注意! 欄位 transient 不會被序列化。反序列化後其值將為 null。這對暫時性或敏感資料很重要。

3. 限制與特點

所有欄位都必須可序列化

如果類別擁有未實作 Serializable 的欄位(或包含此類物件),序列化會以錯誤結束。例如,ThreadSocket 之類的欄位無法「直接」做成可序列化。

static 與 transient 欄位

  • 靜態欄位static)不會被序列化:它們屬於類別,而非特定物件。
  • transient 欄位 — 被標記為 transient 的欄位會被明確排除在序列化之外,復原後會取得預設值(null0 等)。

例外

  • 嘗試序列化未實作 Serializable 的物件時,會拋出 NotSerializableException
  • 在反序列化期間可能發生錯誤:找不到檔案、類別不相符、資料毀損等。

類別版本

如果在序列化之後更改了類別結構(例如新增/刪除欄位),反序列化時可能會出現 InvalidClassException。為了控制版本,會使用特殊欄位 serialVersionUID(詳情將在後續講座說明)。

4. 實作:將物件序列化與反序列化到檔案

假設我們有一個 Person 類別,想把人員清單保存到檔案並再讀回。

Person 類別(可序列化)

import java.io.Serializable;

public class Person implements Serializable {
    private String name;
    private int age;
    private transient String secret; // 不會被序列化

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

    @Override
    public String toString() {
        return name + " (" + age + "), secret: " + secret;
    }
}

序列化人員清單

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;

public class SerializeListDemo {
    public static void main(String[] args) throws Exception {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Alice", 30, "likes pizza"));
        people.add(new Person("Bob", 25, "hates broccoli"));

        FileOutputStream fileOut = new FileOutputStream("people.bin");
        ObjectOutputStream out = new ObjectOutputStream(fileOut);

        // 儲存清單
        out.writeObject(people);

        out.close();
        fileOut.close();

        System.out.println("人員清單已序列化。");
    }
}

反序列化人員清單

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;

public class DeserializeListDemo {
    public static void main(String[] args) throws Exception {
        FileInputStream fileIn = new FileInputStream("people.bin");
        ObjectInputStream in = new ObjectInputStream(fileIn);

        // 還原清單
        List<Person> people = (List<Person>) in.readObject();

        in.close();
        fileIn.close();

        for (Person p : people) {
            System.out.println(p);
        }
    }
}

結果:

Alice (30), secret: null
Bob (25), secret: null

5. 常見錯誤

錯誤 №1:類別未實作 Serializable。 若忘了加入 implements Serializable,在嘗試序列化時會得到 NotSerializableException。這是最常見且最容易犯的錯誤。

錯誤 №2:不可序列化的欄位。 若物件包含不可序列化的欄位(例如 ThreadSocket 或任何沒有 Serializable 的型別),序列化會「失敗」。請將這些欄位標記為 transient,或使其可序列化。

錯誤 №3:更改類別結構。 如果物件已序列化,但之後類別被修改(新增/刪除欄位),讀取時可能出現 InvalidClassException。請指定 serialVersionUID 以穩定控制版本。

錯誤 №4:嘗試序列化靜態欄位。 具有 static 修飾子的欄位不會被序列化。反序列化後,它們的值會是類別中的預設值,而非序列化當下的值。

錯誤 №5:未關閉串流。 如果在操作後未關閉串流,可能導致檔案損毀或資源外洩。請使用 try-with-resources 或明確關閉串流。

錯誤 №6:類別不匹配。 如果類別被重新命名或移至其他套件,反序列化將無法運作 — 需要與串流中保存的類別名稱與套件完全一致。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION