CodeGym /课程 /JAVA 25 SELF /函数组合:compose, andThen

函数组合:compose, andThen

JAVA 25 SELF
第 49 级 , 课程 1
可用

1. 函数组合的概念

一点点理论(但不枯燥)

在数学中,函数的组合是指一个函数的结果作为另一个函数的输入。如果有函数 fg,那么组合 g(f(x)) 表示:先对 x 应用 f,然后把结果传给 g

在编程中也是同样的思想:我们希望用简单函数拼装出复杂的转换,而不是为所有事情写一个巨大的函数。这样代码更灵活、可复用、也更易读。

想象一个点心生产线:先做面团(f),再加奶油(g),最后撒上装饰(h)。整个过程就是 h(g(f(原料)))

为什么组合很重要?

  • 组合让你能用“小积木”函数构建程序。
  • 更易复用:同一个“小积木”可以在不同地方重复使用而不需复制粘贴。
  • 灵活性:要修改某个阶段时,只需替换相应函数——其余部分不动。
  • 可读性和可测试性:小函数更容易阅读、验证和维护。

2. 接口 composeandThen 中的 Function 方法

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 的函数。用于组合谓词有以下方法:

  • and:逻辑与(&&
  • or:逻辑或(||
  • negate:逻辑非(!

示例:复杂的过滤条件

假设有一个用户类:

public class User {
    String name;
    int age;

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

接下来写几个谓词:

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();

现在可以在过滤中使用这些谓词,例如借助 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

假设在我们的应用中,用户以字符串 "Name,Age" 存储,例如 "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 -> 年龄 -> 谓词
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("   bob   ")); // 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:函数中的副作用。
最好让函数和谓词保持“纯”(无副作用),否则组合会变得危险且不可预测。

1
任务
JAVA 25 SELF, 第 49 级, 课程 1
已锁定
秘密社群的登录名标准化 🕵️‍♂️
秘密社群的登录名标准化 🕵️‍♂️
1
任务
JAVA 25 SELF, 第 49 级, 课程 1
已锁定
魔法学校的数字魔法转换 🧙‍♀️
魔法学校的数字魔法转换 🧙‍♀️
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION