1. 概述
Java 8 引入了函数式编程风格,让我们可以通过传入函数来参数化通用方法。
我们对单参数函数式接口(如 Function
、Predicate
和 Consumer
)应该比较熟悉了。
**本文重点介绍 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 上查看。