1. 認識匿名類別
想像你有一個類別 Animal,用來描述動物的行為。這個類別有一個方法 say(),會輸出 "動物發出聲音"。你需要建立一個會像狗一樣行為並輸出 "汪!" 的物件,但你不想為此另外建立一個 Dog.java 檔案。
這時候 匿名類別 就能派上用場。它是沒有名稱的類別,會在使用處直接宣告並建立。🚀 它能讓你「就地」繼承某個類別,而不需要額外建立檔案。與其為只在單一位置使用的許多小類別新增檔案,不如在需要的地方直接把邏輯寫好。
語法長什麼樣?
匿名類別的語法看起來有點特別,但其實很簡單:
TipPeremennoi imya = new TipDlyaNasledovaniya() {
// 在這裡撰寫匿名類別的主體
// 覆寫父類別的方法
};
讓我們拆解一下這段語法:
- TipPeremennoi imya:這是一般的變數宣告,用來保存我們的新物件。
- new TipDlyaNasledovaniya():看起來像是在建立新物件。但這裡用的是我們想要繼承的類別名稱。注意,括號 () 之後沒有分號 !
- { ... }:接著打開大括號,撰寫匿名類別的所有邏輯。我們可以覆寫父類別的方法,或加入自訂邏輯。
從一般類別繼承的範例:
回到我們的動物範例。
class Animal {
void say() {
System.out.println("動物發出聲音");
}
}
// 建立匿名類別,繼承自 Animal
Animal dog = new Animal() {
// 覆寫方法 say()
@Override
void say() {
System.out.println("汪汪! 🐶");
}
};
Animal cat = new Animal() {
@Override
void say() {
System.out.println("喵喵! 🐱");
}
};
dog.say(); // 會輸出: 汪汪! 🐶
cat.say(); // 會輸出: 喵喵! 🐱
在這個例子中,我們建立了兩個物件:dog 與 cat。它們實際上都是從 Animal 繼承而來的匿名類別。同時我們並沒有建立任何額外檔案。
方案比較
// 一般作法(建立獨立的類別)
class Dog extends Animal {
@Override
void say() { System.out.println("汪!"); }
}
Animal dog = new Dog();
// VS
// 匿名類別(全部寫在同一處)
Animal dog = new Animal() {
@Override
void say() { System.out.println("汪!"); }
};
結果相同,但匿名類別更精簡,也不會讓專案變得雜亂!
2. 編譯後匿名類別的名稱
匿名類別在原始碼中沒有名稱,但 Java 編譯器仍需要依某種規則為其命名,以產生 .class 檔。規則如下:
- 匿名類別檔名由宣告它的外部類別名稱開始。
- 外部類別名稱後面會加上美元符號 $。
- 接著是此檔案中匿名類別的序號,從 1 開始。
因此,如果我們的動物範例在檔案 Main.java 中,編譯後會產生三個檔案:
- Main.class
- Main$1.class(對應狗的匿名類別)
- Main$2.class(對應貓的匿名類別)
如果匿名類別宣告在一個方法裡,而該方法本身又位於內部類別中,名稱會更複雜,例如 OuterClass$InnerClass$1.class。
這是編譯器的內部命名慣例,了解即可;在日常開發中很少需要直接關心。重點是——匿名類別依然是完整的類別,只是原始碼中沒有名字而已。
3. 重要特性與限制
匿名類別 是很強大的工具,但也有其規則。
存取變數。 匿名類別可以使用所在方法中的變數,但這些變數必須是 final 或 effectively final(也就是初始化後其值不再變更)。
public void doSomething() {
String greeting = "嗨!"; // 這個變數是 effectively final
class OuterClass {
void greet() {
// 在方法內建立匿名類別
new Object() {
void sayHello() {
System.out.println(greeting); // 這是允許的
// greeting = "再見!"; // 這會導致編譯錯誤!
}
}.sayHello();
}
}
new OuterClass().greet();
}
為什麼?因為匿名類別的生命期可能比方法本身更長;若它能改變該變數,將導致不可預期的問題。
沒有建構子。 由於匿名類別沒有名稱,因此也無法定義建構子。不過可以使用 初始化區塊 在建立物件時執行程式碼:
Animal dog = new Animal() {
// 初始化區塊
{
System.out.println("匿名類別初始化 🐶");
}
@Override
void say() {
System.out.println("汪汪!");
}
};
限制。 匿名類別不能宣告靜態欄位(常數除外)或方法。它們總是作為其他物件的一部分被建立,因此不能標記為 static、public、protected 或 private。
4. 實用細節
何時使用匿名類別
- 你只需要繼承某個類別(或實作介面)一次。
- 實作內容很小——1–2 個方法、不超過幾十行。
- 類別只在單一位置需要,沒必要給它一個名字。
- 你想避免以大量一次性的小類別使套件變得雜亂。
典型情境:
- 事件處理器(GUI、Swing、Android 等)。
- 將回呼(callback)傳入方法。
- 快速實作用於集合排序的比較器。
- 暫時性、改動過的物件(例如用於測試)。
5. 與外部類別的互動
如果匿名類別宣告在外部類別的非靜態方法或區塊內,它可以存取該外部類別的欄位與方法(包含 private!)。
public class Outer {
private String secret = "機密文字";
// 基底類別
class Printer {
public void print() {
System.out.println("一般輸出");
}
}
public void revealSecret() {
Printer p = new Printer() {
@Override
public void print() {
System.out.println("存取私有成員: " + secret);
}
};
p.print();
}
public static void main(String[] args) {
new Outer().revealSecret();
}
}
6. 使用匿名類別時的常見錯誤
錯誤 1:嘗試修改所在方法的變數。
如果你在匿名類別之外宣告了一個變數,並且在匿名類別中使用後又嘗試去修改它——編譯器會報錯。該變數必須是 final 或 effectively final(初始化後不可再變更)。
錯誤 2:匿名類別過於龐大。
如果匿名類別膨脹到數十行且有多個方法——就該把它抽出為具名類別。否則程式碼會變得難以閱讀。
錯誤 3:嘗試使用靜態方法或欄位。
在匿名類別中不能宣告靜態方法或欄位(常數除外)。如果真的需要——請改用一般巢狀類別。
錯誤 4:忽略作用域。
匿名類別只在宣告處可見,且沒有名稱。若需要重複使用——請宣告一般類別。
GO TO FULL VERSION