3.8 复合Lambda表达式的有用方法

Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的ComparatorFunctionPredicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这违背了函数式接口的定义啊!)窍门在于,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。第13章会详谈。现在只需相信我们,等想要进一步了解默认方法以及你可以用它做什么时,再去看看第13章。

3.8.1 比较器复合

我们前面看到,你可以使用静态方法Comparator.comparing,根据提取用于比较的键值的Function来返回一个Comparator,如下所示:

  1. Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
  • 逆序

如果你想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator的实例。接口有一个默认方法reversed可以使给定的比较器逆序。因此仍然用开始的那个Comparator,只要修改一下前一个例子就可以对苹果按重量递减排序:

  1. inventory.sort(comparing(Apple::getWeight).reversed()); ←---- 按重量递减排序
  • 比较器链

上面说得都很好,但如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?你可能需要再提供一个Comparator来进一步定义这个比较。比如,在按重量比较两个苹果之后,你可能想要按原产国排序。thenComparing方法就是做这个用的。它接受一个函数作为参数(就像comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator。你又可以优雅地解决这个问题了:

  1. inventory.sort(comparing(Apple::getWeight)
  2. .reversed() ←---- 按重量递减排序
  3. .thenComparing(Apple::getCountry)); ←---- 两个苹果一样重时,进一步按国家排序

3.8.2 谓词复合

谓词接口包括三个方法:negateandor,让你可以重用已有的Predicate来创建更复杂的谓词。比如,你可以使用negate方法来返回一个Predicate的非,比如苹果不是红的:

  1. Predicate<Apple> notRedApple = redApple.negate(); ←---- 产生现有Predicate对象redApple的非

你可能想要把两个Lambda用and方法组合起来,比如一个苹果既是红色又比较重:

  1. Predicate<Apple> redAndHeavyApple =
  2. redApple.and(apple -> apple.getWeight() > 150); ←---- 链接两个谓词来生成另一个Predicate对象

你可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:

  1. Predicate<Apple> redAndHeavyAppleOrGreen =
  2. redApple.and(apple -> apple.getWeight() > 150)
  3. .or(apple -> GREEN.equals(a.getColor())); ←---- 链接三个谓词来

这一点为什么很好呢?从简单Lambda表达式出发,你可以构建更复杂的表达式,但读起来仍然和问题的陈述差不多!请注意,andor方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a || b) && c。同样,a.and(b).or(c) 可以看作(a && b) || c

3.8.3 函数复合

最后,你还可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThencompose两个默认方法,它们都会返回Function的一个实例。

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。比如,假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字乘2,那么你可以将它们组合成一个函数h,先给数字加1,再给结果乘2:

  1. Function<Integer, Integer> f = x -> x + 1;
  2. Function<Integer, Integer> g = x -> x * 2;
  3. Function<Integer, Integer> h = f.andThen(g); ←---- 数学上会写作g(f(x))或(g o f)(x)
  4. int result = h.apply(1); ←---- 这将返回4

你也可以类似地使用compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x))andThen则意味着g(f(x))

  1. Function<Integer, Integer> f = x -> x + 1;
  2. Function<Integer, Integer> g = x -> x * 2;
  3. Function<Integer, Integer> h = f.compose(g); ←---- 数学上会写作f(g(x))或(f o g)(x)
  4. int result = h.apply(1); ←---- 这将返回3

图3-6说明了andThencompose之间的区别。

3.8 复合Lambda表达式的有用方法 - 图1

图 3-6 使用andThencompose

这一切听起来有点太抽象了。那么在实际中这有什么用呢?比方说你有一系列工具方法,对用String表示的一封信做文本转换:

  1. public class Letter{
  2. public static String addHeader(String text){
  3. return "From Raoul, Mario and Alan: " + text;
  4. }
  5. public static String addFooter(String text){
  6. return text + " Kind regards";
  7. }
  8. public static String checkSpelling(String text){
  9. return text.replaceAll("labda", "lambda");
  10. }
  11. }

现在你可以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上抬头,然后进行拼写检查,最后加上一个落款,如图3-7所示。

  1. Function<String, String> addHeader = Letter::addHeader;
  2. Function<String, String> transformationPipeline
  3. = addHeader.andThen(Letter::checkSpelling)
  4. .andThen(Letter::addFooter);

3.8 复合Lambda表达式的有用方法 - 图2

图 3-7 使用andThen的转换流水线

第二个流水线可能只加抬头、落款,而不做拼写检查:

  1. Function<String, String> addHeader = Letter::addHeader;
  2. Function<String, String> transformationPipeline
  3. = addHeader.andThen(Letter::addFooter);