为了给所有关于Lambda
的内容收个尾,需要继续研究开始的那个问题——用不同的排序策略给一个Apple
列表排序,并需要展示如何把一个原始粗暴的解决方案转变得更为简明。这会用到迄今讲到的所有概念和功能:行为参数化、匿名类、Lambda
表达式和方法引用。想要实现的最终解决方案是这样的:
inventory.sort(comparing(Apple::getWeight));
Java 8
的API
已经提供了一个List
可用的sort
方法,不用自己去实现它。最困难的部分已经搞定了。但是,如何把排序策略传递给sort
方法?sort
方法的签名是这样的:
void sort (Comparator<? super E> c)
它需要一个Comparator
对象来比较两个Apple
。这就是在Java
中传递策略的方式:它们必须包裹在一个对象里。sort
的行为被参数化了:传递给它的排序策略不同,其行为也会不同。
第一个解决方案看上去是这样的:
public class AppleComparator implements Comparator<Apple> {
public int compare(Apple a1, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight());
}
}
inventory.sort(new AppleComparator());
在前面看到了,可以使用匿名类来改进解决方案,而不是实现一个Comparator
却只实例化一次:
inventory.sort(new Comparator<Apple>() {
public int compare(Apple al, Apple a2) {
return a1.getWeight().compareTo(a2.getWeight ());
}
});
但解决方案仍然挺啰嗦的。Java 8
引入了Lambda
表达式,它提供了一种轻量级语法来实现相同的目标:传递代码。在需要函数式接口的地方可以使用Lambda
表达式。回顾一下:函数式接口就是仅仅定义一个抽象方法的接口。抽象方法的签名(称为函数描述符)描述了Lambda
表达式的签名。在这个例子里,Comparator
代表了函数描述符(T, T) -> int
。因为你用的是苹果,所以它具体代表的就是(Apple, Apple) -> int
。改进后的新解决方案看上去就是这样的:
inventory.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())
);
前面解释过了,Java
编译器可以根据Lambda
出现的上下文来推断Lambda
表达式参数的类型。那么解决方案就可以重写成这样:
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
代码还能变得更易读一点吗?Comparator
具有一个叫作comparing
的静态辅助方法,它可以接受一个Function
来提取Comparable
键值,并生成一个Comparator
对象。它可以像下面这样用(注意现在传递的Lambda
只有一个参数:Lambda
说明了如何从苹果中提取需要比较的键值):
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());
现在可以把代码再改得紧凑一点了:
import static java.util.Comparator.comparing;inventory.sort(comparing((a) -> a.getWeight()));
前面解释过,方法引用就是替代那些转发参数的Lambda
表达式的语法糖。可以用方法引用让代码更简洁(假设静态导入了java.util.Comparator.comparing):
inventory.sort(comparing(Apple::getWeight));
这就是你的最终解决方案。这比Java 8
之前的代码好在哪儿呢?它比较短;它的意思也很明显,并且代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量。”
Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竞,这违背了函数式接口的定义啊!)窍门在于,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。我们会在第9章详谈。现在只需相信我们,等想要进一步了解默认方法以及你可以用它做什么时,再去看看第9章。
前面看到,可以使用静态方法Comparator.comparing
,根据提取用于比较的键值的Function
来返回一个Comparator
,如下所示:
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
如果想要对苹果按重量递减排序怎么办?用不着去建立另一个Comparator
的实例。接口有一个默认方法reversed
可以使给定的比较器逆序。因此仍然用开始的那个比较器,只要修改一下前一个例子就可以对苹果按重量递减排序:
//按重量递减排序
inventory.sort(comparing(Apple::getWeight).reversed());
上面说得都很好,但如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?可能需要再提供一个Comparator
来进一步定义这个比较。比如,在按重量比较两个苹果之后,可能想要按原产国排序。thenComparing
方法就是做这个用的。它接受一个函数作为参数(就像comparing
方法一样),如果两个对象用第一个Comparator
比较之后是一样的,就提供第二个Comparator
。又可以优雅地解决这个问题了:
//按重量递减排序
inventory.sort (comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
两个苹果一样重时,进一步按国家排序
##谓词复合
谓词接口包括三个方法:negate
、and
和or
,可以重用已有的Predicate
来创建更复杂的谓词。比如,可以使用negate
方法来返回一个Predicate
的非,比如苹果不是红的: 产生现有
PredicatePredicatecApple> notRedApple = redApple.negate();
对象redApple
想要把两个Lambda
用and
方法组合起来,比如一个苹果既是红色又比较重:
链接两个谓词来生成另
Predicate<Apple> redAndHeavyApple =一个predicate对象redApple.and(a -> a.getWeight()> 150);
可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:
//链接Predicate的方法来构造更复杂Predicate对象
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a ->"green".equals(a.getColor()));
这一点为什么很好呢?从简单Lambda
表达式出发,可以构建更复杂的表达式,但读起来仍然和问题的陈述差不多!请注意,and
和or
方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)
可以看作(a || b)&& c
。
最后,还可以把Function
接口所代表的Lambda
表达式复合起来。Function
接口为此配了andThen
和compose
两个默认方法,它们都会返回Function
的一个实例。
andThen
方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。比如,假设有一个函数f给数字加1
(x -> x +1)
,另一个函数g
给数字乘2
,可以将它们组合成一个函数h
,先给数字加1
,再给结果乘2
:
//数学上会写作g(f(x))或(g o f)(x)
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
//这将返回4
int result = h.apply(1);
也可以类似地使用compose
方法,先把给定的函数用作compose
的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用compose
的话,它将意味着f(g(x))
,而andThen
则意味着g(f(x))
:
Function<Integer, Integer> f = x -> x + 1;
Function<Integer,Integer> g = x -> x * 2;
//数学上会写作f(g(x))或(f o g)(x)
Function<Integer, Integer> h = f.compose(g);
//这将返回3
int result = h.apply(1);
下图说明了andThen
和compose
之间的区别。
这一切听起来有点太抽象了。那么在实际中这有什么用呢?比方说有一系列工具方法,对
用String
表示的一封信做文本转换:
public class Letter {
public static String addHeader(String text) {
return "From Raoul, Mario and Alan:" + text;
}
public static String addFooter(String text) {
return text + "Kind regards";
public static String checkspelling(String text) {
return text.replaceAll("labda","lambda");
现在以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上抬头,然后进行拼写检查,最后加上一个落款,如下图所示:
Punction<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline= addHeader.andThen(Letter::checkspelling).andThen(Letter::addFooter);
第二个流水线可能只加抬头、落款,而不做拼写检查:
Function<String, String> addHeader = Letter::addHeader;
Function<String, String> transformationPipeline= addHeader.andThen(Letter::addFooter);
s