Contents

Java BiFunction简介

1. 简介

Java 8 引入了函数式编程 ,允许我们通过传入函数来参数化通用方法。 我们可能最熟悉单参数 Java 8 函数式接口,例如FunctionPredicateConsumer

在本教程中,我们将研究使用两个参数的函数式接口。此类函数称为二进制函数,并在 Java 中使用 BiFunction函数接口表示。

2. 单参数函数

让我们快速回顾一下我们如何使用单参数或一元函数,就像我们在 中所做的那样:

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

正如我们所看到的,该map使用Function,它接受一个参数并允许我们对该值执行操作,返回一个新值。

3. 双参数运算

Java Stream 库为我们提供了一个reduce函数,它允许我们组合流的元素。我们需要通过添加下一项来表达我们迄今为止积累的值是如何转换的。

reduce函数使用函数接口BinaryOperator<T>,它将两个相同类型的对象作为其输入。

假设我们想通过将新项目放在前面并使用破折号分隔符来加入流中的所有项目。我们将在以下部分中介绍实现此功能的几种方法。

3.1. 使用 Lambda

BiFunction的 lambda 实现 以两个参数为前缀,用括号括起来:

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

正如我们所见,ab这两个值是Strings。我们编写了一个 lambda,将它们组合起来以产生所需的输出,第二个在前,中间有一个破折号。

我们应该注意,reduce使用一个起始值——在本例中是空字符串。因此,我们以上面代码的结尾破折号结束,因为我们的流中的第一个值与之相连。

另外,我们应该注意,Java 的类型推断允许我们在大多数情况下省略参数的类型。在上下文中无法明确 lambda 类型的情况下,我们可以为参数使用类型:

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;
}

然后调用它:

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

正如我们所见,lambda 调用了我们的函数,这比将更复杂的实现内联更容易阅读。

3.3. 使用方法参考

一些 IDE 会自动提示我们将上面的 lambda 转换为方法引用,因为它通常更易于阅读。

让我们重写我们的代码以使用方法引用:

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

方法引用通常使功能代码更加不言自明。

4. 使用BiFunction

到目前为止,我们已经演示了如何使用两个参数类型相同的函数。BiFunction接口允许我们使用不同类型的参数,并带有第三种类型的返回值***。***

假设我们正在创建一个算法,通过对每对元素执行操作,将两个大小相等的列表组合成第三个列表:

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;
}

让我们看看这里发生了什么。共有三种类型的参数:T表示第一个列表中的项目类型,U表示第二个列表中的类型,然后R表示组合函数返回的任何类型。

我们使用提供给这个函数的BiFunction通过调用它的apply方法来获取结果。

4.2. 调用广义函数

我们的组合器是BiFunction,它允许我们注入算法,无论输入和输出的类型如何。让我们试一试:

List<String> list1 = Arrays.asList("a", "b", "c");
List<Integer> list2 = Arrays.asList(1, 2, 3);
List<String> result = listCombiner(list1, list2, (a, b) -> a + b);
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, (a, b) -> a > b);
assertThat(result).containsExactly(true, true, false);

4.3. BiFunction方法参考

让我们用提取的方法和方法引用重写上面的代码:

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;
}

我们应该注意到,这使代码更易于阅读,因为方法firstIsGreaterThanSecond将注入的算法描述为方法引用。

4.4. BiFunction方法参考使用this

假设我们要使用上述基于BiFunction的算法来确定两个列表是否相等:

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, (a, b) -> a.equals(b));
assertThat(result).containsExactly(true, true, true);

我们实际上可以简化解决方案:

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

这是因为 Float 中的equals函数与BiFunction具有相同的签名。它接受this的隐式第一个参数,即Float类型的对象。Object类型的第二个参数other是要比较的值。

5. 组合BiFunctions

如果我们可以使用方法引用来做与我们的数字列表比较示例相同的事情会怎样?

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);

这与我们的示例很接近,但返回一个Integer,而不是原始的Boolean。这是因为Double中的compareTo方法返回Integer

我们可以通过使用andThen来添加我们需要的额外行为来组成一个函数。这会产生一个BiFunction,它首先对两个输入做一件事,然后执行另一个操作。

接下来,让我们创建一个函数来将我们的方法引用Double::compareTo强制转换为BiFunction

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

**lambda 或方法引用仅在通过方法调用转换后才成为BiFunction。**我们可以使用这个辅助函数将我们的 lambda 显式转换为BiFunction对象。

现在,我们可以使用andThen在第一个函数之上添加行为:

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);