1. 函数组合的概念
一点点理论(但不枯燥)
在数学中,函数的组合是指一个函数的结果作为另一个函数的输入。如果有函数 f 和 g,那么组合 g(f(x)) 表示:先对 x 应用 f,然后把结果传给 g。
在编程中也是同样的思想:我们希望用简单函数拼装出复杂的转换,而不是为所有事情写一个巨大的函数。这样代码更灵活、可复用、也更易读。
想象一个点心生产线:先做面团(f),再加奶油(g),最后撒上装饰(h)。整个过程就是 h(g(f(原料)))。
为什么组合很重要?
- 组合让你能用“小积木”函数构建程序。
- 更易复用:同一个“小积木”可以在不同地方重复使用而不需复制粘贴。
- 灵活性:要修改某个阶段时,只需替换相应函数——其余部分不动。
- 可读性和可测试性:小函数更容易阅读、验证和维护。
2. 接口 compose 和 andThen 中的 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 的差异
|
|
|
|
|---|---|---|---|
|
|
|
|
3. 组合 Predicate 及其他接口
Predicate<T>:and、or、negate
函数式接口 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" 开头)
组合 Consumer、Function、Supplier
- 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,后 f;f.andThen(g) —— 先 f,后 g。
错误 2:类型不匹配。
如果一个函数的结果类型与另一个函数的参数类型不一致——编译器就不会让你组合。例如,不能写 Function<Integer, String>.andThen(Function<Double, Boolean>)。
错误 3:链条过长过复杂。
有时会把全部业务逻辑塞进一条链,结果变成“面条式”代码。请拆分为小函数,并赋予清晰的名字。
错误 4:函数中的副作用。
最好让函数和谓词保持“纯”(无副作用),否则组合会变得危险且不可预测。
GO TO FULL VERSION