首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

深入解析 Java 關鍵字 final 與 transient:從基本用法到序列化陷阱

     編輯:Java 相關知識

前言

final 與 transient 是 Java 中兩個極具特色的修飾符,前者主管「不可變性」,後者主管「序列化排除」。雖然語法簡單,但背後涉及編譯期常量、執行期初始化、方法鎖定、繼承限制、序列化機制、靜態變量特性等深層機制。本文將系統整理兩者的完整用法、進階特性與常見誤區,並附上可直接執行的程式碼範例。


第一部分:final 關鍵字全方位解析

1. 修飾變量:不可變的承諾

基本類型 vs 參考類型

  • 基本類型:數值一經初始化即不可更改。
  • 參考類型:引用地址不可變(不能指向新物件),但物件內部狀態仍可修改。

空白 final (Blank Final)

宣告時不賦值,但在建構子或實例初始化塊中必須賦值,確保使用前已初始化。適用於「執行期才能決定數值」的常量場景。

static final:編譯期常量

同時被 static 與 final 修飾,慣例全大寫加底線(如 MAX_SIZE)。編譯期即確定值,存放於類常量池,效能最佳。

修飾方法參數

表示該參數在方法內「唯讀」,不可重新賦值引用。常用於防止誤修改、表達設計意圖。

代碼範例:final 變量完整示範

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); } }

2. 修飾方法:鎖定實作,禁止覆寫

  • 設計目的:確保核心邏輯不被子類改寫,維護行為一致性(如 Template Method 模式中的關鍵步驟)。
  • 效能迷思:JDK 1.5 前 final 方法有內聯優化優勢,現代 JVM 已自動熱點內聯,無需為效能刻意加 final。
public class Parent { public final void criticalOperation() { // 核心邏輯,子類不可覆寫 System.out.println("關鍵操作:不可變更"); } }

3. 修飾類:終極不可繼承

final class 如 String、Integer 等包裝類,保證不可被繼承,利於安全性與不可變性設計。

public final class ImmutableConfig { private final String url; public ImmutableConfig(String url) { this.url = url; } public String getUrl() { return url; } }

第二部分:transient 關鍵字與 Java 序列化深度剖析

1. 核心作用:跳過序列化

標記的實例變量在 ObjectOutputStream.writeObject() 時不會被寫入位元流,反序列化時該欄位將得到預設值(null、0、false 等)。

2. 適用場景

  • 敏感資料(密碼、Token、信用卡號)
  • 衍生/計算欄位(可從其他欄位重算)
  • 非 Serializable 的第三方物件引用
  • 緩存、Logger、Thread 等運行時專用資源

3. 關鍵限制與規則

規則說明
僅能修飾實例變量不可用於方法、類、區域變量
static 變量天然不序列化無論是否加 transient,static 屬於類不屬於實例
自定義類型變量需實作 Serializable否則序列化會拋 NotSerializableException

4. 經典陷阱:static + transient 的行為

許多開發者誤以為 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 省略 }

5. 進階:Externalizable 介面繞過 transient

實作 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 方法驗證可序列化成功 省略 }

總結對照表

特性finaltransient
修飾目標變量、方法、類、參數僅實例變量
核心語義不可變/不可繼承/不可覆寫排除標準序列化
靜態變量影響static final = 編譯期常量static 本身不序列化,transient 無額外效果
繼承關係final 類不可繼承,final 方法不可覆寫無關繼承
Externalizable 影響無關完全失效,由開發者手動控制

最佳實務建議

  1. final:預設給類、方法加 final(除非確定需繼承/覆寫),變量若非必要可變皆加 final,提升程式碼可讀性與執行緒安全。
  2. transient:敏感欄位務必加上;搭配 serialVersionUID 管理版本相容性;若需精細控制序列化格式,改用 Externalizable 或採用 JSON (Jackson/Gson) 替代 Java 原生序列化。

參考資料:《Java 程式設計思想》第四版、Oracle Java 序列化規範、JDK 原始碼。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved