在 Java 序列化體系中,transient 是一個關鍵但常被誤解的修飾符。本文將從語法作用、適用場景、底層原理及 JDK 原始碼實戰(以 HashMap 為例)四個維度,徹底講透這個知識點。
定義: 修飾實例欄位,使其從類的預設序列化形式中排除。即使用 ObjectOutputStream.defaultWriteObject() / defaultReadObject() 時,該欄位不會被自動寫入/讀取。
關鍵點: 它不代表「不序列化」,而是「不使用預設序列化機制」。開發者仍可通過自定義 writeObject/readObject 實現該欄位的序列化邏輯。
如銀行密碼、Token、信用卡號等敏感資訊,不希望出現在序列化後的位元組流中(防止被反序列化還原或日誌洩露)。
public class UserAccount implements Serializable {
private String username;
// 密碼不參與預設序列化
private transient String password;
// 若業務確需持久化,可在 writeObject 中加密寫入
}
註:此用法常見於教材,但實務上更推薦使用專門的加密序列化框架或避免敏感資料進入領域模型。
這是 JDK 集合框架大量採用的模式。以 HashMap 為例,其核心儲存結構 table(陣列+鏈表/紅黑樹)被標記為 transient:
// HashMap.java 片段 transient Node<K,V>[] table;
StackOverflowError(遞迴深度過大)。hash(key) 計算索引更合理。寫入: 僅序列化 size、loadFactor 等元數據,然後遍歷 entrySet() 依次寫入 Key-Value。
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject(); // 寫入非 transient 欄位
s.writeInt(size); // 額外寫入 size
for (Map.Entry<K,V> e : entrySet()) {
s.writeObject(e.getKey());
s.writeObject(e.getValue());
}
}
讀取: 讀取元數據後,依次讀取 Key-Value 並調用 putForCreate 重建表結構。
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
s.readInt(); // 讀取 size
for (int i = 0; i < size; i++) {
K key = (K) s.readObject();
V value = (V) s.readObject();
putForCreate(key, value); // 重新計算 hash、放入桶位
}
}
| 場景 | 處理方式 | 關鍵點 |
|---|---|---|
| 敏感欄位 | transient + 不自定義序列化 | 完全排除,反序列化後為 null/default |
| 衍生/可計算欄位 | transient + readObject 中重算 | 如緩存值、哈希碼、聚合統計值 |
| 複雜結構 | transient + 完整自定義 writeObject/readObject | 只序列化邏輯數據,重建時按業務邏輯組裝 |
| 非 Serializable 物件引用 | transient + 代理模式/外部化 | 如資料庫連接、執行緒池、原生資源句柄 |
transient 修飾的欄位反序列化後一定是 null。readObject 並手動賦值,則為手動賦的值;否則才是預設值。static 欄位需要 transient 才不序列化。static 欄位屬於類不屬於實例,預設就不參與序列化,加不加 transient 效果相同。transient 就無法序列化該欄位。writeObject 中顯式 s.writeObject(field) 即可序列化。transient 的本質是「交還序列化控制權給開發者」。理解它與 Serializable、Externalizable、writeReplace/readResolve 的協作關係,是掌握 Java 序列化進階能力的關鍵。下次看到 HashMap、ArrayList、ThreadPoolExecutor 等核心類中大量 transient 欄位時,你就知道:它們正在用最高效的方式保護你的數據完整性與效能。