在實作物件持久化、網路傳輸、分散式快取時,Java 序列化機制雖然方便(實作 Serializable 即自動生效),但往往伴隨著「某些欄位不想被序列化」的需求。transient 正是為此而生,然而許多開發者僅知皮毛,對 static 交互行為、Externalizable 繞過機制一知半解。本文透過三個可直接執行的完整案例,徹底釐清 transient 的真實行為邊界。
import java.io.*; public class TransientBasicTest { public static void main(String[] args) throws Exception { User user = new User(); user.setUsername("Alexia"); user.setPasswd("123456"); System.out.println("序列化前:username=" + user.getUsername() + ", password=" + user.getPasswd()); // 序列化 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("user.dat")); os.writeObject(user); os.close(); // 反序列化 ObjectInputStream is = new ObjectInputStream(new FileInputStream("user.dat")); user = (User) is.readObject(); is.close(); System.out.println("序列化後:username=" + user.getUsername() + ", password=" + user.getPasswd()); } } class User implements Serializable { private static final long serialVersionUID = 1L; private String username; private transient String passwd; // 關鍵:標記 transient public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPasswd() { return passwd; } public void setPasswd(String passwd) { this.passwd = passwd; } } 序列化前:username=Alexia, password=123456 序列化後:username=Alexia, password=null transient 欄位在反序列化後獲得預設值(null),不會從位元流還原。
若將 username 改為 static,反序列化後似乎仍能讀到值,導致誤以為 static 會被序列化。
public class StaticTransientDeepTest { public static void main(String[] args) throws Exception { User user = new User(); user.setUsername("Alexia"); user.setPasswd("123456"); // 序列化 ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("user.dat")); os.writeObject(user); os.close(); // ★關鍵:反序列化前修改靜態變量 User.username = "jmwang"; // 反序列化 ObjectInputStream is = new ObjectInputStream(new FileInputStream("user.dat")); user = (User) is.readObject(); is.close(); System.out.println("username=" + user.getUsername()); // 輸出 jmwang(當前 JVM 值) System.out.println("password=" + user.getPasswd()); // 輸出 null } } class User implements Serializable { public static String username; // static 不序列化 private transient String passwd; // getter/setter 省略 } username=jmwang password=null static 變量屬於類,不屬於實例,永遠不會被 Java 原生序列化機制序列化。 反序列化後讀到的值,是當前 JVM 中該類的靜態欄位當前值,而非序列化時刻的值。transient 對 static 無任何額外影響。
實作 Externalizable 介面表示「我要完全手動控制序列化格式」,JVM 不再自動處理任何欄位,transient 自然失效。開發者在 writeExternal 中決定寫什麼、readExternal 中決定讀什麼。
import java.io.*; public class ExternalizableBypassTest implements Externalizable { private transient String content = "我被 transient 標記,但會被序列化!"; @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(content); // 手動寫入,無視 transient } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { content = (String) in.readObject(); } public static void main(String[] args) throws Exception { ExternalizableBypassTest obj = new ExternalizableBypassTest(); System.out.println("序列化前:" + obj.content); ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("ext.dat")); os.writeObject(obj); os.close(); ObjectInputStream is = new ObjectInputStream(new FileInputStream("ext.dat")); obj = (ExternalizableBypassTest) is.readObject(); is.close(); System.out.println("序列化後:" + obj.content); // 輸出原字串,證明 transient 失效 } } 序列化前:我被 transient 標記,但會被序列化! 序列化後:我被 transient 標記,但會被序列化! | 場景 | 是否序列化 | 反序列化後值 | 備註 |
|---|---|---|---|
| 一般實例變量 | 是 | 原值 | 需實作 Serializable |
| transient 實例變量 | 否 | 預設值 (null/0/false) | 標準序列化有效 |
| static 變量 | 否 | 當前 JVM 類靜態值 | 與 transient 無關 |
| static + transient | 否 | 當前 JVM 類靜態值 | transient 無額外效果 |
| Externalizable + transient | 由開發者決定 | 由開發者決定 | transient 完全失效 |
@JsonIgnore,更直觀且跨語言。static 保存實例狀態,極易造成誤解。Externalizable 或 Protobuf/Kryo 等高效序列化框架。private static final long serialVersionUID,防止類變更導致反序列化失敗。作者:Alexia (minmin)|原文連結:Java transient 關鍵字使用小記