interface Printer { void print(); }
class MyApplication {
private int field = 10; // 成員變數
public void print(final Integer param) {
final long local = 100;
final long local2 = param.longValue() + 100;
Printer printer = new Printer() {
@Override
public void print() {
System.out.println("Local: " + local); // 編譯器常量優化
System.out.println("Local2: " + local2); // 捕獲 final 複本
System.out.println("Param: " + param); // 捕獲 final 引用
System.out.println("Field: " + field); // 透過外部實例引用
}
};
printer.print();
}
}
疑問:param、local、local2 必須 final,但 field 不需要。為什麼?
編譯器生成 MyApplication$1.class,構造函數簽名:
MyApplication$1(MyApplication this$0, long val$local2, Integer val$param)
生成三個 final synthetic 欄位:
this$0:外部類實例引用(關鍵)。val$local2:long 基本類型值拷貝。val$param:Integer 引用拷貝。local 因為是編譯期常量(final long local = 100),被編譯器內聯優化,直接嵌入位元碼 ldc2_w 100,不生成欄位。
// 匿名類構造函數
aload_0 // this
aload_1 // 外部實例 this$0
putfield this$0
lload_2 // local2 值
putfield val$local2
aload 4 // param 引用
putfield val$param
// print() 方法中訪問 field
aload_0
getfield this$0
invokestatic Access$0 // 編譯器生成的靜態存取器
外部類生成 static int Access$0(MyApplication) 返回 field,而非直接訪問 private 欄位——體現封裝與復用。
構造函數將 local2 的值拷貝到匿名類欄位。若允許在匿名類中修改,只會改變拷貝,外部原變數不變。但程式碼讀起來同名,極易誤以為修改了外部變數 → 語義混淆 → 強制 final 防止賦值。
param 引用拷貝給 val$param,兩者指向同一 Integer 物件。若允許 param = new Integer(99),只改變匿名類內部引用,外部 param 不變 → 同樣的語義陷阱。雖可修改物件內部狀態(若非不可變類),但重新賦值引用會誤導 → 強制 final。
匿名類持有 this$0 直接引用外部實例。訪問 field 實際是 this$0.field,任何修改直接作用於外部物件,語義一致,無混淆 → 無需 final。
Java 8 起,若局部變數實際上只賦值一次,可省略 final 關鍵字(Lambda 亦同)。本質仍是「不可變捕獲」,只是編譯器幫你驗證。
| 變數類型 | 捕獲方式 | 修改語義 | 需 final? |
|---|---|---|---|
| 基本類型局部變數 | 值拷貝 | 修改拷貝,外部不變 → 誤導 | 是 |
| 引用類型局部變數 | 引用拷貝 | 重新賦值只改內部引用 → 誤導 | 是 |
| 成員變數 | 外部實例引用 | 直接修改外部物件 → 一致 | 否 |
這不是任意語法限制,而是語義一致性的編譯器保證。理解字節碼實現,才能真正掌握語言設計的深意。