CodeGym /課程 /JAVA 25 SELF /將函式作為參數傳遞:範例

將函式作為參數傳遞:範例

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

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 超過一兩行,最好把它抽出去成為變數或方法。否則程式碼會變得難以閱讀,最後只有你看得懂(而且不一定每次都看得懂)。

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