1.方法引用
Java8引入了一个功能:方法引用,可以将其看作是只调用特定方法的Lambda表达式的一种简写。它的基本思想是,如果一个Lambda表示的是"直接调用这个方法",那最好按名称来引用该方法,而不是描述如何调用它。方法引用根据已有的方法来创建的,基本格式为目标引用::方法名称
,这里需要显式指明方法名称。比如Apple::getWeight
就是引用了 Apple 类中定义的getWeight()方法,方法引用并不需要方法的括号,因为没有实际调用方法,这个方法引用是Lambda表达式(Apple a) -> a.getWeight()
的一种快捷写法,但比起Lambda表达式,方法引用可读性更好。例如下面分别使用Lambda和方法引用实现苹果库存根据重量排序。
//Lambda表达式
inventory.sort((Apple a1, Apple a2)-> a1.getWeight().compareTo(a2.getWeight()));
//方法引用
inventory.sort(Comparator.comparing(Apple::getWeight))
下表提供了一些Lambda和方法引用等效的例子,可以将方法引用看做针对仅仅涉及单一方法的Lambda的语法糖,表示同样的事情,方法引用代码更少。
方法引用主要有三类:
- 指向静态方法的方法引用(如Integer的parseInt方法,写作
Integer::parseInt
)。 - 指向任意类型的实例方法的方法引用(例如String的length方法,写作
String::length
)。 - 指向现有对象的实例方法的方法引用(假设一个局部变量expensiveTransaction用于存放 Transaction类型的对象,它支持实例方法getValue,方法引用可以写作
expensiveTransaction::getValue
)。
其中第二种方法引用,它的思想是引用一个对象的方法,而这个对象本身是Lambda的一个参数,例如Lambda表达式中(String s)->s.toUpperCase()
可以写作String::toUpperCase
。第三种方法引用指的是在Lambda中调用一个已经存在的外部对象中的方法,如Lambda表达式()->expensiveTransaction.getValue()
可以写作expensiveTransaction::getValue()
。可以根据下图所示这个简单诀窍,将Lambda表达式重构为等价的方法引用。
另外还有一些针对构造函数、数组构造函数和父类调用的一些特殊形式的方法引用。假设要对一个字符串的List排序,忽略大小写。List的sort方法需要一个Comparator
作为参数,其中Comparator
描述的是一个(T,T)->int
签名的函数描述符。利用String类的compareToIgnoreCase
方法来定义一个Lambda表达式(compareToIgnoreCase
是String类中预定义方法)。如下代码分别展示了Lambda表达式和方法引用的写法。
List<String> str = Arrays.asList("a","b","A","B");
//使用Lambda表达式
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
//使用方法引用
str.sort(String::compareToIgnoreCase);
这里需要注意的是编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配。
——————构造函数引用
上面提到的都是利用现有的方法来创建方法引用,对于一个现有的构造函数,也可以利用它的名称和关键字new来创建它的一个引用,格式为ClassName::new
。
1.【无参构造函数】:假如有个无参构造函数,它适合Supplier
的签名()->Apple
,那么可以这样做:
//构造函数引用指向默认的Apple()构造函数
Supplier<Apple> c2 = Apple::new;
//调用Supplier的get方法将产生一个新的Apple
Apple apple2 = c2.get();
等价于:
//利用默认构造函数创建,Apple的Lambda表达式
Supplier<Apple> c1 = ()->new Apple();
//调用Supplier的get方法将产生一个新的Apple
Apple apple = c1.get();
2.【单参构造函数】:假如有一个具有单个参数的构造函数,比如Apple(Integer weight)
,它适合Function
接口的签名,因此可以这样写:
//指向 Apple(Integer weight)的构造函数引用
Function<Integer,Apple> f1 = Apple::new;
//调用该Function函数的apply方法,并给出要求的重量,将产生一个Apple
Apple a1 = f1.apply(110);
等价于:
//用要求的重量创建一个Apple 的Lambda表达式
Function<Integer,Apple> f2 = weight->new Apple(weight);
//调用该Function函数的apply方法,并给出要求的重量,将产生一个新的Apple对象
Apple a2 = f1.apply(110);
3.【双参构造函数】:假如有一个具有两个参数的构造函数Apple(String color, Integer weight)
,那么它就适合 BiFunction
接口的签名,可以这样写:
//指向 Apple(String color,Integer weight) 的构造函数引用
BiFunction<Integer,String,Apple> f3 = Apple::new;
//调用该BiFunction函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
Apple a3 = f3.apply("green", 110);
等价于:
//用要求的颜色和重量创建一个Apple的Lambda表达式
BiFunction<Integer,String,Apple> biFunction1 = (weight,color)->new Apple(weight,color);
//调用该BiFunction 函数的apply方法,并给出要求的颜色和重量,将产生一个新的Apple对象
Apple a4 = f3.apply("green", 110);
对于大于两个参数的构造函数,可参考如下:
构造函数的引用:
上面提到了无参,具有一个,两个参数的构造函数转换为函数的引用方式,如果还有其他数量的构造函数,如Color(int ,int ,int )。由于Java8中没有提供此类型的函数式接口与构造函数引用的签名匹配。因此可以自定义一个接口,如:
public interface TriFunction<T, U, V, R>{
R apply(T t, U u, V v);
}
现在可以这样使用:
TriFunction<Integer, Integer, Integer, Color> colorFactory = Color::new;
2.Lambda和方法引用实战
以不同的排序策略给Apple列表排序为例,本节将会展示如何利用一些概念和特性:行为参数化、匿名类、Lambda表达式和方法引用,逐步将一个原始解决方案发展为一个简洁的解决方案,并给出最终解决方案如下:
inventory.sort(comparing(Apple::getWeight));
【方式一:传递代码】
Java8 API为List
提供了一个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 a1, Apple a2){
return a1.getWeight().compareTo(a2.getWeight());
}
});
【方式三:使用Lambda表达式】
匿名类实现方式依旧有点繁琐,Java8引入的Lambda表达式,它提供了一种轻量级语法来实现传递代码。在有函数式接口的地方就可以使用Lambda表达式,函数式接口就是仅仅定义了一个抽象方法的接口。抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。本例中Comparator
代表了函数描述符(T,T)->int
。因为使用的是Apple,所以它具体代表的是(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
对象。
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));
这就是最终的解决方案,比Java8之前的代码更简洁,并且意思明显,代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量”。
3.复合Lambda表达的有用方法
Java8中的很多函数式接口如Comparator
、Function
和Predicate
都提供了复合的方法,这意味着可以将多个简单的Lambda表达式复合成复杂的表达式,如让两个Predicate
之间做一个or
操作,组合成一个更大的Predicate
,还可以让一个函数的结果成为另外一个函数的输入。函数式接口这些方法实际上是默认方法,而不是抽象方法。
3.1 比较器复合
可以将一个比较键值的Function函数传给静态方法Comparator.comparing
来排序,如根据苹果重量排序:
Comparator<Apple> c = Comparator.comparing(Apple::getWeight);
【逆序】:如果需要对苹果按重量递减排序时,不需要去创建另外一个Comparator
实例,此接口有一个默认方法reversed
可以使给定的比较器逆序。代码如下:
//按重量递减排序
inventory.sort(Comparator.comparing(Apple::getWeight).reversed());
【比较器链】:上面排序,如果两个苹果重量一样时,需要通过原产国排序,那么这时候可以使用thenComparing
方法,它接收一个函数作为参数,代码如下:
//重量递减排序,两个苹果一样重时,根据原产国排序
inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry));
3.2 谓词复合
谓词接口Predicate
包括三个方法:nagate
、and
和or
,可以在已有的Predicate
基础上构建更复杂的Predicate
,如使用nagate
方法来返回一个Predicate
的非,比如不是红色苹果:
//产生现有 Predicate对象 redApple 的非
Predicate<Apple> notRedApple = redApple.negate();
可以将Predicate
类型的Lambda用and
方法组合起来,比如一个苹果既是红色又比较重:
//链接两个谓词来生成另一个 Predicate 对象
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
还可以进一步组合谓词,例如要么是大于150g的红苹果,要么是绿苹果:
Predicate<Apple> redAndHeavyAppleOrGreen = redApple
.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
由简单表达式构建出复杂的表达式,读起来仍然跟问题的陈述差不多,需要注意的是and
和or
方法是按照在表达式链中的位置,从左到右确定优先级的,因此a.or(b).and(c)
可以看作(a || b)&&c
。
3.3 函数复合
可以将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);
int result = h.apply(1);//结果返回4
可以类似地使用compose
方法,先把给定的函数用作 compose
的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子里用 compose
的话,它将意味着 f(g(x))
,而 andThen
则意味着 g(f(x))
:
//数学上会写作 f(g(x))或 (f o g)(x)
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1);//结果返回3
下图说明了andThen
和compose
之间的区别。
4.总结
- 方法引用让开发人员重复使用现有的方法实现并直接传递它们。
Comparator
、Predicate
和Function
等函数式接口都有几个可以用来结合 Lambda 表达式的默认方法。