1. Serializable 介面
還記得上一堂課的範例嗎?我們想要序列化的 Java 類別必須實作特殊介面 — java.io.Serializable。這是所謂的標記介面:它不包含任何方法,只是「標記」類別可被序列化。若類別實作了這個介面,JVM 就允許序列化其物件的標準流程。
不建議對所有東西一股腦地序列化,因為不是所有物件都能或需要被轉成位元組保存。有些物件依賴作業系統狀態、已開啟的檔案或網路連線。因此,Java 要求你明確將類別標記為可序列化。
標記介面就像盒子上的「允許打包」貼紙。 如果沒有這張貼紙——包裝工(JVM)就會拒絕工作。
範例:宣告可序列化的類別
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
// 建構子、getter 與 setter
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 為了美觀:toString() 方法
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
請注意:
- 我們只是在類別宣告上加上 implements Serializable。
- 不需要實作任何方法(此介面是空的)。
- 所有可被序列化的 Java 標準類別(例如,ArrayList、HashMap、String)都已經實作了 Serializable。
2. 如何讓自己的類別可序列化
規則第1條:只要加入 implements Serializable
對於類別本身,這就足夠了。但還有一些細節!
重要:所有被引用的物件也必須是可序列化的。
如果你的類別有指向其他物件的欄位,它們也必須可序列化。例如:
public class Profile implements Serializable {
private User user; // User 必須可序列化!
private int level;
}
只要有任一欄位不可序列化,嘗試序列化時就會拋出例外。
3. 序列化與反序列化範例
來看看如何把物件序列化到檔案、以及從檔案反序列化。這會用到類別 ObjectOutputStream 和 ObjectInputStream。
範例:將 User 物件序列化到檔案
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
User user = new User("Alice", 30);
// 將物件存到檔案
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("物件已成功序列化到檔案 user.ser");
} catch (IOException e) {
System.out.println("序列化錯誤: " + e.getMessage());
}
}
}
這裡發生了什麼?
- 建立物件 User。
- 開啟串流 ObjectOutputStream,寫入檔案 "user.ser"。
- 呼叫 writeObject(user)。此時 JVM 會把物件轉成位元組串流並存到檔案。
範例:從檔案反序列化物件
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
// 從檔案讀取物件
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println("物件已成功還原: " + user);
} catch (IOException | ClassNotFoundException e) {
System.out.println("反序列化錯誤: " + e.getMessage());
}
}
}
這裡發生了什麼?
- 開啟串流 ObjectInputStream,從檔案 "user.ser" 讀取。
- 呼叫 readObject()。JVM 會從位元組還原物件。
- 別忘了將結果轉型成需要的型別(User),因為 readObject() 會回傳 Object。
- 若找不到類別 User,可能會拋出 ClassNotFoundException。
綜合範例:序列化與反序列化
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
User user = new User("Bob", 22);
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("序列化完成!");
} catch (IOException e) {
System.out.println("序列化錯誤: " + e.getMessage());
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User loaded = (User) ois.readObject();
System.out.println("反序列化完成! " + loaded);
} catch (IOException | ClassNotFoundException e) {
System.out.println("反序列化錯誤: " + e.getMessage());
}
}
}
結果:
序列化完成!
反序列化完成! User{name='Bob', age=22}
4. 序列化時「底層」發生了什麼
當你呼叫 writeObject 時,JVM 會先檢查類別是否實作介面 Serializable。如果類別未被標記為可序列化,會丟出例外。接著 JVM 會遍歷物件的所有一般欄位(也就是既非 static、也非 transient 的欄位),並把它們的值寫入位元組串流。若其中包含其他物件,且那些物件也實作了 Serializable,就會遞迴地序列化它們。
在反序列化時,物件會在未呼叫一般建構子的情況下被建立,欄位則以儲存的值填入——就像「沒有建構子的建構子」把物件從位元組串流中喚醒。
有些欄位不會被序列化。static 欄位屬於類別本身,而非個別物件,因此其值不會被保存。標記為 transient 的欄位也會被略過——這對暫存資料、快取或像密碼這類機密資訊很方便。
序列化流程示意圖
flowchart TB
A[記憶體中的 User 物件] -- writeObject --> B[ObjectOutputStream]
B -- 儲存位元組 --> C[檔案 user.ser]
C -- readObject --> D[ObjectInputStream]
D -- 還原 --> E[記憶體中的 User 物件]
5. 使用 Serializable 的常見錯誤
錯誤第1點:指向不可序列化物件的欄位。 如果在類別 User 中出現像 Thread 或 Socket 這樣的欄位,序列化將無法進行。不是所有物件都能序列化——請記住這點!
錯誤第2點:不可序列化的巢狀類別。 如果類別 User 包含非 static 的內部類別,序列化可能會失敗。最好使用 static 巢狀類別或獨立的類別檔。
錯誤第3點:嘗試序列化 static 欄位。 static 欄位不會被序列化——它們屬於類別而非物件。反序列化後,static 欄位的值將以類別中定義的值為準,而非序列化時物件的值。
錯誤第4點:類別版本不相符。 如果在序列化之後修改了類別結構(例如新增或移除欄位),再嘗試反序列化舊物件時,可能會出現 InvalidClassException。為了控管版本,會使用特殊欄位 serialVersionUID——下一堂課我們會更詳細討論它。
GO TO FULL VERSION