Java 闭包复合,用户希望连续应用一系列简单且独立的函数,使用 Function
、Consumer
与 Predicate
接口中定义为默认方法的复合方法(composition method)。函数式编程的优点之一在于支持创建若干简单、可重复使用的函数,将这些函数组合在一起就能解决复杂的问题。
Java 闭包复合 问题描述
用户希望连续应用一系列简单且独立的函数。
Java 闭包复合 解决方案
使用 Function
、Consumer
与 Predicate
接口中定义为默认方法的复合方法(composition method)。
Java 闭包复合 具体实例
函数式编程的优点之一在于支持创建若干简单、可重复使用的函数,将这些函数组合在一起就能解决复杂的问题。为此,java.util.function
包引入的函数式接口定义了各种有助于简化复合操作的方法。
例如,Function
接口包括 compose
和 andThen
两种默认方法,二者的签名如例 5-23 所示。
例 5-23
Function
接口定义的复合方法
default <V> Function<V,R> compose(Function<? super V,? extends T> before)
default <V> Function<T,V> andThen(Function<? super R,? extends V> after)
从方法签名中的哑元(dummy argument)不难看出两种方法的作用:compose
方法在原始函数之前应用其参数,而 andThen
方法在原始函数之后应用其参数。
为说明两种方法的应用,考虑例 5-24 所示的简单示例。
例 5-24
compose
和andThen
方法的应用
Function<Integer, Integer> add2 = x -> x + 2;
Function<Integer, Integer> mult3 = x -> x * 3;
Function<Integer, Integer> mult3add2 = add2.compose(mult3); ➊
Function<Integer, Integer> add2mmult3 = add2.andThen(mult3); ➋
System.out.println("mult3add2(1): " + mult3add2.apply(1));
System.out.println("add2mult3(1): " + add2mult3.apply(1));
❶ 先执行 mult3
函数,再执行 add2
函数
❷ 先执行 add2
函数,再执行 mult3
函数
可以看到,add2
函数的作用是将参数加 2,而 mult3
函数的作用是将参数乘 3。通过 compose
方法创建的复合函数 mult3add2
先执行 mult3
,再执行 add2
;通过 andThen
方法创建的复合函数 add2mult3
则相反,先执行 add2
,再执行 mult3
。
两个复合函数的执行结果如下:
mult3add2(1): 5 // 因为(1 * 3) + 2 == 5
add2mult3(1): 9 // 因为(1 + 2) * 3 == 9
复合函数的结果仍然是函数,因此这个过程创建的操作可供今后使用。例如,如果收到的数据属于 HTTP 请求的一部分,说明数据是以字符串形式传输的。虽然已有可用于操作数据的方法,但前提为数据是数字。如果这种情况频繁发生,可以考虑在应用数值操作之前编写一个解析字符串数据的函数。详见例 5-25。
例 5-25 首先将字符串解析为整数,然后加 2
Function<Integer, Integer> add2 = x -> x + 2;
Function<String, Integer> parseThenAdd2 = add2.compose(Integer::parseInt);
System.out.println(parseThenAdd2.apply("1"));
// 打印3
本例创建了一个名为 parseThenAdd2
的函数,它先调用静态方法 Integer.parseInt
,再将所得结果加 2。另一方面,也可以定义一个先执行数值操作,再调用 toString
方法的函数,如例 5-26 所示。
例 5-26 首先加 2,然后将数字转换为字符串
Function<Integer, Integer> add2 = x -> x + 2;
Function<Integer, String> plus2toString = add2.andThen(Object::toString);
System.out.println(plus2toString.apply(1));
// 打印"3"
上述操作返回一个函数,它传入 Integer
参数并返回 String
。
如例 5-27 所示,Consumer
接口也定义了一个用于闭包复合的方法。
例 5-27
Consumer
接口定义的复合方法
default Consumer<T> andThen(Consumer<? super T> after)
根据 Javadoc 的描述,andThen
方法返回一个复合 Consumer
,它依次执行原始操作和 after
指定的操作。如果执行任一操作时抛出异常,异常将被转发给组合操作的调用者。
示例代码如例 5-28 所示。
例 5-28 用于打印和记录的复合
Consumer
Logger log = Logger.getLogger(...);
Consumer<String> printer = System.out::println;
Consumer<String> logger = log::info;
Consumer<String> printThenLog = printer.andThen(logger);
Stream.of("this", "is", "a", "stream", "of", "strings").forEach(printThenLog);
程序首先创建了两个 Consumer
,一个用于打印结果到控制台,另一个用于日志记录。接下来,程序创建了一个复合 Consumer
,可以一次性打印并记录流的所有元素。
Predicate
接口定义了三种用于谓词复合的方法,如例 5-29 所示。
例 5-29
Predicate
接口定义的复合方法
default Predicate<T> and(Predicate<? super T> other)
default Predicate<T> negate()
default Predicate<T> or(Predicate<? super T> other)
and
、or
、negate
方法分别使用逻辑与、逻辑或、逻辑非操作实现谓词的复合,每种方法返回一个复合 Predicate
。
接下来,我们讨论一个关于整数的有趣问题。完全平方数(perfect square)是其平方根同样为整数的数,而三角形数(triangle number)是一定数目的点或圆在等距离排列下可以形成等边三角形的数8。
8参见维基百科有关“三角形数”的介绍。在一个房间中,如果每个人只与其他人握手一次,则三角形数是握手次数的总和。(例如,房间中有 2 个人时握手次数为 1,有 3 个人时握手次数为 3,有 4 个人时握手次数为 6,有 5 个人时握手次数为 10,有 6 个人时握手次数为 15,以此类推。那么 1、3、6、10、15 就是前 5 个三角形数,第 n 个三角形数的计算公式为
例 5-30 创建了两个用于计算完全平方数和三角形数的方法,并通过 and
方法查找既是完全平方数,又是三角形数的数。
例 5-30 既是完全平方数,又是三角形数的数
public static boolean isPerfect(int x) { ➊
return Math.sqrt(x) % 1 == 0;
}
public static boolean isTriangular(int x) { ➋
double val = (Math.sqrt(8 * x + 1) - 1) / 2;
return val % 1 == 0;
}
// 其他代码
IntPredicate triangular = CompositionDemo::isTriangular;
IntPredicate perfect = CompositionDemo::isPerfect;
IntPredicate both = triangular.and(perfect);
IntStream.rangeClosed(1, 10_000)
.filter(both)
.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 操作系统就是基于这种理念构建的,具有类似的优点。