匿名內部類 是「沒有名字的內部類」。它同時完成定義類與創建實例兩個動作,且只能使用一次。主要用於:
前提條件:必須繼承一個父類或實現一個介面(不能獨立存在)。
abstract class Person {
public abstract void eat();
}
class Child extends Person {
@Override public void eat() {
System.out.println("eat something");
}
}
public class Demo {
public static void main(String[] args) {
Person p = new Child(); // 向上轉型
p.eat();
}
}
缺點:Child 類污染命名空間、文件分散、僅用一次卻佔用一個 .class 文件。
public class Demo {
public static void main(String[] args) {
Person p = new Person() { // ① new 父類/介面
@Override // ② 大括號內是類體
public void eat() {
System.out.println("eat something");
}
}; // ③ 分號結束語句
p.eat();
}
}
語法結構:new 父類型/介面類型() { 類體 }。編譯器自動生成 Demo$1.class。
interface Person { void eat(); }
Person p = new Person() {
@Override public void eat() { System.out.println("eat something"); }
};
介面無構造器,new Person() 實為「實現介面的匿名類實例化」。
Thread t = new Thread() {
@Override public void run() {
for (int i = 1; i <= 5; i++) System.out.print(i + " ");
}
};
t.start(); // 輸出: 1 2 3 4 5
Runnable r = new Runnable() {
@Override public void run() {
for (int i = 1; i <= 5; i++) System.out.print(i + " ");
}
};
new Thread(r).start();
public void startTask(final String config) { // 參數加 final
new Thread() {
public void run() {
System.out.println("Config: " + config); // 捕獲 config
}
}.start();
}
局部變量同理:
void demo() {
final int count = 10; // 必須 final
Runnable r = new Runnable() {
public void run() {
System.out.println(count); // 捕獲
}
};
}
void demo() {
int count = 10; // 無需寫 final,但不能再賦值
// count = 20; // 取消註釋 → 編譯錯誤:Variable used in lambda expression should be final or effectively final
Runnable r = () -> System.out.println(count); // Lambda 同樣受限
}
本質: 匿名類/Lambda 捕獲的是變量的值副本(基本類型)或引用副本(引用類型)。若外部變量後續修改,內部副本不同步,會導致語義混亂。編譯器強制「不再賦值」保證副本與原始值一致。
// 使用單元素陣列/容器包裝
void demo() {
final int[] counter = {0}; // 陣列引用 final,內容可變
Runnable r = new Runnable() {
public void run() {
System.out.println(++counter[0]); // 修改陣列元素
}
};
}
// 使用 AtomicInteger 等線程安全容器
AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> System.out.println(counter.incrementAndGet());
| 維度 | 匿名內部類 | Lambda 表達式 |
|---|---|---|
| 適用目標 | 任意類/介面(可有多個抽象方法) | 僅函數式介面(單一抽象方法) |
| this 指向 | 匿名類實例自身 | 外部封閉類實例 |
| 序列化 | 可序列化(若實現 Serializable) | 僅目標類型可序列化時才可序列化 |
| 代碼冗長度 | 高(樣板代碼) | 極低 |
| 遞歸調用 | 可用 this.method() | 無法直接遞歸(需通過字段引用) |
| 性能 | 創建類實例,類加載開銷 | invokedynamic,首次調用鏈接,後續極快 |
// 匿名類
Comparator cmp = new Comparator() {
@Override public int compare(String a, String b) {
return a.length() - b.length();
}
};
// Lambda (Java 8+)
Comparator cmp = (a, b) -> a.length() - b.length();
// 或方法引用
Comparator cmp = Comparator.comparingInt(String::length);
// Demo.java
public class Demo {
public void test() {
Runnable r = new Runnable() {
public void run() { System.out.println("hello"); }
};
}
}
// 編譯產物
Demo.class
Demo$1.class // 匿名類:final class Demo$1 implements Runnable
// 字段:final Demo this$0; // 持有外部類引用
// 構造器:Demo$1(Demo outer) { this$0 = outer; }
// 方法:run() { System.out.println("hello"); }
若匿名類訪問外部 final 變量,構造器會新增對應字段並複製值:
// 捕獲 final String config
Demo$1(Demo outer, String config) {
this$0 = outer;
this.config = config; // 額外字段
}
| 問題 | 後果 | 建議 |
|---|---|---|
| 匿名類過長(>30 行) | 可讀性差、難測試、難復用 | 提取為具名靜態內部類 / 獨立類 / Lambda |
| 在循環中創建匿名類捕獲循環變量 | Java 7 編譯錯誤;Java 8+ 若變量變化會捕獲最終值 | 循環內定義 final 局部變量副本,或用 forEach/Stream |
| 匿名類實現 Serializable 但外部類未實現 | 序列化失敗 (NotSerializableException) | 外部類也實現 Serializable,或用靜態內部類 |
| 濫用匿名類實現工具類邏輯 | 代碼分散、無法單元測試 | 遷移至獨立類,依賴注入 |
| this 語義混淆 | 匿名類內 this 指向匿名實例,非外部類 | 訪問外部類成員用 OuterClass.this.field |
ClassName::method、instance::method、Constructor::new。Stream、CompletableFuture 替代手動線程管理。匿名內部類是 Java 1.1 時代為「函數式編程」做出的妥協產物,解決了「一次性實現介面」的語法痛點。Java 8 引入 Lambda 與 invokedynamic 後,匿名類完成了歷史使命,退居為「非函數式介面、需捕獲可變狀態、需遞歸、需序列化」等邊界場景的備選方案。理解其捕獲語義、this 指向、字節碼結構,有助於排查遺留系統疑難雜症;新代碼請擁抱 Lambda 與流式 API,寫出更簡潔、更安全、更高效的現代 Java 代碼。