CodeGym /課程 /JAVA 25 SELF /函式介面:Predicate、Consumer、Supplier、Function

函式介面:Predicate、Consumer、Supplier、Function

JAVA 25 SELF
等級 49 , 課堂 0
開放

1. 深入了解函式介面

函式介面是指其中只有恰好一個抽象(亦即未實作)方法的介面。正因如此,Java 能理解: 「啊,這裡可以放入 lambda!」或方法參照。

為了避免出錯,這類介面通常會加上註解 @FunctionalInterface。它不是強制的,但如果你加入了它,卻不小心又寫了第二個抽象方法,編譯器會立刻警告你。

範例:

@FunctionalInterface
interface MyAction {
    void run();
}
MyAction action = () -> System.out.println("來自 lambda 的問候!");
action.run(); // 將輸出:來自 lambda 的問候!

這有什麼用?

  • 允許以 lambda 表達式與方法參照取代建立匿名類別(更少樣板程式碼)。
  • 讓編譯器明白該介面是為函式式程式設計所設計。

小知識: 在 Java 標準程式庫中已經有數十個這樣的介面——不必重新發明輪子!

2. 標準函式介面概覽

java.util.function 套件中有數十個函式介面。以下介紹四個最常見的(它們的「出場率」在所有 Java 介面中名列前茅)。

介面 接受什麼 回傳什麼 常見用途
Predicate<T>
T
boolean
條件檢查(過濾)
Consumer<T>
T
void
對物件執行動作
Supplier<T>
T
取得/產生物件
Function<T, R>
T
R
將 T 轉換為 R

Predicate<T>

說明: 接受一個 T 型別的物件並回傳 truefalse。典型用法:過濾清單。關鍵方法——test

Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java"));       // false
System.out.println(isLong.test("Functional")); // true

Consumer<T>

說明: 接受一個 T 型別的物件並對其執行動作,不回傳任何結果。關鍵方法——accept

Consumer<String> printer = s -> System.out.println("列印:" + s);
printer.accept("Hello, world!"); // 列印:Hello, world!

Supplier<T>

說明: 不接受參數,回傳 T 型別的物件。可把它視為值的「產生器」。關鍵方法——get

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // 例如,0.1234567

Function<T, R>

說明: 接受 T 型別物件並回傳 R 型別物件。典型用法:資料轉換。關鍵方法——apply

Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4

簡要:UnaryOperator、BinaryOperator、BiFunction

  • UnaryOperator<T> —— 等同於 Function<T, T>:接受與回傳相同型別。
  • BinaryOperator<T> —— 等同於 BiFunction<T, T, T>:接受兩個 T,回傳一個 T。
  • BiFunction<T, U, R> —— 接受兩種不同型別,回傳第三種型別。
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);

3. 使用範例

看看這些介面在實務情境中如何應用,特別是在集合與 Stream API 中。

傳遞給集合與 Stream API 的方法

範例 1:Predicate 與過濾

List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
    .filter(word -> word.length() > 4) // Predicate<String>
    .toList();
System.out.println(longWords); // [stream, lambda]

範例 2:Consumer 與 forEach

words.forEach(word -> System.out.println("單字:" + word)); // Consumer<String>

範例 3:Function 與 map

List<Integer> lengths = words.stream()
    .map(word -> word.length()) // Function<String, Integer>
    .toList();
System.out.println(lengths); // [4, 6, 6, 4]

範例 4:Supplier 與值的產生

Supplier<String> greetingSupplier = () -> "你好,Java!";
System.out.println(greetingSupplier.get()); // 你好,Java!

與匿名類別的比較

以前必須這樣寫:

Predicate<String> isShort = new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.length() < 5;
    }
};

有了 lambda 之後就簡潔多了:

Predicate<String> isShort = s -> s.length() < 5;

4. 實作:為每個介面撰寫 lambda 表達式

我們來實作一個小應用——使用者清單。每個使用者以 User 類別表示:

public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

建立使用者清單:

List<User> users = List.of(
    new User("Anna", 23),
    new User("Boris", 17),
    new User("Vika", 31),
    new User("Gosha", 15)
);

Predicate:篩選成年人

Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
    .filter(isAdult)
    .toList();

System.out.println("成年人:" + adults); // 成年人: [Anna (23), Vika (31)]

Consumer:列印使用者

Consumer<User> printUser = user -> System.out.println("使用者:" + user);
adults.forEach(printUser);

Supplier:產生使用者

Supplier<User> randomUserSupplier = () -> {
    String[] names = {"Dima", "Katya", "Lyosha"};
    int randomAge = 10 + (int)(Math.random() * 30);
    String randomName = names[(int)(Math.random() * names.length)];
    return new User(randomName, randomAge);
};

User randomUser = randomUserSupplier.get();
System.out.println("隨機的使用者:" + randomUser);

Function:取得使用者名稱

Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
    .map(getName)
    .toList();

System.out.println("名字:" + names); // 名字: [Anna, Boris, Vika, Gosha]

5. 實用細節

在 Stream API 中的使用:filter、map、forEach 等

把一切串起來,寫一個轉換鏈:

users.stream()
     .filter(user -> user.getAge() >= 18)         // Predicate<User>
     .map(user -> user.getName().toUpperCase())    // Function<User, String>
     .forEach(name -> System.out.println("成年人:" + name)); // Consumer<String>

結果:

成年人:ANNA
成年人:VIKA

小抄表:該放哪個介面

使用位置 需要哪個介面 使用範例
filter (Stream)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

為什麼要了解函式介面?

  • 它們是 Java 中所有 lambda 表達式的基礎。
  • 有助於撰寫通用、可重用又精簡的程式碼。
  • 讓你更輕鬆地處理集合、串流與非同步工作。

何時該用哪個介面?

  • Predicate —— 當需要檢查條件(過濾、搜尋)。
  • Consumer —— 當需要對物件做些事(輸出、寫入、傳送)。
  • Supplier —— 當需要取得或產生物件(工廠、產生器)。
  • Function —— 當需要把物件從一種型別轉換為另一種型別。

6. 使用函式介面時的常見錯誤

錯誤一:選錯介面。 新手常把 PredicateFunction 搞混——例如嘗試在 Function 回傳 boolean,而不是在 Predicate。記住:Predicate 永遠回傳 booleanFunction 則回傳其他任何型別。

錯誤二:未使用標準介面。 常見做法是自己寫一個像「Checker」並含有 boolean check(T t) 方法的介面,來取代 Predicate。更好的做法是使用標準介面——它們在各處都受到支援,並讓其他程式設計師更容易理解你的程式碼。

錯誤三:Lambda 過於複雜。 如果一個 lambda 變成 10 行的小小說,就該把它抽出成獨立的方法或類別。Lambda 追求的是簡潔與可讀性。

錯誤四:忘了加上註解 @FunctionalInterface 如果你正在撰寫自己的函式介面——別忘了這個註解。它能防止偶發的失誤(例如不小心新增第二個抽象方法)。

錯誤五:在 lambda 內使用可變動狀態。 如果 lambda 會修改外部變數或集合,可能導致難以預料的 bug,尤其在多執行緒情境下。最好避免副作用。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION