public class Test {
public static int a = 0;
}
Test t1 = new Test();
t1.a = 10;
Test t2 = new Test();
System.out.println(t1.a); // 10
System.out.println(t2.a); // 10
System.out.println(Test.a); // 10 - 推薦通過類名訪問
三個輸出均為 10,證明共享同一內存。強烈建議通過類名訪問靜態成員,避免誤以為是實例成員。
| 修飾目標 | 語義 |
|---|---|
| 屬性 (字段) | 賦值一次後不可變;引用型指向不可變,但對象內部狀態可變 |
| 方法 | 不可被子類覆蓋 |
| 類 | 不可被繼承 (隱含所有方法 final) |
內存分配時機:對象實例化時 (構造器執行前)
合法初始化位置 (二選一,必須且只能在一處):
public final int a = 0;public final int a; public Test() { a = 0; }原理:構造器執行前字段已分配內存,構造器完成前必須賦值,編譯器強制檢查所有構造路徑均賦值一次。
內存分配時機:類載入時 (<clinit> 階段)
合法初始化位置 (二選一):
public static final int A = 0;public static final int A; static { A = compute(); }構造器中賦值非法:構造器屬於實例創建階段,晚於類初始化,編譯器報錯「variable might not have been initialized」。
若 static final 字段:
則屬於 編譯期常量,編譯器會將其值內聯到調用處 (如 Test.A 直接替換為 0),類似 C/C++ #define。修改值需重新編譯所有調用方。
| 維度 | 非 static final | static final |
|---|---|---|
| 所屬實體 | 對象實例 | 類 |
| 內存分配 | new 實例時 | 類載入時 |
| 初始化截止點 | 構造器結束前 | <clinit> 結束前 |
| 構造器可見性 | 可見 (在構造器執行中) | 不可見 (構造器尚未執行) |
public final List list = new ArrayList<>();
list.add("hello"); // 合法!final 只保護引用不變
list = new ArrayList<>(); // 編譯錯誤:引用不可變
解決:返回不可變視圖 (Collections.unmodifiableList) 或防御性拷貝。
public final int[] arr = {1, 2, 3};
arr[0] = 100; // 合法!
// 編譯錯誤:編譯器無法確認 catch 塊一定執行
public final Thing thing;
public Test() {
try { thing = new Thing(); }
catch (Exception e) { thing = Thing.getDefault(); }
}
// 正確:引入臨時變量
public final Thing thing;
public Test() {
Thing temp;
try { temp = new Thing(); }
catch (Exception e) { temp = Thing.getDefault(); }
thing = temp; // 唯一賦值點,編譯器可驗證
}
static final 常量全大寫下劃線分隔 (MAX_SIZE)private final、無 setter、構造器注入、防御性拷貝可變組件static 決定 存儲位置與生命週期 (類級 vs 實例級),final 決定 可變性承諾 (一次賦值、引用不變)。二者組合產生四種語義截然不同的字段類型,掌握其初始化時機與規則,是編寫正確、高效、線程安全 Java 代碼的基礎。