1. 更詳談 transient
在 Java 中,關鍵字 transient 是對序列化器說:「拜託,別碰這個欄位,保存物件時把它忽略!」。如果你把欄位宣告為 transient,它就不會進入序列化後的位元組串流。這對敏感資料(例如密碼)或不需要保存的暫時計算結果特別有用。
範例:為什麼需要 transient?
假設我們有一個使用者類別:
import java.io.Serializable;
public class User implements Serializable {
private String username;
private transient String password; // 不希望保存密碼!
public User(String username, String password) {
this.username = username;
this.password = password;
}
// 這裡有 getters 與 setters
}
如果我們序列化該類別的物件,欄位 password 不會寫入檔案(或其他串流)。這表示在反序列化時,密碼會是預設值——對物件而言是 null,對數值是 0,對 boolean 是 false。
實務上怎麼運作?
做個小實驗。先把使用者序列化:
import java.io.*;
public class TransientDemo {
public static void main(String[] args) throws Exception {
User user = new User("vasya", "qwerty123");
// 將物件寫入檔案
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"));
out.writeObject(user);
out.close();
// 現在把物件讀回來
ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.ser"));
User restored = (User) in.readObject();
in.close();
System.out.println("Username: " + restored.username);
System.out.println("Password: " + restored.password);
}
}
結果:
Username: vasya
Password: null
可以看到,欄位 password 沒有被還原——它是 transient,因此序列化器忽略了它。
在哪些情況下該使用 transient?
- 密碼與 token。永遠不要序列化它們!
- 快取或暫時性資料。例如某個欄位可以「即時」計算得出。
- 不應或無法序列化的物件。例如資料庫連線、串流、socket 的參照。
transient 欄位的行為特性
當物件被反序列化時,所有標示為 transient 的欄位都會取得預設值。若需要讓它們恢復意義,可以在 readObject 方法中手動填回(重新計算快取、向使用者索取密碼等)。
2. serialVersionUID:類別版本的唯一識別碼
serialVersionUID 是一個 long 型別的特殊靜態欄位,用來定義可序列化類別的「版本」。序列化時會寫出 serialVersionUID 的值;反序列化時,JVM 會將它與目前類別中的值比對。若兩者不相同,會丟出例外,物件也無法還原。
如何宣告 serialVersionUID?
非常簡單:
private static final long serialVersionUID = 1L;
通常直接在實作 Serializable 的類別裡宣告:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ... 其他欄位與方法
}
為什麼需要 serialVersionUID?
想像你把某個類別的物件存到檔案,之後又修改了類別結構(新增欄位、重新命名等等)。如果 serialVersionUID 不同,JVM 會認為此類別與舊版本不相容,並不允許你反序列化該物件。這能避免出乎意料的錯誤。
如果不宣告 serialVersionUID 會怎樣?
若沒有明確宣告 serialVersionUID,編譯器會根據類別結構自行產生。但即使是很小的變更(例如新增或刪除欄位)也會改變 serialVersionUID。結果就是:你將無法反序列化由舊版本類別保存的物件。
因此建議一律明確指定 serialVersionUID!
示範:serialVersionUID 不相符
1) 先建立類別並序列化物件:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
public User(String username) {
this.username = username;
}
}
2) 接著修改 serialVersionUID:
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 2L; // 從 1L 改成 2L!
private String username;
public User(String username) {
this.username = username;
}
}
結果:
java.io.InvalidClassException: User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
JVM 清楚地警告:『版本不相容!』
serialVersionUID 該選什麼值?
多半使用簡單的數值(1L、2L、42L);在大型專案中,IDE 也會產生「很長」的數值。重點是——只有在類別結構發生不相容的變更時,才需要修改它。
3. 實作:transient 欄位與 serialVersionUID 的實際運用
範例:含有 transient 欄位的類別
讓我們修改一個教學應用(例如聯絡人管理器),在使用者類別中新增一個用來保存暫時授權 token 的欄位,而且不該被序列化。
import java.io.Serializable;
public class Contact implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String phone;
private transient String sessionToken; // 暫時性的 token
public Contact(String name, String phone, String sessionToken) {
this.name = name;
this.phone = phone;
this.sessionToken = sessionToken;
}
@Override
public String toString() {
return "Contact{" +
"name='" + name + '\'' +
", phone='" + phone + '\'' +
", sessionToken='" + sessionToken + '\'' +
'}';
}
}
現在嘗試序列化並反序列化物件:
import java.io.*;
public class TransientAndSUIDDemo {
public static void main(String[] args) throws Exception {
Contact c = new Contact("Ivan", "+19990001122", "token-12345");
// 保存物件
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("contact.ser"));
out.writeObject(c);
out.close();
// 還原物件
ObjectInputStream in = new ObjectInputStream(new FileInputStream("contact.ser"));
Contact restored = (Contact) in.readObject();
in.close();
System.out.println("序列化之前: " + c);
System.out.println("反序列化之後: " + restored);
}
}
輸出:
序列化之前: Contact{name='Ivan', phone='+19990001122', sessionToken='token-12345'}
反序列化之後: Contact{name='Ivan', phone='+19990001122', sessionToken='null'}
如你所見,欄位 sessionToken 沒有被還原——它是 transient。
範例:關於 serialVersionUID 的小實驗
1) 先以 serialVersionUID = 1L 序列化物件。
2) 然後把 serialVersionUID 改為 2L,再嘗試反序列化同一個檔案。
結果:你會得到 InvalidClassException,就如上所示。
4. 為什麼最好明確指定 serialVersionUID?
- 明確優於隱含。你可以掌控相容性:若類別結構沒有重大變動,就保留原本的 serialVersionUID,物件即可順利反序列化。
- 自動產生具有風險。任何變更都可能改變計算出的值,進而「破壞」已保存資料的相容性。
- IDE 很好用。多數 IDE(例如 IntelliJ IDEA)都能自動產生 serialVersionUID。
5. 使用 transient 與 serialVersionUID 時的常見錯誤
錯誤 1:忘了把敏感欄位標記為 transient。
結果導致密碼或 token 會不小心出現在序列化檔案中。這不只尷尬,更是危險。
錯誤 2:沒有明確宣告 serialVersionUID。
類別被修改後,就無法反序列化舊物件:JVM 視其為不相容;但實際上結構可能並未發生關鍵變化。
錯誤 3:不必要地變更 serialVersionUID。
如果只是新增 getter 或註解,就不需要改 serialVersionUID——否則舊資料將無法反序列化。
錯誤 4:serialVersionUID 不是 static 或不是 final。
此欄位必須宣告為 private static final long serialVersionUID。否則 JVM 不會正確辨識。
錯誤 5:transient 欄位在反序列化後忘了還原。
若其值對物件運作至關重要,請在 readObject 中復原——否則物件可能無法正確運作。
GO TO FULL VERSION