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 串流之上(OutputStream 與 InputStream)。最常與檔案串流 — FileOutputStream 與 FileInputStream 搭配使用,也可以用於網路串流。
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 的欄位(或包含此類物件),序列化會以錯誤結束。例如,Thread 或 Socket 之類的欄位無法「直接」做成可序列化。
static 與 transient 欄位
- 靜態欄位(static)不會被序列化:它們屬於類別,而非特定物件。
- transient 欄位 — 被標記為 transient 的欄位會被明確排除在序列化之外,復原後會取得預設值(null、0 等)。
例外
- 嘗試序列化未實作 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:不可序列化的欄位。 若物件包含不可序列化的欄位(例如 Thread、Socket 或任何沒有 Serializable 的型別),序列化會「失敗」。請將這些欄位標記為 transient,或使其可序列化。
錯誤 №3:更改類別結構。 如果物件已序列化,但之後類別被修改(新增/刪除欄位),讀取時可能出現 InvalidClassException。請指定 serialVersionUID 以穩定控制版本。
錯誤 №4:嘗試序列化靜態欄位。 具有 static 修飾子的欄位不會被序列化。反序列化後,它們的值會是類別中的預設值,而非序列化當下的值。
錯誤 №5:未關閉串流。 如果在操作後未關閉串流,可能導致檔案損毀或資源外洩。請使用 try-with-resources 或明確關閉串流。
錯誤 №6:類別不匹配。 如果類別被重新命名或移至其他套件,反序列化將無法運作 — 需要與串流中保存的類別名稱與套件完全一致。
GO TO FULL VERSION