最近在重构一个统计模块时,我遇到了一个很有意思的代码片段。这个简单的求和方法让我对Java函数式编程有了更深的理解。先看这段代码:
java复制private Double sumPointsValue(
List<ConsumptionPointDto> points,
Function<ConsumptionPointDto, Double> extractor) {
return points.stream()
.map(extractor)
.filter(Objects::nonNull)
.reduce(0.0, Double::sum);
}
调用方式是这样的:
java复制Double totalConsumption = sumPointsValue(points, ConsumptionPointDto::getConsumptionLiter);
Double totalRefill = sumPointsValue(points, ConsumptionPointDto::getRefillLiter);
初看这段代码时,我产生了几个疑问:
如果不使用Function,我们可能会写出这样的代码:
java复制private Double calcTotalConsumption(List<ConsumptionPointDto> points) {
return points.stream()
.map(ConsumptionPointDto::getConsumptionLiter)
.filter(Objects::nonNull)
.reduce(0.0, Double::sum);
}
private Double calcTotalRefill(List<ConsumptionPointDto> points) {
return points.stream()
.map(ConsumptionPointDto::getRefillLiter)
.filter(Objects::nonNull)
.reduce(0.0, Double::sum);
}
这种写法存在明显的代码重复问题。三个方法几乎完全一样,唯一的区别就是提取的字段不同。这正是函数式接口要解决的问题——将变化的部分参数化。
函数式接口(Functional Interface)是Java 8引入的重要概念,它是指仅包含一个抽象方法的接口。这种设计允许我们把"行为"当作参数传递,而不仅仅是数据。
Java中常见的函数式接口有:
函数式接口的核心价值在于"行为参数化"。它让我们能够:
在前面的例子中,求和流程是固定的,而变化的是"如何从对象中提取数值"。通过Function参数,我们完美地将这两者解耦。
Function<T, R>表示接收一个T类型参数,返回R类型结果的函数。它的抽象方法是:
java复制R apply(T t);
在我们的求和方法中,Function的作用是从ConsumptionPointDto中提取Double值:
java复制Function<ConsumptionPointDto, Double> extractor =
point -> point.getConsumptionLiter();
你可能注意到我们使用了方法引用(::)的写法:
java复制sumPointsValue(points, ConsumptionPointDto::getConsumptionLiter);
这实际上是以下lambda表达式的简写:
java复制sumPointsValue(points, point -> point.getConsumptionLiter());
Java编译器会自动将方法引用转换为对应的Function实现。这种语法糖让代码更加简洁易读。
java复制Function<User, UserDTO> toDTO = user -> new UserDTO(user.getName(), user.getAge());
java复制Function<Order, Double> getAmount = Order::getTotalAmount;
java复制Function<String, String> pipeline = ((Function<String, String>)String::toLowerCase)
.andThen(String::trim)
.andThen(s -> s.replace(" ", "-"));
Consumer
java复制void accept(T t);
典型使用场景包括:
java复制// 打印所有用户名
Consumer<User> printName = user -> System.out.println(user.getName());
users.forEach(printName);
// 批量修改状态
Consumer<Order> markAsPaid = order -> order.setStatus(OrderStatus.PAID);
orders.forEach(markAsPaid);
Consumer支持andThen方法实现链式调用:
java复制Consumer<String> logAndStore = ((Consumer<String>)System.out::println)
.andThen(this::saveToDatabase);
Supplier
java复制T get();
主要用途包括:
java复制// 提供当前时间
Supplier<LocalDateTime> timeSupplier = LocalDateTime::now;
// 懒加载配置
Supplier<Config> configSupplier = () -> loadConfigFromRemote();
// 对象工厂
Supplier<List<String>> listSupplier = ArrayList::new;
Supplier常用于Optional的orElseGet方法:
java复制String username = Optional.ofNullable(inputName)
.orElseGet(() -> System.getProperty("user.name"));
Predicate
java复制boolean test(T t);
主要应用场景:
java复制// 过滤空值
Predicate<String> notEmpty = s -> s != null && !s.isEmpty();
// 校验用户
Predicate<User> isAdult = user -> user.getAge() >= 18;
// 组合条件
Predicate<User> isValid = isAdult.and(user -> user.isActive());
Predicate支持and、or、negate等逻辑组合:
java复制Predicate<String> complexCondition = ((Predicate<String>)s -> s.startsWith("A"))
.and(s -> s.length() > 5)
.or(s -> s.contains("special"));
| 接口 | 输入 | 输出 | 典型用途 | 记忆口诀 |
|---|---|---|---|---|
| Function<T,R> | T | R | 转换、提取 | 有进有出 |
| Consumer |
T | void | 消费、处理 | 有进无出 |
| Supplier |
无 | T | 提供、生成 | 无进有出 |
| Predicate |
T | boolean | 判断、过滤 | 有进有判 |
虽然函数式接口很强大,但也要避免以下滥用情况:
给函数式参数起有意义的名字:
java复制// 好的命名
Function<User, String> nameExtractor = User::getName;
Predicate<Order> isExpensive = order -> order.getTotal() > 1000;
// 不好的命名
Function<User, String> f1 = User::getName;
Predicate<Order> p = order -> order.getTotal() > 1000;
利用函数组合代替复杂的继承体系:
java复制Function<User, String> pipeline = ((Function<User, String>)User::getName)
.andThen(String::toUpperCase)
.andThen(name -> "Mr. " + name);
lambda中处理检查异常比较麻烦,可以考虑:
java复制// 包装运行时异常
Function<String, Integer> safeParser = s -> {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new RuntimeException(e);
}
};
// 或者使用自定义函数式接口
@FunctionalInterface
interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
lambda调试不太直观,可以:
现在再看这个求和方法,就能完全理解它的精妙之处了:
java复制private Double sumPointsValue(
List<ConsumptionPointDto> points,
Function<ConsumptionPointDto, Double> extractor) {
return points.stream()
.map(extractor)
.filter(Objects::nonNull)
.reduce(0.0, Double::sum);
}
它成功地将不变的求和流程与变化的取值逻辑分离,实现了:
这种设计模式在实际开发中非常实用,特别是在处理集合操作、数据转换等场景时。理解并熟练运用这些函数式接口,能让我们的Java代码更加简洁、灵活和富有表达力。