1. 更深入認識匿名類別
匿名類別是在使用位置直接建立的無名子類別或介面實作。在 Lambda 出現之前(Java 8),這是最方便的一次性實作介面或抽象類別的方法。
經典範例:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("來自匿名類別的問候!");
}
};
r.run();
在這裡,我們宣告並立刻實作了介面 Runnable——不需要額外的檔案與類別名稱。這樣的實作常用於事件處理器、比較器、執行緒,以及其他需要快速提供一段行為的情境。
如果把 Lambda 看作「就地的表達式」,那麼匿名類別就像「沒有名字的小演員」,演完客串就退場。
2. 與 Lambda 表達式的比較
語法
匿名類別:
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Lambda 表達式:
Comparator<String> comp = (a, b) -> a.length() - b.length();
差異很明顯:Lambda 更精簡——若行為很單純,就不必明確寫出型別、方法名稱與多餘的大括號。
功能性
- 匿名類別——是完整的物件。可以宣告欄位、額外方法,並覆寫 Object 的方法(toString、equals 等)。
- Lambda 表達式——是函式式介面的單一抽象方法的實作。內部不能宣告自己的欄位或額外方法。
何時選哪一個?
- Lambda——當你只需要為函式式介面簡短地實作一個方法。
- 匿名類別——當你需要:
- 實作多個方法(例如抽象類別);
- 宣告欄位以保存狀態;
- 覆寫 Object 的方法(例如 toString);
- 利用繼承/存取特性(例如存取父類別的受保護成員)。
3. 作用域與關鍵字 this
這裡有個常見陷阱:
- 在匿名類別中,this 指向匿名類別本身的實例;
- 在Lambda 表達式中,this 指向宣告該 Lambda 的外部類別。
範例:比較其行為
public class Outer {
String name = "外部類別";
void test() {
Runnable anon = new Runnable() {
String name = "匿名類別";
@Override
public void run() {
System.out.println(this.name); // "匿名類別"
}
};
Runnable lambda = () -> System.out.println(this.name); // "外部類別"
anon.run();
lambda.run();
}
}
輸出:
匿名類別
外部類別
在匿名類別中,this 指向匿名類別本身(取用它的 name 欄位)。在 Lambda 中,this 指的是 Outer。
4. 何時使用匿名類別?
當需要實作多於一個方法時
Lambda 僅適用於函式式介面(恰好一個抽象方法)。若介面/抽象類別要求實作多個方法——就需要匿名類別。
abstract class Animal {
abstract void say();
abstract void jump();
}
Animal cat = new Animal() {
@Override
void say() {
System.out.println("喵!");
}
@Override
void jump() {
System.out.println("跳!");
}
};
當需要保存狀態(欄位)
Runnable r = new Runnable() {
int counter = 0;
@Override
public void run() {
counter++;
System.out.println("已呼叫 " + counter + " 次");
}
};
r.run(); // 已呼叫 1 次
r.run(); // 已呼叫 2 次
當需要覆寫 Object 的方法
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
@Override
public String toString() {
return "依字串長度的比較器";
}
};
System.out.println(comp); // 依字串長度的比較器
5. 範例:Comparator 與 Runnable —— Lambda vs 匿名類別
依字串長度排序
匿名類別:
List<String> words = Arrays.asList("貓", "大象", "老鼠", "老虎");
words.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
System.out.println(words);
Lambda 表達式:
List<String> words = Arrays.asList("貓", "大象", "老鼠", "老虎");
words.sort((a, b) -> a.length() - b.length());
System.out.println(words);
結果相同,但使用 Lambda 的程式碼更短、也更易讀。
Runnable:啟動執行緒
匿名類別:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("透過匿名類別的執行緒");
}
});
t1.start();
Lambda 表達式:
Thread t2 = new Thread(() -> System.out.println("透過 Lambda 的執行緒"));
t2.start();
帶有欄位的匿名類別
Runnable r = new Runnable() {
int count = 0;
@Override
public void run() {
count++;
System.out.println("已呼叫 " + count + " 次");
}
};
r.run(); // 已呼叫 1 次
r.run(); // 已呼叫 2 次
在 Lambda 裡不能這樣——無法宣告欄位。
6. 特點:作用域、變數與 final
在匿名類別與 Lambda 表達式中,外部方法的區域變數只有在它們是 final 或「實質上為 final」(初始化後不再變更)時才能使用。不過在命名上有個差異:
- 在匿名類別中可以宣告與外層同名的變數(「遮蔽」);
- 在 Lambda 中不行:名稱不能與外層變數衝突。
範例:
int x = 10;
Runnable r = new Runnable() {
@Override
public void run() {
int x = 20; // OK:遮蔽外部變數
System.out.println(x); // 20
}
};
r.run();
Runnable l = () -> {
// int x = 30; // 編譯錯誤:變數已經定義
System.out.println(x); // 10
};
l.run();
7. 何時 Lambda 較佳,何時匿名類別不可或缺?
以下情況選 Lambda:
- 需要為函式式介面實作一個簡短的函式;
- 不需要保存狀態;
- 不需要覆寫 Object 的方法;
- 實作「此時此地」即可,且邏輯簡單。
以下情況必須用匿名類別:
- 要實作具有多個方法的介面或抽象類別;
- 需要宣告欄位或額外方法;
- 需要覆寫 toString、equals、hashCode;
- 需要存取父類別的受保護成員。
8. 實作:透過範例比較
練習 1:用 Predicate 過濾清單
匿名類別:
List<String> animals = Arrays.asList("貓", "大象", "老鼠", "老虎");
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 4;
}
});
System.out.println(animals); // [大象, 老鼠, 老虎]
Lambda 表達式:
List<String> animals = Arrays.asList("貓", "大象", "老鼠", "老虎");
animals.removeIf(s -> s.length() < 4);
System.out.println(animals); // [大象, 老鼠, 老虎]
練習 2:比較 this 的作用域
public class Demo {
String name = "Demo";
void check() {
Runnable anon = new Runnable() {
String name = "Anon";
@Override
public void run() {
System.out.println(this.name); // "Anon"
}
};
Runnable lambda = () -> System.out.println(this.name); // "Demo"
anon.run();
lambda.run();
}
public static void main(String[] args) {
new Demo().check();
}
}
9. 使用匿名類別與 Lambda 的常見錯誤
錯誤 #1:期望 Lambda 能實作多個方法。 Lambda 僅適用於函式式介面(只有一個抽象方法)。如果方法不只一個——請使用匿名類別。
錯誤 #2:混淆 this 的作用域。 在 Lambda 中,this 是外部類別;在匿名類別中,this 是匿名類別本身。這樣很容易取得「不是你要的」欄位與值。
錯誤 #3:嘗試在 Lambda 宣告欄位。 在 Lambda 中不能宣告自己的欄位——只能使用外部環境的變數(final/「實質上為 final」)。若需要狀態,請使用匿名類別。
錯誤 #4:變數遮蔽。 在匿名類別中可以宣告與外部同名的區域變數——這就是遮蔽。在 Lambda 中不行:編譯器會回報錯誤。
錯誤 #5:在 Lambda 中放入過於複雜的邏輯。 如果 Lambda 主體長度超過 3–5 行,易讀性會下降。最好把程式碼抽出成獨立方法,或在需要狀態/多個方法時改用匿名類別。
GO TO FULL VERSION