CodeGym /課程 /JAVA 25 SELF /函數組合:compose、andThen

函數組合:compose、andThen

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

1. 函數組合的概念

一點點理論(不會太無聊)

在數學中,函數組合是指一個函數的結果成為另一個函數的輸入。若我們有函數 fg,則組合 g(f(x)) 表示:先把 f 應用到 x,然後把結果交給 g

在程式設計中也是一樣:我們希望用簡單的函數來組裝複雜的轉換,而不是為了所有事情寫一個巨大的函數。這會讓程式碼更靈活、可重用且易讀。

想像一條做點心的生產線:先做麵糰(f),再擠奶油(g),最後灑上裝飾(h)。整個流程就是 h(g(f(ingredients)))

為什麼函數組合很重要?

  • 組合能把程式組成許多小小的「積木」函數。
  • 更容易重用:一個「積木」可放在不同位置,不必重複撰寫。
  • 靈活性:想改某個步驟時,只需替換對應函數——其他不必動。
  • 可讀性與可測性:小函數更容易閱讀、測試與維護。

2. 介面 composeandThenFunction

Function 介面:複習

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    // 用於組合的方法:
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
}
  • compose:會先執行你傳給 compose 的函數,然後才是目前這個函數。
  • andThen:會先執行目前這個函數,接著再執行傳給 andThen 的函數。

視覺化示意

// 假設有兩個函數:
Function<String, Integer> parse = s -> Integer.parseInt(s);
Function<Integer, Integer> square = x -> x * x;

// compose: square.compose(parse) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// andThen: parse.andThen(square) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// 但當型別不同時,順序就很重要!

範例:將字串轉為數字,再平方

import java.util.function.Function;

public class ComposeAndThenDemo {
    public static void main(String[] args) {
        // 函數:把字串轉成數字
        Function<String, Integer> parse = s -> Integer.parseInt(s);

        // 函數:把數字平方
        Function<Integer, Integer> square = x -> x * x;

        // 組合:先解析,再平方
        Function<String, Integer> parseThenSquare = parse.andThen(square);

        System.out.println(parseThenSquare.apply("7")); // 49

        // 如果對調順序呢?
        // square.compose(parse) — 對於這兩個函數來說,結果相同
        Function<String, Integer> squareOfParsed = square.compose(parse);

        System.out.println(squareOfParsed.apply("8")); // 64
    }
}

什麼時候順序會影響結果?

當函數的型別不相容時,順序就變得關鍵。例如:

Function<String, String> addPrefix = s -> "User: " + s;
Function<String, Integer> length = s -> s.length();

Function<String, Integer> composed = addPrefix.andThen(length);
System.out.println(composed.apply("Alice")); // "User: Alice" -> 11

// 但若這樣寫:
// length.andThen(addPrefix) — 編譯錯誤!
// length 回傳 Integer,而 addPrefix 需要 String。

表格:compose 與 andThen 的差異

f
g
f.compose(g)
f.andThen(g)
T → R
V → T
V → R: f(g(x))
T → V: g(f(x))

3. Predicate 與其他介面的組合

Predicate<T>andornegate

函數式介面 Predicate<T> 是一個回傳 boolean 的函數。要組合 Predicate,有以下專用方法:

  • and:邏輯且(&&
  • or:邏輯或(||
  • negate:邏輯非(!

範例:複合的篩選條件

假設我們有一個使用者類別:

public class User {
    String name;
    int age;

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

接著寫幾個不同的 Predicate:

import java.util.function.Predicate;

Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");

// 組合:已成年且名字以 "A" 開頭
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);

// 已成年或名字以 "A" 開頭
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);

// 未成年
Predicate<User> notAdult = isAdult.negate();

現在可以在篩選時使用這些 Predicate,例如配合 Stream API:

import java.util.List;
import java.util.stream.Collectors;

List<User> users = List.of(
    new User("Alice", 20),
    new User("Bob", 17),
    new User("Anna", 15),
    new User("Mike", 22)
);

List<User> filtered = users.stream()
    .filter(adultAndA)
    .collect(Collectors.toList());

// filtered 只會包含 Alice(已成年且名字以「A」開頭)

組合 ConsumerFunctionSupplier

  • Consumer<T>:方法 andThen 可讓兩個操作依序執行。
  • Function<T, R>:前面已介紹其組合方式。
  • Supplier<T>:不能直接組合,但可在其他函數內部使用。

範例:Consumer<T>.andThen

import java.util.function.Consumer;

Consumer<String> print = s -> System.out.println("收到:" + s);
Consumer<String> printUpper = s -> System.out.println("大寫:" + s.toUpperCase());

Consumer<String> combined = print.andThen(printUpper);

combined.accept("hello");
// 會輸出:
// 收到:hello
// 大寫:HELLO

4. 實作練習:轉換與篩選的鏈式組合

練習 1:組成 Function 轉換鏈

假設在應用中,我們把使用者儲存為字串 "姓名,年齡",例如 "Alice,20"。需要:

  • 把字串轉為 User 物件
  • 取得年齡
  • 檢查使用者是否已成年
import java.util.function.Function;
import java.util.function.Predicate;

Function<String, User> stringToUser = str -> {
    String[] parts = str.split(",");
    return new User(parts[0], Integer.parseInt(parts[1]));
};
Function<User, Integer> getAge = user -> user.age;
Predicate<Integer> isAdultAge = age -> age >= 18;

// 組合:字串 -> User -> 年齡 -> Predicate
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);

String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("年齡:" + age); // 19

System.out.println("已成年?" + isAdultAge.test(age)); // true

練習 2:組合多個 Predicate 來進行篩選

我們要選出年齡大於 18 且名字以「A」或「M」開頭的使用者。

Predicate<User> isAdult = user -> user.age > 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
Predicate<User> nameStartsWithM = user -> user.name.startsWith("M");

Predicate<User> filter = isAdult.and(nameStartsWithA.or(nameStartsWithM));

List<User> filtered = users.stream()
    .filter(filter)
    .collect(Collectors.toList());

練習 3:多階段的 Function 轉換

需求:取得字串、去除前後空白、轉為大寫,並加上前綴 "USER: "

Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;

// 串接成一條管線
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);

System.out.println(pipeline.apply("   vasya   ")); // USER: VASYA

5. 函數組合的常見錯誤

錯誤 1:把 compose/andThen 的順序搞反。
新手常分不清先執行哪個、後執行哪個。記住:f.compose(g) —— 先執行 g,再執行 ff.andThen(g) —— 先執行 f,再執行 g

錯誤 2:型別不匹配。
如果一個函數的回傳型別與另一個函數的參數型別不一致——編譯器不會讓你組合。例如,不能這樣寫:Function<Integer, String>.andThen(Function<Double, Boolean>)

錯誤 3:鏈太長、太複雜。
有時想把所有商業邏輯都塞進一條鏈,結果變成「麵條式」。請把它拆成小函數並賦予清楚的名稱。

錯誤 4:函數內含副作用。
最好讓函數與 Predicate 保持「純粹」(無副作用),否則組合起來既危險又難以預測。

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