Java 函数式接口:从无参任务到自定义多参数查询
文章目录
- [Java 函数式接口:从无参任务到自定义多参数查询](#Java 函数式接口:从无参任务到自定义多参数查询)
-
- 读者问题与范围
- 核心判断与证据边界
- 先按输入输出选接口,再按业务语义细分
- 三个参数与自定义业务参数怎么封装
- BiFunction:两个执行参数的最小抽象
-
- [BiFunction 的类型契约](#BiFunction 的类型契约)
- [用 BiFunction 抽离机构路由](#用 BiFunction 抽离机构路由)
- [为什么三个参数的方法可以返回 BiFunction](#为什么三个参数的方法可以返回 BiFunction)
- [四参数函数:手写 QuadFunction](#四参数函数:手写 QuadFunction)
-
- [为什么 Java 没有标准 QuadFunction](#为什么 Java 没有标准 QuadFunction)
- [定义 QuadFunction](#定义 QuadFunction)
- 四参数查询示例
- [让 Spring Util 实现四参数查询](#让 Spring Util 实现四参数查询)
- 参数对象:四参数接口的替代方案
- 失败路径与边界
- 验证与可复现步骤
- 取舍与落地建议
内容摘要:如何根据参数数量、返回值和业务语义选择 Java 函数式接口?本文覆盖无参任务、无参取值、一参转换与判断、双参处理、三参和四参自定义接口,结合可运行示例解释 Lambda、参数捕获和方法引用。选择接口应先看执行时的输入输出,再看业务语义;固定参数可提前绑定,复杂查询条件应封装成对象。文章保留机构分页案例,并说明异常、空值和数据源边界。
读者问题与范围
在业务代码中,经常需要根据机构、渠道或数据源选择不同的查询方法。原始写法通常把大量 if-else 放进分页循环:
java
while (true) {
if (orgId.equals(1L)) {
list = kkdUtil.query(maxId, pageSize, orgId);
} else if (orgId.equals(2L)) {
list = wyUtil.query(maxId, pageSize, orgId);
}
// 保存、推进游标、记录日志
}
这种写法的问题不在于 if-else 本身,而在于机构路由和分页处理耦合在一起。机构只需要判断一次,却被重复判断了每一页。
本文范围包括:
- 零参、一参、双参标准接口,以及三参和四参自定义接口的使用;
- 通过 Lambda 捕获固定参数;
- 手写四参数函数式接口;
- Lambda、方法引用、查询对象三种写法的边界;
- 分页查询中函数式方法的异常、空值和游标约束。
本文不讨论异步函数、响应式流、函数组合框架,也不把函数式接口当作替代所有面向对象设计的工具。
核心判断与证据边界
本文的核心判断是:
函数接口的参数数量应该由"执行时需要变化的参数"决定,而不是由底层方法总参数个数机械决定。
例如底层方法有三个参数:
java
query(maxId, pageSize, orgId)
如果 orgId 在选择查询方法时就已经确定,那么执行阶段只剩 maxId 和 pageSize,此时 BiFunction<Long, Integer, Result> 是合适的。
如果四个参数都要在执行阶段传入,BiFunction 就不够,应使用自定义四参数接口,或者把参数封装为一个请求对象。
新增的独立完整示例在 Java 17.0.13 中实际编译和运行;原有机构分页和 Spring 代码仍为未执行的示意代码。验证只覆盖本地函数调用,不覆盖真实数据库、事务和数据源配置。
先按输入输出选接口,再按业务语义细分
"函数式方法"通常指把行为封装为函数式接口实例,再用 Lambda 或方法引用实现它。Java 没有独立的函数类型关键字;函数式接口是 Lambda 的目标类型。接口可以有默认方法和静态方法,关键是满足单一抽象方法契约;对应 Object 的公共方法声明不计入这一契约。
零到多个参数的选择表
| 执行参数 | 返回值 | 首选接口 | 执行方法 | 典型场景 |
|---|---|---|---|---|
| 0 个 | 无,void | Runnable |
run() |
任务回调、固定动作 |
| 0 个 | 有,R | Supplier<R> |
get() |
延迟生成配置、默认对象、查询结果 |
| 0 个 | 有,V,可抛受检异常 | Callable<V> |
call() |
向 ExecutorService 提交有结果任务 |
| 1 个 | 无 | Consumer<T> |
accept(T) |
保存对象、打印、逐项处理 |
| 1 个 | 有,R | Function<T, R> |
apply(T) |
DTO 转换、按 ID 查询 |
| 1 个 | boolean | Predicate<T> |
test(T) |
过滤、资格判断 |
| 1 个 | 与输入同类型 | UnaryOperator<T> |
apply(T) |
字符串规范化、同类型变换 |
| 2 个 | 无 | BiConsumer<T, U> |
accept(T, U) |
更新键值、携带上下文保存 |
| 2 个 | 有,R | BiFunction<T, U, R> |
apply(T, U) |
分页查询、两种输入合成结果 |
| 2 个 | boolean | BiPredicate<T, U> |
test(T, U) |
两对象比较、权限匹配 |
| 2 个 | 输入输出同类型 | BinaryOperator<T> |
apply(T, T) |
合并、求和、归约 |
| 3 个 | 有,R | 自定义 TriFunction<A, B, C, R> |
apply(A, B, C) |
三个独立条件查询 |
| 3 个 | 无 | 自定义 TriConsumer<A, B, C> |
accept(A, B, C) |
携带三个字段执行动作 |
| 4 个 | 有,R | 自定义 QuadFunction<A, B, C, D, R> |
apply(A, B, C, D) |
稳定的四参数查询 |
| 4 个 | 无 | 自定义 QuadConsumer<A, B, C, D> |
accept(A, B, C, D) |
携带四个字段执行动作 |
| 多个业务字段 | 有或无 | Function<Request, R> 或 Consumer<Request> |
apply 或 accept |
条件扩展、业务命令封装 |
Runnable 在 java.lang 中;Callable 在 java.util.concurrent 中;其余标准接口在 java.util.function 中。TriFunction、TriConsumer、QuadFunction 是本文自定义接口,不属于 JDK 标准接口。
判断、同类型转换优先用 Predicate 和 Operator,是因为名称直接表达意图;虽然 Function<T, Boolean> 也能表达判断,但过滤 API 通常直接要求 Predicate。没有返回值时使用 Consumer,而不是为了套用 Function<T, Void> 每次 return null。
场景一:没有参数,没有返回值
java
Runnable task = () -> System.out.println("执行固定汇总动作");
task.run();
输入为零,输出是控制台副作用,返回类型是 void。适合把固定动作交给回调或任务调度组件。直接 run() 在当前线程同步执行,不会自动启动线程;定义 Lambda 本身也不会执行动作。
场景二:没有参数,有返回值
java
Supplier<String> supplier = () -> "默认机构";
String name = supplier.get();
输入为零,输出为 String。适合延迟构造默认值、对象工厂和无参查询。每次 get() 都调用函数体,Supplier 不保证缓存、单例、新对象或无副作用。若函数体查询数据库,多次 get() 就可能多次访问数据库。
需要允许受检异常或提交任务并取得 Future 时,可选择 Callable:
java
Callable<String> task = () -> "任务结果";
String result = task.call(); // 所在方法需声明或处理 Exception
直接 call() 也在当前线程执行;异步执行来自 ExecutorService.submit(task),不来自 Callable 接口自身。
场景三:一个参数,用返回语义区分接口
java
Consumer<String> printer = value -> System.out.println(value);
printer.accept("商品A");
Function<String, Integer> length = String::length;
int size = length.apply("Java");
Predicate<String> nonBlank = value -> !value.isBlank();
boolean valid = nonBlank.test("Java");
UnaryOperator<String> normalize = String::trim;
String normalized = normalize.apply(" Java ");
四者都接收一个 String,但分别承担动作、转换、判断和同类型变换。示例要求参数非 null,否则方法调用可能抛 NullPointerException;是否允许空值必须由业务契约决定,接口不会自动校验。
场景四:两个参数,仍然先判断结果含义
java
BiConsumer<String, Integer> report = (name, count) ->
System.out.println(name + ":" + count);
report.accept("商品A", 3);
BiFunction<String, Integer, String> format = (name, count) ->
name + ":" + count;
String text = format.apply("商品A", 3);
BiPredicate<String, String> same = String::equals;
boolean matched = same.test("A", "A");
BinaryOperator<Integer> sum = Integer::sum;
int total = sum.apply(2, 3);
report 通过副作用输出,format 返回文本,same 返回判断结果,sum 合并同类型值。金额运算不要直接复用 Integer 示例,应根据单位与精度采用明确的金额类型。大量基本类型运算可选 IntSupplier、IntConsumer、IntPredicate、IntUnaryOperator、IntBinaryOperator、ToIntFunction 等专用接口,减少装箱;泛型参数不能直接写 int。
三个参数与自定义业务参数怎么封装
三个参数,有返回值:TriFunction
以下接口展示完整契约,需放在独立 TriFunction.java 文件中,或作为嵌套接口使用:
java
/**
* 接收三个参数并返回结果。
* @param <A> 第一个参数类型
* @param <B> 第二个参数类型
* @param <C> 第三个参数类型
* @param <R> 返回值类型
*/
@FunctionalInterface
public interface TriFunction<A, B, C, R> {
/**
* 执行三参数函数。
* @param first 第一个参数
* @param second 第二个参数
* @param third 第三个参数
* @return 执行结果
*/
R apply(A first, B second, C third);
}
java
TriFunction<Long, Integer, Long, String> query =
(cursor, size, orgId) -> orgId + ":" + cursor + ":" + size;
String result = query.apply(0L, 5000, 1L);
输入依次为游标、批次大小、机构 ID,返回 String 仅用于观察参数绑定,不代表执行了数据库查询。使用业务实体列表作为返回类型时,接口定义无需改变。
三个参数,无返回值:TriConsumer
java
/**
* 接收三个参数并执行动作,无返回值。
* @param <A> 第一个参数类型
* @param <B> 第二个参数类型
* @param <C> 第三个参数类型
*/
@FunctionalInterface
public interface TriConsumer<A, B, C> {
/**
* 执行三参数动作。
* @param first 第一个参数
* @param second 第二个参数
* @param third 第三个参数
*/
void accept(A first, B second, C third);
}
java
TriConsumer<Long, String, Integer> save = (orgId, sku, count) ->
System.out.println(orgId + ":" + sku + ":" + count);
save.accept(1L, "SKU-A", 3);
这是保存动作的结构示例,实际副作用只有打印。若需要判断,可定义返回 boolean 的 TriPredicate;不要仅因参数数量改变就忽视返回值语义。
四个参数,无返回值:QuadConsumer
java
/**
* 接收四个参数并执行动作。
*
* @param <A> 第一个参数类型
* @param <B> 第二个参数类型
* @param <C> 第三个参数类型
* @param <D> 第四个参数类型
*/
@FunctionalInterface
public interface QuadConsumer<A, B, C, D> {
/**
* 执行四参数动作。
*
* @param first 第一个参数
* @param second 第二个参数
* @param third 第三个参数
* @param fourth 第四个参数
*/
void accept(A first, B second, C third, D fourth);
}
java
QuadConsumer<Long, String, Integer, String> save =
(orgId, sku, count, source) ->
System.out.println(orgId + ":" + sku + ":" + count + ":" + source);
save.accept(1L, "SKU-A", 3, "receipt");
输入为机构、商品编码、数量和来源,输出是打印副作用,没有返回值。适用于调用方不需要结果的业务动作;void 不代表执行成功,底层异常仍会传播。此处打印示例未连接数据库,后面的完整程序验证参数映射和副作用。
自定义参数不等于不断增加接口参数
自定义类型完全可以作为标准接口的泛型参数。例如把机构、商品和数量封装为 SaveCommand,再使用 Consumer。有返回值的查询使用 Function<QueryRequest, Result>,后文提供完整 record 示例。
也可选择带业务名称的接口,例如 GoodsQuery,抽象方法命名 query,比到处使用泛型多参数接口更能表达业务约束。若行为只有一个普通调用点,没有传递、替换或回调需求,直接写普通方法通常更简单。
BiFunction:两个执行参数的最小抽象
本节机构路由代码为未执行的示意代码;不要把它当作现有项目的真实接口实现。
BiFunction 的类型契约
BiFunction<T, U, R> 表示:
text
输入第一个参数 T
输入第二个参数 U
返回结果 R
它只有一个抽象方法:
java
R apply(T first, U second);
以分页查询为例:
java
BiFunction<Long, Integer, List<String>> queryFunction =
(maxId, pageSize) -> {
return List.of(
"maxId=" + maxId,
"pageSize=" + pageSize
);
};
List<String> result = queryFunction.apply(0L, 5000);
System.out.println(result);
这里的两个参数分别是分页游标和批次大小,返回值是查询结果列表。定义 Lambda 时不会立即执行;只有调用 apply 才会执行函数体。
用 BiFunction 抽离机构路由
假设不同机构由不同的 Spring Util Bean 查询,但方法签名一致:
java
interface GoodsQueryUtil {
List<String> query(Long maxId, Integer pageSize, Long orgId);
}
final class KkdQueryUtil implements GoodsQueryUtil {
@Override
public List<String> query(Long maxId, Integer pageSize, Long orgId) {
return List.of("KKD:" + maxId + ":" + pageSize + ":" + orgId);
}
}
final class WyQueryUtil implements GoodsQueryUtil {
@Override
public List<String> query(Long maxId, Integer pageSize, Long orgId) {
return List.of("WY:" + maxId + ":" + pageSize + ":" + orgId);
}
}
路由方法可以返回一个只需要两个分页参数的函数:
java
private BiFunction<Long, Integer, List<String>> resolveQuery(
Long orgId,
GoodsQueryUtil kkdUtil,
GoodsQueryUtil wyUtil) {
if (1L.equals(orgId)) {
return (maxId, pageSize) ->
kkdUtil.query(maxId, pageSize, orgId);
}
if (2L.equals(orgId)) {
return (maxId, pageSize) ->
wyUtil.query(maxId, pageSize, orgId);
}
return null;
}
调用方式:
java
BiFunction<Long, Integer, List<String>> queryFunction =
resolveQuery(1L, new KkdQueryUtil(), new WyQueryUtil());
if (queryFunction == null) {
throw new IllegalArgumentException("不支持的机构");
}
long maxId = 0L;
int pageSize = 5000;
while (true) {
List<String> result = queryFunction.apply(maxId, pageSize);
if (result.isEmpty()) {
break;
}
System.out.println(result);
// 示例实现每次都返回非空列表,因此这里只演示一次调用。
// 真实分页应从结果末条记录取得下一游标。
break;
}
这个重构把两个问题分开:
resolveQuery负责一次性选择机构路由;while中这里只演示所选函数的一次调用,并没有保存或推进游标;真实业务循环还需要承担分页结束判断、保存和游标推进。
为什么三个参数的方法可以返回 BiFunction
resolveQuery 中的 orgId 被 Lambda 捕获:
java
(maxId, pageSize) ->
kkdUtil.query(maxId, pageSize, orgId)
因此,调用者不需要每页再次传入 orgId。这称为闭包捕获。被捕获的局部变量必须是 final 或"有效 final",也就是初始化后没有重新赋值:
java
Long orgId = 1L;
// 可以捕获,因为后面没有重新给 orgId 赋值
BiFunction<Long, Integer, List<String>> query =
(maxId, pageSize) -> List.of(orgId.toString());
下面的写法不能通过编译:
java
Long orgId = 1L;
orgId = 2L;
// orgId 已经被重新赋值,不再是有效 final
BiFunction<Long, Integer, String> query =
(maxId, pageSize) -> orgId + ":" + maxId;
因此,参数是否能被"提前绑定",是选择 BiFunction 的关键。
有效 final 只限制局部变量不能重新赋值,不保证其引用的对象不可变。下面的 Supplier 捕获的是同一个列表引用,并没有保存列表内容的快照:
java
List<String> names = new ArrayList<>();
names.add("A");
Supplier<Integer> sizeSupplier = names::size;
names.add("B");
int observedSize = sizeSupplier.get(); // 2
如果需要快照,可在定义函数前使用 List.copyOf(names) 创建不可修改的浅拷贝,再捕获该副本。浅拷贝不冻结元素内部状态;并发访问可变对象时,还需要单独考虑线程安全。
四参数函数:手写 QuadFunction
本节业务代码为未执行的示意代码;四参数接口调用和方法引用在后面的完整程序中验证。
为什么 Java 没有标准 QuadFunction
Java 标准库提供了 Function、BiFunction、Consumer、BiConsumer、Predicate 等常用接口,但没有继续提供 TriFunction 或 QuadFunction。
不能仅凭缺少接口推断标准库设计者的具体原因。从业务设计角度看,持续增加参数会增加接口维护和调用顺序的成本。业务代码确实需要四个独立参数时,可以定义项目自己的函数式接口。
定义 QuadFunction
java
/**
* 接收四个参数并返回一个结果的函数式接口。
*
* @param <A> 第一个参数类型
* @param <B> 第二个参数类型
* @param <C> 第三个参数类型
* @param <D> 第四个参数类型
* @param <R> 返回结果类型
*/
@FunctionalInterface
public interface QuadFunction<A, B, C, D, R> {
/**
* 使用四个参数执行函数。
*
* @param first 第一个参数
* @param second 第二个参数
* @param third 第三个参数
* @param fourth 第四个参数
* @return 函数执行结果
*/
R apply(A first, B second, C third, D fourth);
}
@FunctionalInterface 让编译器检查接口是否满足函数式接口契约;即使省略该注解,只要接口满足契约,仍可以使用 Lambda 和方法引用。该注解不会创建实现,也不会自动执行函数。
四参数查询示例
假设业务方法需要以下参数:
maxId:分页游标;pageSize:批次大小;orgId:机构 ID;supplierId:供应商 ID。
定义查询方法:
java
public List<String> query(
Long maxId,
Integer pageSize,
Long orgId,
Long supplierId) {
return List.of(
"maxId=" + maxId,
"pageSize=" + pageSize,
"orgId=" + orgId,
"supplierId=" + supplierId
);
}
使用 Lambda:
java
QuadFunction<
Long,
Integer,
Long,
Long,
List<String>
> queryFunction = (maxId, pageSize, orgId, supplierId) ->
query(maxId, pageSize, orgId, supplierId);
List<String> result = queryFunction.apply(
0L,
5000,
1L,
100L
);
如果方法参数和返回值能适配目标接口,可以使用方法引用;不要求签名字面完全一致,适配可能涉及装箱、拆箱或引用类型转换。这里参数按相同顺序传递:
java
QuadFunction<
Long,
Integer,
Long,
Long,
List<String>
> queryFunction = this::query;
方法引用更短,但它隐藏了参数映射关系。参数顺序不直观时,Lambda 通常更容易审查。
让 Spring Util 实现四参数查询
在真实项目中,不要手动 new 一个带 @DS、事务或其他 Spring 注解的 Util。应该使用注入的 Bean:
java
QuadFunction<
Long,
Integer,
Long,
Long,
List<MallReceiptCompletedUnsoldGoods>
> queryFunction = sunnyKkdUtil::getGoodsList;
前提是 Util 中存在匹配的方法:
java
public List<MallReceiptCompletedUnsoldGoods> getGoodsList(
Long maxId,
Integer pageSize,
Long orgId,
Long supplierId) {
return replenishmentRepo.queryGoods(
maxId,
pageSize,
orgId,
supplierId
);
}
通过注入的 Bean 调用,才能保留 Spring 代理、动态数据源切换和事务边界。函数式接口只负责保存调用关系,不负责实现数据源切换。
参数对象:四参数接口的替代方案
四参数接口虽然直接,但参数顺序容易传错。特别是多个参数都是 Long 时,编译器无法判断它们的业务含义:
java
queryFunction.apply(
supplierId, // 如果误传到 maxId,仍可能编译通过
5000,
orgId,
maxId
);
更稳妥的做法是封装请求对象:
java
/**
* 未动销商品分页查询条件。
*/
public record GoodsQueryRequest(
Long maxId,
Integer pageSize,
Long orgId,
Long supplierId) {
}
使用 Function:
java
Function<GoodsQueryRequest, List<String>> queryFunction =
request -> query(
request.maxId(),
request.pageSize(),
request.orgId(),
request.supplierId()
);
GoodsQueryRequest request =
new GoodsQueryRequest(0L, 5000, 1L, 100L);
List<String> result = queryFunction.apply(request);
这里的运行时参数从四个变成一个,但业务字段仍然是四个。请求对象能够通过字段名表达含义,也便于未来增加排序、渠道或时间范围条件。
三种方式的选择
| 方式 | 适用场景 | 主要代价 |
|---|---|---|
BiFunction |
执行阶段只有两个参数变化,其余参数可提前绑定 | 依赖闭包捕获,隐藏固定参数 |
QuadFunction |
四个参数都需要在执行阶段传入,且参数稳定 | 参数顺序容易传错,接口需要维护 |
Function<Request, R> |
参数有明确业务含义,未来可能扩展 | 需要增加请求对象类型 |
选择顺序建议是:先判断是否能提前绑定;如果不能,再判断四个参数是否稳定且语义简单;如果参数会扩展或同类型参数较多,优先使用请求对象。
失败路径与边界
不支持机构时返回 null
路由方法返回 null 是一种明确但容易遗漏的失败契约:
java
BiFunction<Long, Integer, List<String>> queryFunction =
resolveQuery(orgId, kkdUtil, wyUtil);
if (queryFunction == null) {
log.warn("机构:{}未配置查询路由", orgId);
return;
}
如果调用方直接执行:
java
queryFunction.apply(maxId, pageSize);
就会产生 NullPointerException。因此要么强制调用方判空,要么改为抛出带机构信息的异常:
java
return Optional.ofNullable(resolveQuery(orgId, kkdUtil, wyUtil))
.orElseThrow(() ->
new IllegalArgumentException(
"未配置机构查询路由:" + orgId
));
在定时任务中,是否抛异常要根据任务的失败策略决定。不能捕获异常后仍发送"执行完成"通知,否则会把失败伪装成成功。
分页游标不递增
函数式接口不会自动保证分页正确。查询结果仍然必须满足游标约束:
下面切换为包含游标的实体类型,不再使用前面的 List<String>。片段放在返回 void 的批次处理方法中;maxId 为当前游标。示例没有连接数据库,只展示类型和校验契约。
java
record UnsoldGoods(Long maxId, String sku) {
}
List<UnsoldGoods> result = List.of(new UnsoldGoods(10L, "SKU-A"));
if (result.isEmpty()) {
return;
}
Long nextMaxId = result.get(result.size() - 1).maxId();
if (nextMaxId == null || nextMaxId <= maxId) {
throw new IllegalStateException(
"分页游标未递增,当前:" + maxId
+ ",下一页:" + nextMaxId
);
}
maxId = nextMaxId;
如果 SQL 使用 id > maxId 和 ORDER BY id ASC,末条 ID 作为下一次游标才有意义。函数式重构只能改善代码组织,不能替代 SQL 排序、唯一性和并发数据校验。
异常传播
BiFunction.apply 和自定义 QuadFunction.apply 都不会自动处理异常:
下面的 queryFunction 明确指四参数 QuadFunction<Long, Integer, Long, Long, List<String>>;使用 BiFunction 时只能传入两个参数。
java
try {
List<String> result = queryFunction.apply(0L, 5000, 1L, 100L);
} catch (RuntimeException exception) {
log.error("查询未动销商品失败", exception);
throw exception;
}
如果底层方法声明受检异常,函数式接口也必须声明异常,或者在适配层转换为项目约定的运行时异常。不要为了满足 Lambda 而静默吞掉异常。
验证与可复现步骤
可复制运行的完整示例(已验证)
将以下代码保存为 UTF-8 编码的 FunctionExamples.java。使用 JDK 17,无需 Maven、Spring 或数据库。前面的短片段用于解释接口契约;这个完整文件用于复现调用行为。
java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.function.*;
/** 验证不同输入输出契约的独立示例。 */
public class FunctionExamples {
/** 三参数取值函数。 */
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A first, B second, C third);
}
/** 三参数动作。 */
@FunctionalInterface
interface TriConsumer<A, B, C> {
void accept(A first, B second, C third);
}
/** 四参数取值函数。 */
@FunctionalInterface
interface QuadFunction<A, B, C, D, R> {
R apply(A first, B second, C third, D fourth);
}
/** 四参数动作,无返回值。 */
@FunctionalInterface
interface QuadConsumer<A, B, C, D> {
void accept(A first, B second, C third, D fourth);
}
/** 查询条件,大小单位为记录条数。 */
record QueryRequest(long cursor, int size, long orgId, long supplierId) {
}
/** 以显式检查验证所有接口调用,不依赖 JVM 的 -ea 参数。 */
public static void main(String[] args) throws Exception {
List<String> events = new ArrayList<>();
Runnable action = () -> events.add("run");
action.run();
Supplier<String> value = () -> "default";
check(value.get().equals("default"));
Callable<String> task = () -> "done";
check(task.call().equals("done"));
Consumer<String> save = events::add;
save.accept("saved");
Function<String, Integer> length = String::length;
check(length.apply("Java") == 4);
Predicate<String> nonBlank = text -> !text.isBlank();
check(nonBlank.test("Java"));
UnaryOperator<String> trim = String::trim;
check(trim.apply(" Java ").equals("Java"));
BiConsumer<String, Integer> report = (name, count) ->
events.add(name + ":" + count);
report.accept("A", 3);
BiFunction<String, Integer, String> format = (name, count) ->
name + ":" + count;
check(format.apply("A", 3).equals("A:3"));
BiPredicate<String, String> same = String::equals;
check(same.test("A", "A"));
BinaryOperator<Integer> sum = Integer::sum;
check(sum.apply(2, 3) == 5);
TriFunction<Long, Integer, Long, String> query = (cursor, size, org) ->
org + ":" + cursor + ":" + size;
check(query.apply(0L, 5000, 1L).equals("1:0:5000"));
TriConsumer<Long, String, Integer> record = (org, sku, count) ->
events.add(org + ":" + sku + ":" + count);
record.accept(1L, "SKU-A", 3);
QuadConsumer<Long, String, Integer, String> saveFour =
(org, sku, count, source) ->
events.add(org + ":" + sku + ":" + count + ":" + source);
saveFour.accept(1L, "SKU-A", 3, "receipt");
QuadFunction<Long, Integer, Long, Long, String> quad =
FunctionExamples::queryFour;
check(quad.apply(0L, 5000, 1L, 100L).equals("1:100:0:5000"));
long orgId = 1L;
BiFunction<Long, Integer, String> bound = (cursor, size) ->
queryFour(cursor, size, orgId, 100L);
check(bound.apply(0L, 5000).equals("1:100:0:5000"));
Function<QueryRequest, String> requestQuery = request ->
queryFour(request.cursor(), request.size(),
request.orgId(), request.supplierId());
check(requestQuery.apply(new QueryRequest(0L, 5000, 1L, 100L))
.equals("1:100:0:5000"));
List<String> names = new ArrayList<>();
names.add("A");
Supplier<Integer> sizeSupplier = names::size;
List<String> snapshot = List.copyOf(names);
Supplier<Integer> snapshotSize = snapshot::size;
names.add("B");
check(sizeSupplier.get() == 2);
check(snapshotSize.get() == 1);
check(events.equals(List.of(
"run", "saved", "A:3", "1:SKU-A:3", "1:SKU-A:3:receipt"
)));
System.out.println("PASS: 所有接口调用及参数绑定检查通过");
}
/** 拼接参数以观察四参数方法引用的映射关系。 */
private static String queryFour(
Long cursor,
Integer size,
Long orgId,
Long supplierId) {
return orgId + ":" + supplierId + ":" + cursor + ":" + size;
}
/** 检查失败时明确中止示例。 */
private static void check(boolean condition) {
if (!condition) {
throw new AssertionError("示例结果与预期不符");
}
}
}
在保存该文件的目录执行:
bash
javac -encoding UTF-8 --release 17 FunctionExamples.java
java -Dfile.encoding=UTF-8 FunctionExamples
2026-10-08 在 Java/javac 17.0.13 下执行通过。预期输出以 PASS 开头;任何显式检查失败都会抛 AssertionError,进程不会报告成功。显式 UTF-8 编码避免中文注释在默认编码为 US-ASCII 的环境中编译失败。
| 项目 | 契约与验证结果 |
|---|---|
| 输入 | 固定的字符串、游标、条数、机构和供应商示例值;无外部输入 |
| 输出 | 副作用列表、转换结果、布尔结果及 PASS 提示 |
| 不变量 | 参数位置映射和返回值与显式预期一致 |
| 失败 | check 失败抛 AssertionError;异常向 main 传播 |
| 已证明 | 零参至四参接口调用(含四参无返回值)、方法引用、固定参数捕获、可变对象捕获与浅拷贝、请求对象调用通过 |
| 未证明 | 真实商品查询、SQL、Spring 代理、线程安全和异步执行 |
| 下一步 | 接入业务后分别验证数据源切换、查询结果及分页结束条件 |
未验证范围
以下内容没有在本文示例中运行:
- Spring Bean 注入;
@DS动态数据源切换;- MyBatis SQL;
- 真实数据库分页;
- 事务提交和定时任务通知。
因此,示例能够说明函数式接口的 Java 语义,但不能证明接入具体项目后不会出现 SQL、数据源或事务问题。
取舍与落地建议
建议按以下规则落地:
- 先确定有无返回值,再判断是否为 boolean 判断或同类型运算,依据前面的选择表选标准接口。只有两个参数在循环中变化时,优先使用
BiFunction,把机构或固定条件绑定在路由选择阶段。 - 四个参数都需要动态传入,且参数含义稳定时,可以使用
QuadFunction。 - 参数超过四个、同类型参数较多,或条件未来会扩展时,使用请求对象和
Function<Request, Result>。 - 需要保留 Spring AOP、
@DS或事务时,只引用注入的 Bean,不要手动创建 Util。 - 路由选择、分页执行、保存结果和完成通知分开处理;函数式接口只承担"选择并执行方法"的职责。
- 对不支持的机构、空列表、游标不递增和底层异常分别定义契约,不要用一个空列表掩盖所有失败。
选择结论
BiFunction 适合"执行时有两个变化参数"的场景,即使底层方法总共有三个或更多参数,也可以通过 Lambda 提前绑定固定条件。
当四个参数都必须在执行时传入时,可以手写带 @FunctionalInterface 的 QuadFunction。不过,接口参数越多,调用方越容易因顺序错误而产生隐蔽问题;如果参数具有明确业务含义或未来会增长,封装请求对象通常更稳妥。
函数式方法的价值不是减少几行代码,而是把"选择哪个实现"和"如何执行分页"拆开。它只有在参数契约、异常语义和数据源边界都清晰时,才会真正提高可维护性。