CodeGym /課程 /JAVA 25 SELF /匿名類別:與 Lambda 的差異、範例

匿名類別:與 Lambda 的差異、範例

JAVA 25 SELF
等級 48 , 課堂 4
開放

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 的方法(toStringequals 等)。
  • 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 的方法;
  • 實作「此時此地」即可,且邏輯簡單。

以下情況必須用匿名類別:

  • 要實作具有多個方法的介面或抽象類別;
  • 需要宣告欄位或額外方法;
  • 需要覆寫 toStringequalshashCode
  • 需要存取父類別的受保護成員。

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 主體長度超過 35 行,易讀性會下降。最好把程式碼抽出成獨立方法,或在需要狀態/多個方法時改用匿名類別。

1
問卷/小測驗
Lambda 表達式,等級 48,課堂 4
未開放
Lambda 表達式
Lambda 表達式
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION