1. 概述

Java 8 引入了函数式编程风格,让我们可以通过传入函数来参数化通用方法。

我们对单参数函数式接口(如 FunctionPredicateConsumer)应该比较熟悉了。
**本文重点介绍 Java 中处理两个参数的函数式接口,即 BiFunction**。这类函数也被称为“二元函数”。

2. 单参数函数回顾

Java 8 的 Stream API 中广泛使用了单参数函数。例如我们使用 map 来转换流中的每个元素:

List<String> mapped = Stream.of("hello", "world")
  .map(word -> word + "!")
  .collect(Collectors.toList());

assertThat(mapped).containsExactly("hello!", "world!");

如上所示,map 接受一个 Function,它只接收一个参数,用于对数据进行转换。

3. 双参数操作

Java Stream 提供了 reduce 方法,允许我们将流中的元素两两合并。
这个过程需要使用到 BinaryOperator<T> 接口,它接受两个相同类型的参数并返回一个同类型的结果。

假设我们想将流中的字符串按倒序拼接,并用短横线连接:

3.1 使用 Lambda 表达式

String result = Stream.of("hello", "world")
  .reduce("", (a, b) -> b + "-" + a);

assertThat(result).isEqualTo("world-hello-");

⚠️ 注意:初始值是空字符串,所以第一次拼接会以 "hello" + "-" + "" 的形式执行,导致最终结果末尾多了一个 -

你也可以显式声明参数类型:

String result = Stream.of("hello", "world")
  .reduce("", (String a, String b) -> b + "-" + a);

3.2 使用函数方法

如果逻辑变得复杂,我们可以将 Lambda 提取为一个普通方法:

private String combineWithoutTrailingDash(String a, String b) {
    if (a.isEmpty()) {
        return b;
    }
    return b + "-" + a;
}

然后在 reduce 中调用它:

String result = Stream.of("hello", "world")
  .reduce("", (a, b) -> combineWithoutTrailingDash(a, b));

assertThat(result).isEqualTo("world-hello");

✅ 这样可以让逻辑更清晰,也方便测试。

3.3 使用方法引用

很多 IDE 会提示你将上面的 Lambda 转换为方法引用,这样代码更简洁易懂:

String result = Stream.of("hello", "world")
  .reduce("", this::combineWithoutTrailingDash);

assertThat(result).isEqualTo("world-hello");

4. 使用 BiFunction

前面的例子中我们只处理了两个相同类型的参数。**BiFunction<T, U, R> 的优势在于它可以处理两个不同类型参数,并返回一个第三种类型的值**。

举个例子:我们想将两个不同类型的列表按元素一一组合:

List<String> list1 = Arrays.asList("a", "b", "c");
List<Integer> list2 = Arrays.asList(1, 2, 3);

List<String> result = new ArrayList<>();
for (int i=0; i < list1.size(); i++) {
    result.add(list1.get(i) + list2.get(i));
}

assertThat(result).containsExactly("a1", "b2", "c3");

4.1 抽象为通用函数

我们可以将上面的逻辑抽象成一个通用方法,使用 BiFunction 来处理每对元素:

private static <T, U, R> List<R> listCombiner(
  List<T> list1, List<U> list2, BiFunction<T, U, R> combiner) {
    List<R> result = new ArrayList<>();
    for (int i = 0; i < list1.size(); i++) {
        result.add(combiner.apply(list1.get(i), list2.get(i)));
    }
    return result;
}

4.2 调用通用方法

我们可以像这样使用:

List<String> list1 = Arrays.asList("a", "b", "c");
List<Integer> list2 = Arrays.asList(1, 2, 3);

List<String> result = listCombiner(list1, list2, (letter, number) -> letter + number);

assertThat(result).containsExactly("a1", "b2", "c3");

也可以处理完全不同的类型:

List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, (doubleNumber, floatNumber) -> doubleNumber > floatNumber);

assertThat(result).containsExactly(true, true, false);

4.3 使用方法引用替代 Lambda

我们也可以将判断逻辑提取为方法,并通过方法引用传入:

List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, this::firstIsGreaterThanSecond);

assertThat(result).containsExactly(true, true, false);

private boolean firstIsGreaterThanSecond(Double a, Float b) {
    return a > b;
}

这样代码更易读,方法名清晰表达了逻辑意图。

4.4 使用 this 的方法引用

我们还可以直接使用对象的 equals 方法来比较两个元素是否相等:

List<Float> list1 = Arrays.asList(0.1f, 0.2f, 4f);
List<Float> list2 = Arrays.asList(0.1f, 0.2f, 4f);

List<Boolean> result = listCombiner(list1, list2, Float::equals);

assertThat(result).containsExactly(true, true, true);

✅ 这是因为 Float.equals() 的签名正好符合 BiFunction<Float, Object, Boolean> 的要求。

5. BiFunction 的组合

Java 提供了 andThen 方法,允许我们对 BiFunction 进行组合,实现链式处理。

例如我们想比较两个浮点数列表中元素的大小:

List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Double> list2 = Arrays.asList(0.1d, 0.2d, 4d);

List<Integer> result = listCombiner(list1, list2, Double::compareTo);

assertThat(result).containsExactly(1, 1, -1);

如果我们希望最终结果是布尔值,可以使用 andThen 组合处理:

private static <T, U, R> BiFunction<T, U, R> asBiFunction(BiFunction<T, U, R> function) {
    return function;
}

然后这样调用:

List<Double> list1 = Arrays.asList(1.0d, 2.1d, 3.3d);
List<Double> list2 = Arrays.asList(0.1d, 0.2d, 4d);

List<Boolean> result = listCombiner(list1, list2,
  asBiFunction(Double::compareTo).andThen(i -> i > 0));

assertThat(result).containsExactly(true, true, false);

6. 总结

本文介绍了 Java 中的 BiFunction 接口,以及如何结合 Stream 和自定义函数使用它。我们演示了:

  • 使用 Lambda 表达式和方法引用传递 BiFunction
  • 抽象通用函数来处理不同类型的数据
  • 通过 andThen 实现函数组合

Java 提供的函数式接口只支持一元和二元函数。如果你需要处理更多参数,可以参考我们的 Java 柯里化(Currying)教程

完整代码示例可在 GitHub 上查看。


原始标题:Guide to Java BiFunction Interface | Baeldung