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 介面中名列前茅)。
| 介面 | 接受什麼 | 回傳什麼 | 常見用途 |
|---|---|---|---|
|
|
|
條件檢查(過濾) |
|
|
|
對物件執行動作 |
|
無 | |
取得/產生物件 |
|
|
|
將 T 轉換為 R |
Predicate<T>
說明: 接受一個 T 型別的物件並回傳 true 或 false。典型用法:過濾清單。關鍵方法——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) | |
|
| map (Stream) | |
|
| forEach (Stream, List) | |
|
| generate (Stream) | |
|
為什麼要了解函式介面?
- 它們是 Java 中所有 lambda 表達式的基礎。
- 有助於撰寫通用、可重用又精簡的程式碼。
- 讓你更輕鬆地處理集合、串流與非同步工作。
何時該用哪個介面?
- Predicate —— 當需要檢查條件(過濾、搜尋)。
- Consumer —— 當需要對物件做些事(輸出、寫入、傳送)。
- Supplier —— 當需要取得或產生物件(工廠、產生器)。
- Function —— 當需要把物件從一種型別轉換為另一種型別。
6. 使用函式介面時的常見錯誤
錯誤一:選錯介面。 新手常把 Predicate 和 Function 搞混——例如嘗試在 Function 回傳 boolean,而不是在 Predicate。記住:Predicate 永遠回傳 boolean,Function 則回傳其他任何型別。
錯誤二:未使用標準介面。 常見做法是自己寫一個像「Checker」並含有 boolean check(T t) 方法的介面,來取代 Predicate。更好的做法是使用標準介面——它們在各處都受到支援,並讓其他程式設計師更容易理解你的程式碼。
錯誤三:Lambda 過於複雜。 如果一個 lambda 變成 10 行的小小說,就該把它抽出成獨立的方法或類別。Lambda 追求的是簡潔與可讀性。
錯誤四:忘了加上註解 @FunctionalInterface。 如果你正在撰寫自己的函式介面——別忘了這個註解。它能防止偶發的失誤(例如不小心新增第二個抽象方法)。
錯誤五:在 lambda 內使用可變動狀態。 如果 lambda 會修改外部變數或集合,可能導致難以預料的 bug,尤其在多執行緒情境下。最好避免副作用。
GO TO FULL VERSION