1. 為什麼需要序列化
想像你的物件就像你去度假時要帶的東西。序列化就像把整個行李箱的內容打包到一個特殊容器裡,能放進託運行李或寄送。相對地,反序列化就是拆封這個容器,取回原樣的物品。
本質上,序列化把物件轉成位元組串流,可以保存到檔案、透過網路傳送,或直接存放在記憶體中。反序列化則相反:從這個串流還原物件。更簡單地說,序列化就像把物件「冷凍」,之後再「解凍」回來,拿回相同狀態。
在程式多次啟動之間保存物件狀態
最常見的情境之一是保存程式的狀態。例如,你有使用者清單、遊戲成績或應用程式設定。這些都適合直接以物件形式保存。為了避免資料在重新啟動之間遺失,會將它們序列化到檔案,下一次啟動時再反序列化。
一個好例子是一般的遊戲存檔。當玩家通關某一關時,他的進度被「冷凍」並透過序列化寫入檔案。隔天他啟動遊戲,進度被「解凍」:檔案中的資料再次變回物件,玩家可從上次中斷處繼續。
我們來做一個簡單的存檔:
import java.io.*;
// Player 類別必須實作 Serializable
class Player implements Serializable {
String name;
int score;
Player(String name, int score) {
this.name = name;
this.score = score;
}
}
public class GameSaveExample {
public static void main(String[] args) throws Exception {
// 建立玩家物件
Player player = new Player("Ihor", 1500);
// --- 儲存(序列化) ---
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
out.writeObject(player);
System.out.println("進度已儲存!");
}
// --- 載入(反序列化) ---
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
Player loaded = (Player) in.readObject();
System.out.println("進度已載入: " + loaded.name + ",分數 " + loaded.score);
}
}
}
請注意:為了讓這段程式碼能運作,類別 Player 必須實作介面 Serializable。更多細節會在下一講介紹!
- Player 是包含姓名與分數欄位的普通類別,並以介面 Serializable 標示(implements Serializable)。
- ObjectOutputStream 將物件寫入檔案 "save.dat"。
- ObjectInputStream 讀回同一個物件。
- 結果就是實際的存檔:下次啟動時,程式會載入狀態相同的玩家物件。
透過網路與不同 JVM 之間傳遞物件
在分散式系統中,經常需要在不同的程式甚至不同的機器之間傳遞物件。例如你有客戶端與伺服器要交換訊息。序列化允許在一端「打包」物件,透過網路傳送,並在另一端「解包」還原。
範例:客戶端傳送訂單物件(Order)到伺服器,伺服器接收後反序列化並加以處理。
在 Java 技術中的使用
- RMI(Remote Method Invocation):允許呼叫遠端物件的方法 — 序列化用於傳遞引數與傳回值。
- HTTP 工作階段:在 servlets 中,工作階段中的物件會在容器重啟時被序列化。
- JMS(Java Message Service):元件之間的訊息可以是序列化的。
- 快取:物件可以序列化以存放於快取(寫入磁碟或分散式儲存)。
快取與可攜性
若你想快速保存中間結果(例如用於快取),序列化是絕佳工具。將物件序列化後保存到磁碟或記憶體,之後即可快速還原,不必重做計算。
2. 序列化的使用範例
將使用者集合存到檔案
假設你有一個類別 User:
public class User {
String name;
int age;
// ... 其他欄位
}
還有一個使用者清單:
List<User> users = new ArrayList<>();
users.add(new User("Vasya", 25));
users.add(new User("Masha", 30));
// ... 等等
若要把這個清單保存到檔案,你會對它做序列化。等需要時再反序列化,就能取回相同的清單與相同的使用者。提醒一下,類別 User(以及它的所有欄位)必須支援序列化,也就是要實作 Serializable。
在客戶端與伺服器之間傳送訊息
經典的例子是聊天。使用者輸入一則訊息,物件 Message 被序列化並透過網路送出。伺服器接收位元組串流,將其反序列化為物件,處理後也許再轉發。
import java.io.*;
import java.net.*;
// 訊息必須是 Serializable
class Message implements Serializable {
String text;
Message(String text) {
this.text = text;
}
}
// 伺服器
class Server {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(5000)) {
System.out.println("伺服器正在等待連線...");
Socket socket = serverSocket.accept();
System.out.println("客戶端已連線!");
try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
Message msg = (Message) in.readObject();
System.out.println("收到訊息: " + msg.text);
}
}
}
}
// 客戶端
class Client {
public static void main(String[] args) throws Exception {
try (Socket socket = new Socket("localhost", 5000)) {
try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
Message msg = new Message("你好,伺服器!");
out.writeObject(msg);
System.out.println("訊息已送出!");
}
}
}
}
運作方式:
- 先啟動 Server(它會等待連線)。
- 接著啟動 Client(它會連到 "localhost:5000")。
- 客戶端序列化 Message 物件,並透過 socket 送出。
- 伺服器接收位元組串流,將其反序列化並印出文字。
這裡我們使用 socket(ServerSocket、Socket)——這是你之後會學到的網路溝通機制。此刻重要的不是網路細節,而是核心概念:客戶端建立 Message 物件、將其序列化並傳送;伺服器收到位元組串流,反序列化還原為物件並印出訊息。因此,即使你還不熟 ServerSocket 與 Socket,這個例子仍清楚展示序列化的價值:我們能把物件「打包」、經由網路傳遞,並在另一端不需額外轉換就能解包使用。
物件快取
在大型應用中常用快取來加速。例如,會把昂貴計算的結果序列化後保存到快取(檔案、資料庫或分散式儲存)。下次請求時,只要反序列化物件即可快速回復結果。
import java.io.*;
// 我們想要快取的計算結果
class Result implements Serializable {
int value;
Result(int value) {
this.value = value;
}
}
public class CacheExample {
private static final String CACHE_FILE = "cache.dat";
public static void main(String[] args) throws Exception {
Result result;
// 檢查是否有快取
File file = new File(CACHE_FILE);
if (file.exists()) {
// 從快取載入結果
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
result = (Result) in.readObject();
System.out.println("已從快取載入: " + result.value);
}
} else {
// 「昂貴」的計算(此處僅示範平方)
int x = 12345;
System.out.println("計算中...(這會很久)");
result = new Result(x * x);
// 將結果保存到快取
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
out.writeObject(result);
System.out.println("已保存到快取: " + result.value);
}
}
}
}
3. 序列化的限制與風險
序列化是強大的工具,但也有不少坑。我們來看看主要的限制與風險。
不是所有物件都能被序列化
在 Java 中,並非所有物件都能「開箱即用」地序列化。例如,與外部資源相關的物件(檔案、網路連線、輸入輸出串流)通常不能序列化。這很合理:無法序列化「開啟中的檔案」或「存活的網路連線」——它們的狀態取決於作業系統與執行環境。
範例:具有 FileInputStream 欄位的類別無法序列化——嘗試序列化時會發生錯誤。
安全性議題
序列化可能成為安全弱點。如果你反序列化來自不受信任來源(例如網際網路)的資料,攻擊者可能夾帶惡意的位元組串流,導致程式出現非預期行為,甚至在某些情況下執行惡意程式碼。
規則: 絕對不要反序列化來自不受信任來源的資料! 這就像接受未知寄件者的包裹——裡面可能是任何東西。
版本相容性
如果你更動了類別結構(例如新增或刪除欄位),先前序列化的物件可能與新版本類別不相容,導致反序列化時發生錯誤。更詳細的討論會在後續課程中說明。
效能
Java 的二進位序列化相當快速,但有時不夠精簡,也不一定方便與其他語言交換資料。與外部系統交換時,常會使用文字格式(JSON、XML)。
4. 初學序列化時的常見錯誤
錯誤 №1:嘗試序列化未實作介面 Serializable 的物件。
結果會得到例外 NotSerializableException。別忘了在類別上明確宣告 implements Serializable,並確保所有欄位也能被序列化!
錯誤 №2:序列化含有不可序列化欄位的物件。
如果類別包含不支援序列化的類型欄位(例如串流或資料庫連線),序列化將無法成功。解法是將這些欄位標記為 transient(稍後會提到)。
錯誤 №3:從不受信任來源反序列化資料。
這可能導致安全性弱點,甚至執行惡意程式碼。只信任由你自己的程式序列化產生的資料!
錯誤 №4:在序列化之後修改類別結構。
如果你先保存了物件,接著在類別中新增或刪除欄位,反序列化時可能發生錯誤,或出現「奇怪」的數值。詳情會在後續課程中說明。
GO TO FULL VERSION