1. 傳遞行為而非資料
為什麼需要把函式當成參數傳入?
想像一下:你有一個用來排序清單的方法。但它要如何知道要依什麼排序?按字母?長度?生日?你可以為每種情況寫一個獨立的方法,但很快就會變成可怕的複製貼上地獄。
取而代之,Java 允許你傳遞 行為(也就是函式或 lambda),來決定如何排序、篩選、轉換等等。這讓程式碼更靈活且可重用。
範例:使用比較器排序
List<String> names = List.of("Anna", "Boris", "Vika");
// 傳遞行為:如何比較元素(依字串長度)
names.stream()
.sorted((a, b) -> a.length() - b.length())
.forEach(System.out::println);
範例:用 Predicate 篩選
List<String> names = List.of("Anna", "Boris", "Vika");
// 傳遞行為:保留哪些(名字長度大於 4)
names.stream()
.filter(name -> name.length() > 4)
.forEach(System.out::println);
在這兩種情況下,你並沒有把邏輯硬寫死在方法裡,而是交給它一小段行為,讓它套用到每個元素上。
好處:更少重複,更高彈性
不再為不同邏輯寫一堆內容相似的方法,而是寫一個通用方法,透過函式參數告訴它「要做什麼」。這能省時間、減少錯誤,並讓程式碼更容易測試。
2. 傳遞函式的語法
以 lambda 表達式作為引數
最常見的方式——在呼叫方法的地方直接寫出帶有箭頭 -> 的 lambda:
list.forEach(item -> System.out.println(item));
或更精簡一些,如果已有合適的方法——使用方法參照 (::):
list.forEach(System.out::println);
方法參照(method references)
如果你已經有合適的方法,可以用參照 :: 來傳遞它:
// 一般方法
public static boolean isLongName(String name) {
return name.length() > 4;
}
// 傳遞方法參照
names.stream()
.filter(MyClass::isLongName)
.forEach(System.out::println);
只要方法簽章與函式式介面所期望的相符(Predicate<T>、Function<T, R>、Comparator<T> 等),它就能運作。
3. 標準程式庫中的範例
Collections.sort 與 Comparator
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
// 依名稱長度排序
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]
Stream.filter 與 Predicate
List<String> names = List.of("Anna", "Boris", "Vika");
// 只保留以 'V' 開頭的名字
names.stream()
.filter(name -> name.startsWith("V"))
.forEach(System.out::println); // Vika
Stream.map 與 Function
List<String> names = List.of("Anna", "Boris", "Vika");
// 將名字轉為大寫
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Optional.ifPresent 與 Consumer
Optional<String> opt = Optional.of("你好!");
// 若有值則列印
opt.ifPresent(s -> System.out.println("在字串中:" + s));
4. 實作:撰寫帶有函式參數的方法
該把知識用起來了!讓我們撰寫幾個方法,接收函式作為參數並在其中使用。
範例 1:處理清單元素的方法 (forEach 的自製版)
假設我們有一個使用者清單(User)。我們想對每位使用者執行某個動作——例如輸出姓名、寄送 e-mail、發放獎勵等。不要把行為寫死,改為把它作為參數傳入——Consumer<User>!
import java.util.List;
import java.util.function.Consumer;
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class UserProcessor {
// 方法,接收 Consumer<User>
public static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) {
action.accept(user);
}
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 30),
new User("Vika", 22)
);
// 輸出所有使用者的名字
processUsers(users, user -> System.out.println("姓名: " + user.name));
// 發放獎勵(此處僅示範輸出)
processUsers(users, user -> System.out.println(user.name + " 獲得獎金!"));
}
}
範例 2:篩選元素的方法 (Predicate)
我們來寫一個方法,只回傳符合條件的使用者(例如僅成年人):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;
public class UserFilter {
public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (condition.test(user)) {
result.add(user);
}
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 僅篩選成年人
List<User> adults = filterUsers(users, user -> user.age >= 18);
adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
}
}
範例 3:轉換器方法 (Function)
接著撰寫一個方法,把使用者清單轉成他們的名字清單(或任何其他轉換):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
public class UserMapper {
public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) {
result.add(mapper.apply(user));
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 取得名字清單
List<String> names = mapUsers(users, user -> user.name);
System.out.println(names); // [Anna, Boris, Vika]
// 取得年齡清單
List<Integer> ages = mapUsers(users, user -> user.age);
System.out.println(ages); // [25, 17, 22]
}
}
範例 4:產生器方法 (Supplier)
有時需要「按需」取得某個值——例如產生亂數、建立物件、取得目前時間。這時就適合使用介面 Supplier<T>。
import java.util.function.Supplier;
public class ValueGenerator {
public static int getValue(Supplier<Integer> supplier) {
return supplier.get();
}
public static void main(String[] args) {
// 取得隨機數
int random = getValue(() -> (int)(Math.random() * 100));
System.out.println("隨機數: " + random);
// 取得目前的毫秒時間
long time = getValue(System::currentTimeMillis);
System.out.println("時間: " + time);
}
}
5. 整合應用:把範例串起來
想像我們在開發一個「使用者清單」的小應用。我們已經會:
- 依條件篩選使用者(例如僅成年人);
- 將使用者轉換成其他資料(姓名、e-mail、年齡);
- 對每位使用者執行動作(列印、發放獎勵);
- 按需產生值(例如建立新使用者)。
現在藉由這些方法,我們可以建立彈性的資料處理流程,不必為每個新需求重寫程式碼。
import java.util.*;
import java.util.function.*;
public class UserApp {
static class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// 處理使用者的通用方法
static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) action.accept(user);
}
// 通用篩選器
static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) if (condition.test(user)) result.add(user);
return result;
}
// 通用轉換器
static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) result.add(mapper.apply(user));
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 1. 輸出所有使用者
processUsers(users, user -> System.out.println("使用者: " + user));
// 2. 僅找出成年人
List<User> adults = filterUsers(users, user -> user.age >= 18);
System.out.println("成年人: " + adults);
// 3. 取得成年人的名字
List<String> adultNames = mapUsers(adults, user -> user.name);
System.out.println("成年人的名字: " + adultNames);
// 4. 為成年人發放獎勵
processUsers(adults, user -> System.out.println(user.name + " 獲得獎金!"));
}
}
為什麼這比「傳統」作法更好?
這種作法帶來高度彈性:同一組方法可以在完全不同的情境下重用,只要傳入不同的函式即可。程式碼不會膨脹成無止境的「這樣篩選」「那樣列印」變體——我們擁有通用的工具,處處適用。因此程式碼更精簡、易懂,也更不容易出錯。這種風格也與現代的 Stream API 完美契合,所以轉向串流處理時無須重新學習——觀念完全一致。
6. 將函式作為參數時的常見錯誤
錯誤 №1:簽章與預期的介面不相符。
若方法接收 Predicate<User>,但你傳入的 lambda 回傳的不是 boolean,而是例如 String,編譯器會立刻報錯,無法編譯。請確認回傳值與參數符合預期。
錯誤 №2:lambda 使用了可能會改變的變數。
在 Java 中,lambda 表達式只能使用外部環境中 final 或 effectively final 的變數。若你嘗試在 lambda 內修改這樣的變數,會出現編譯錯誤。
錯誤 №3:把介面搞混。
有時你想傳入例如 Consumer<T>,卻不小心寫成會回傳某些東西的函式(例如 Function<T, R>)。請確認你的 lambda 回傳的內容正確(或對於 Consumer 而言不回傳任何東西)。
錯誤 №4:把過於複雜的 lambda 直接寫在參數裡。
如果一段 lambda 超過一兩行,最好把它抽出去成為變數或方法。否則程式碼會變得難以閱讀,最後只有你看得懂(而且不一定每次都看得懂)。
GO TO FULL VERSION