5.8 闭包复合

问题

用户希望连续应用一系列简单且独立的函数。

方案

使用 FunctionConsumerPredicate 接口中定义为默认方法的复合方法(composition method)。

讨论

函数式编程的优点之一在于支持创建若干简单、可重复使用的函数,将这些函数组合在一起就能解决复杂的问题。为此,java.util.function 包引入的函数式接口定义了各种有助于简化复合操作的方法。

例如,Function 接口包括 composeandThen 两种默认方法,二者的签名如例 5-23 所示。

例 5-23 Function 接口定义的复合方法

  1. default <V> Function<V,R> compose(Function<? super V,? extends T> before)
  2. default <V> Function<T,V> andThen(Function<? super R,? extends V> after)

从方法签名中的哑元(dummy argument)不难看出两种方法的作用:compose 方法在原始函数之前应用其参数,而 andThen 方法在原始函数之后应用其参数。

为说明两种方法的应用,考虑例 5-24 所示的简单示例。

例 5-24 composeandThen 方法的应用

  1. Function<Integer, Integer> add2 = x -> x + 2;
  2. Function<Integer, Integer> mult3 = x -> x * 3;
  3. Function<Integer, Integer> mult3add2 = add2.compose(mult3);
  4. Function<Integer, Integer> add2mmult3 = add2.andThen(mult3);
  5. System.out.println("mult3add2(1): " + mult3add2.apply(1));
  6. System.out.println("add2mult3(1): " + add2mult3.apply(1));

❶ 先执行 mult3 函数,再执行 add2 函数

❷ 先执行 add2 函数,再执行 mult3 函数

可以看到,add2 函数的作用是将参数加 2,而 mult3 函数的作用是将参数乘 3。通过 compose 方法创建的复合函数 mult3add2 先执行 mult3,再执行 add2;通过 andThen 方法创建的复合函数 add2mult3 则相反,先执行 add2,再执行 mult3

两个复合函数的执行结果如下:

  1. mult3add2(1): 5 // 因为(1 * 3) + 2 == 5
  2. add2mult3(1): 9 // 因为(1 + 2) * 3 == 9

复合函数的结果仍然是函数,因此这个过程创建的操作可供今后使用。例如,如果收到的数据属于 HTTP 请求的一部分,说明数据是以字符串形式传输的。虽然已有可用于操作数据的方法,但前提为数据是数字。如果这种情况频繁发生,可以考虑在应用数值操作之前编写一个解析字符串数据的函数。详见例 5-25。

例 5-25 首先将字符串解析为整数,然后加 2

  1. Function<Integer, Integer> add2 = x -> x + 2;
  2. Function<String, Integer> parseThenAdd2 = add2.compose(Integer::parseInt);
  3. System.out.println(parseThenAdd2.apply("1"));
  4. // 打印3

本例创建了一个名为 parseThenAdd2 的函数,它先调用静态方法 Integer.parseInt,再将所得结果加 2。另一方面,也可以定义一个先执行数值操作,再调用 toString 方法的函数,如例 5-26 所示。

例 5-26 首先加 2,然后将数字转换为字符串

  1. Function<Integer, Integer> add2 = x -> x + 2;
  2. Function<Integer, String> plus2toString = add2.andThen(Object::toString);
  3. System.out.println(plus2toString.apply(1));
  4. // 打印"3"

上述操作返回一个函数,它传入 Integer 参数并返回 String

如例 5-27 所示,Consumer 接口也定义了一个用于闭包复合的方法。

例 5-27 Consumer 接口定义的复合方法

  1. default Consumer<T> andThen(Consumer<? super T> after)

根据 Javadoc 的描述,andThen 方法返回一个复合 Consumer,它依次执行原始操作和 after 指定的操作。如果执行任一操作时抛出异常,异常将被转发给组合操作的调用者。

示例代码如例 5-28 所示。

例 5-28 用于打印和记录的复合 Consumer

  1. Logger log = Logger.getLogger(...);
  2. Consumer<String> printer = System.out::println;
  3. Consumer<String> logger = log::info;
  4. Consumer<String> printThenLog = printer.andThen(logger);
  5. Stream.of("this", "is", "a", "stream", "of", "strings").forEach(printThenLog);

程序首先创建了两个 Consumer,一个用于打印结果到控制台,另一个用于日志记录。接下来,程序创建了一个复合 Consumer,可以一次性打印并记录流的所有元素。

Predicate 接口定义了三种用于谓词复合的方法,如例 5-29 所示。

例 5-29 Predicate 接口定义的复合方法

  1. default Predicate<T> and(Predicate<? super T> other)
  2. default Predicate<T> negate()
  3. default Predicate<T> or(Predicate<? super T> other)

andornegate 方法分别使用逻辑与、逻辑或、逻辑非操作实现谓词的复合,每种方法返回一个复合 Predicate

接下来,我们讨论一个关于整数的有趣问题。完全平方数(perfect square)是其平方根同样为整数的数,而三角形数(triangle number)是一定数目的点或圆在等距离排列下可以形成等边三角形的数8。

8参见维基百科有关“三角形数”的介绍。在一个房间中,如果每个人只与其他人握手一次,则三角形数是握手次数的总和。(例如,房间中有 2 个人时握手次数为 1,有 3 个人时握手次数为 3,有 4 个人时握手次数为 6,有 5 个人时握手次数为 10,有 6 个人时握手次数为 15,以此类推。那么 1、3、6、10、15 就是前 5 个三角形数,第 n 个三角形数的计算公式为 \frac{n(n+1)}{2} 。——译者注)

例 5-30 创建了两个用于计算完全平方数和三角形数的方法,并通过 and 方法查找既是完全平方数,又是三角形数的数。

例 5-30 既是完全平方数,又是三角形数的数

  1. public static boolean isPerfect(int x) {
  2. return Math.sqrt(x) % 1 == 0;
  3. }
  4.  
  5. public static boolean isTriangular(int x) {
  6. double val = (Math.sqrt(8 * x + 1) - 1) / 2;
  7. return val % 1 == 0;
  8. }
  9.  
  10. // 其他代码
  11.  
  12. IntPredicate triangular = CompositionDemo::isTriangular;
  13. IntPredicate perfect = CompositionDemo::isPerfect;
  14. IntPredicate both = triangular.and(perfect);
  15.  
  16. IntStream.rangeClosed(1, 10_000)
  17. .filter(both)
  18. .forEach(System.out::println);

❶ 部分完全平方数:1、4、9、16、25、36、49、64、81……

❷ 部分三角形数:1、3、6、10、15、21、28、36、45……

❸ 既是完全平方数,又是三角形数(1 到 10 000 之间):1、36、1225

借由复合函数,可以将若干简单的函数组合在一起以构建复杂的操作 9。

9Unix 操作系统就是基于这种理念构建的,具有类似的优点。

另见

有关 Function 接口的讨论请参见范例 2.4,有关 Consumer 接口的讨论请参见范例 2.1,有关 Predicate 接口的讨论请参见范例 2.3。