final 與 transient 是 Java 中兩個極具特色的修飾符,前者主管「不可變性」,後者主管「序列化排除」。雖然語法簡單,但背後涉及編譯期常量、執行期初始化、方法鎖定、繼承限制、序列化機制、靜態變量特性等深層機制。本文將系統整理兩者的完整用法、進階特性與常見誤區,並附上可直接執行的程式碼範例。
宣告時不賦值,但在建構子或實例初始化塊中必須賦值,確保使用前已初始化。適用於「執行期才能決定數值」的常量場景。
同時被 static 與 final 修飾,慣例全大寫加底線(如 MAX_SIZE)。編譯期即確定值,存放於類常量池,效能最佳。
表示該參數在方法內「唯讀」,不可重新賦值引用。常用於防止誤修改、表達設計意圖。
public class FinalVariableDemo { private final int n1 = 12; // 宣告即初始化 private final int number = new Random().nextInt(30); // 執行期初始化 private static final int NUMBER2 = new Random().nextInt(40); // 靜態常量,類載入時初始化 // 空白 final,必須在建構子初始化 private final String blankFinal; public FinalVariableDemo(String val) { this.blankFinal = val; } // final 參數:唯讀 public void finalParam(final String sk) { // sk = "new"; // 編譯錯誤:無法修改 final 參數 System.out.println(sk); } } public class Parent { public final void criticalOperation() { // 核心邏輯,子類不可覆寫 System.out.println("關鍵操作:不可變更"); } } final class 如 String、Integer 等包裝類,保證不可被繼承,利於安全性與不可變性設計。
public final class ImmutableConfig { private final String url; public ImmutableConfig(String url) { this.url = url; } public String getUrl() { return url; } } 標記的實例變量在 ObjectOutputStream.writeObject() 時不會被寫入位元流,反序列化時該欄位將得到預設值(null、0、false 等)。
| 規則 | 說明 |
|---|---|
| 僅能修飾實例變量 | 不可用於方法、類、區域變量 |
| static 變量天然不序列化 | 無論是否加 transient,static 屬於類不屬於實例 |
| 自定義類型變量需實作 Serializable | 否則序列化會拋 NotSerializableException |
許多開發者誤以為 static transient 會改變靜態變量行為,實則不然。靜態變量值在反序列化後取決於當前 JVM 中該類的靜態欄位值,而非序列化時的值。以下程式碼可驗證:
public class StaticTransientTest { 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(); // 修改靜態變量(模擬 JVM 重啟或類重載) 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(transient 效果) } } class User implements Serializable { public static String username; // static 不序列化 private transient String passwd; // transient 不序列化 // getter/setter 省略 } 實作 Externalizable 介面時,序列化完全由開發者在 writeExternal/readExternal 手動控制,transient 失效。這意味著即使欄位標記 transient,只要在 writeExternal 寫入,依然會被序列化。
public class ExternalizableDemo implements Externalizable { private transient String content = "我會被序列化!"; @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(); } // main 方法驗證可序列化成功 省略 } | 特性 | final | transient |
|---|---|---|
| 修飾目標 | 變量、方法、類、參數 | 僅實例變量 |
| 核心語義 | 不可變/不可繼承/不可覆寫 | 排除標準序列化 |
| 靜態變量影響 | static final = 編譯期常量 | static 本身不序列化,transient 無額外效果 |
| 繼承關係 | final 類不可繼承,final 方法不可覆寫 | 無關繼承 |
| Externalizable 影響 | 無關 | 完全失效,由開發者手動控制 |
serialVersionUID 管理版本相容性;若需精細控制序列化格式,改用 Externalizable 或採用 JSON (Jackson/Gson) 替代 Java 原生序列化。參考資料:《Java 程式設計思想》第四版、Oracle Java 序列化規範、JDK 原始碼。