Java 函数式接口1:从无参任务到自定义多参数查询

Java 函数式接口:从无参任务到自定义多参数查询

文章目录

内容摘要:如何根据参数数量、返回值和业务语义选择 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、数据源或事务问题。

取舍与落地建议

建议按以下规则落地:

  1. 先确定有无返回值,再判断是否为 boolean 判断或同类型运算,依据前面的选择表选标准接口。只有两个参数在循环中变化时,优先使用 BiFunction,把机构或固定条件绑定在路由选择阶段。
  2. 四个参数都需要动态传入,且参数含义稳定时,可以使用 QuadFunction。
  3. 参数超过四个、同类型参数较多,或条件未来会扩展时,使用请求对象和 Function<Request, Result>。
  4. 需要保留 Spring AOP、@DS 或事务时,只引用注入的 Bean,不要手动创建 Util。
  5. 路由选择、分页执行、保存结果和完成通知分开处理;函数式接口只承担"选择并执行方法"的职责。
  6. 对不支持的机构、空列表、游标不递增和底层异常分别定义契约,不要用一个空列表掩盖所有失败。

选择结论

BiFunction 适合"执行时有两个变化参数"的场景,即使底层方法总共有三个或更多参数,也可以通过 Lambda 提前绑定固定条件。

当四个参数都必须在执行时传入时,可以手写带 @FunctionalInterface 的 QuadFunction。不过,接口参数越多,调用方越容易因顺序错误而产生隐蔽问题;如果参数具有明确业务含义或未来会增长,封装请求对象通常更稳妥。

函数式方法的价值不是减少几行代码,而是把"选择哪个实现"和"如何执行分页"拆开。它只有在参数契约、异常语义和数据源边界都清晰时,才会真正提高可维护性。

相关推荐
IT_Octopus1 小时前
【零基础入门 LLM 开发 · Day 10】:LangChain 入门——一条管道符串起 prompt 和模型
python·langchain·prompt
天空鸟_时光不老1 小时前
01-我不转Python把AI塞进Java里
java·人工智能·spring boot·后端·spring·spring cloud·架构
vx_Biye_Design1 小时前
flask学生课程笔记共享系统29026-计算机课程设计、毕业设计
java·javascript·spring boot·后端·elasticsearch·flask·课程设计
代码山河2 小时前
JDK、JRE、JVM的区别:一文讲清楚Java运行环境
java·学习·架构·教程·面向对象·项目
zaemyn20202 小时前
macOS 上 AccessClient 无法唤起?排查 Python 架构与 Windows App 识别问题
windows·python·macos·远程桌面·accessclient
深入云栈2 小时前
Netty 4.2.x 源码深度解析 (十七):Channel 底层读写 —— Unsafe 的 I/O 操作内核
java·后端·架构
SimonKing2 小时前
Qoder中Qwen3.8-Flash 限时免费使用
java·后端·程序员
云和数据.ChenGuang2 小时前
langchain4j InMemoryEmbeddingStore常用的方法
java·人工智能·windows·java-ee·fastapi·springai