方法的引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更加容易读,感觉也更加自然。下面就是我们借助更新的Java8 API,用方法的引用写的一个排序例子:
先前:
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
之后(使用方法引用和java.util.Comparator.comparing):
inventory.sort(Comparator.comparing(Apple::getWeight));
(1).管中窥豹
Lambda | 等效的方法引用 |
---|---|
(Apple a) -> a.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpstack() | Thread.currentThread()::dumpstack |
(String str, int i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
为什么应该关心方法引用呢?方法引用可以被看做仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是"直接调用这个方法",那最好还是用名称来调用它,而不是去描述如何调用。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显示的指明方法的名称,你的代码的可读性会更好。它是如何工作的呢?当你你需要使用方法引用时,目标引用放在分隔符前::前,方法的名称放在后面。例如,Apple::getWeight就是引用了Apple类中定义的方法getWeight。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是Lambda表达式(Apple a) -> a.getWeight()的快捷写法。下表给出了Java8中方法引用的其他例子:
Lambda | 等效的方法引用 |
---|---|
(Apple a) -> a.getWeight() | Apple::getWeight |
() -> Thread.currentThread().dumpstack() | Thread.currentThread()::dumpstack |
(String str, int i) -> str.substring(i) | String::substring |
(String s) -> System.out.println(s) | System.out::println |
你可以把方法引用看作一个针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更加少了。
2.如何构建方法引用
方法引用主要有三类:
A.指向静态方法的方法引用(例如,Integer的parseInt方法,写作Integer::parseInt)。
B.指向任意类型实例方法的方法引用(例如,String的length方法,写作String::length)。
C.指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction 用来存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。
第二种和第三种方法引用可能咋看起来有点儿晕。类似于String::length的第二个方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式(String s) -> s.toUpperCase()可以写作String::toUpperCase。但是第三种方法引用值得是,你在Lambda表达式调用了一个已经存在的外部对象中的方法。例如,Lambda表达式() -> expersiveTransaction.getValue()可以写作expensiveTransaction::getValue。
请注意,还有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用。让我们举一个方法引用的例子吧。例如,你想要对一个字符串的List排序,忽略大小写。List的sort方法需要一个Comparator作为参数。你在之前看到了,Comparator描述了一个具有(T, T) -> int签名的函数描述符。你可以利用String类的compareToIgnoreCase方法来定义一个Lambda表达式(注意:compareToIgnoreCase是String类中预先定义的)。
List str = Arrays.asList("a", "b", "c", "d");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
Lambda表达式的签名与Comparator的函数描述符兼容。利用前面所述的方法,这个例子可以用方法引用改写成下面这个样子
List str = Arrays.asList("b", "a", "d", "c");
str.sort(String::compareToIgnoreCase);
注意,编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配
例如:
A.
先前的Lambda表达式:
Function stringToInteger = (String s) -> Integer.parseInt(s);
现在的方法引用:
Function stringToInteger = Integer::parseInt;
B.
先前的Lambda表达式:
BiPredicate, String> contains = (list, element) -> list.contains(element);
现在的方法引用:
BiPredicate, String> contains = List::contains;
到现在,我们只展示了如何利用现有的方法实现和如何创建方法引用。但是你也可以对类的构造函数做类似的事情。
3.构造函数引用
对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数,那它比较适合Supplier的签名() -> Apple。你可以这样做:
Supplier c1 = Apple::new; //构造函数的引用指向Apple类的默认构造函数
Apple a1 = c1.get(); //调用Supplier的get方法将产生一个新的Apple对象
这就等价于:
Supplier c1 = () -> new Apple(); // 利用默认构造函数创建Apple的Lambda表达式
Apple a1 = c1.get(); //调用Supplier的get方法将产生一个新的Apple
也等价于:
Supplier c1 = new Supplier() {
@Override
public Apple get() {
return new Apple();
}
};
Apple a1 = c1.get();
如果你的构造函数的签名是int -> Apple(Apple的构造函数为 public Apple(int weight)), 那么它适合Function接口的签名,于是你可以这样写:
Function f = Apple::new; // 指向Apple(int weight)的构造函数引用
Apple a1 = f.apply(100); //调用该Function函数的apply方法,并且给出了要求的质量,将产生一个Apple
这就等价于:
Function f = (weight) -> new Apple(weight); // 用要求的重量创建一个Apple的Labda表达式
Apple a1 = f.apply(100); //调用该Function函数的apply方法,并且给出要求的重量,将产生一个新的Apple对象。
在下面的代码中,一个有Integer构成的List中的每个元素都通过我们前面定义的类似的map方法传递给Apple的构造函数,得到了一格具有不同质量苹果的List:
public class Demo7 {
public static void main(String[] args) {
List weights = Arrays.asList(70, 20, 100, 200);
List apples = map(weights, Apple::new);
}
public static List map(List weights, Function f){
List result = new ArrayList<>();
for(Integer i : weights){
result.add(f.apply(i));
}
return result ;
}
}
如果你有一个具有两个参数的构造函数Apple(String color, Integer weight) ,那么它就适合BiFunction接口的签名,于是你就可以这样写:
BiFunction b = Apple::new;//指向Apple(String color, Integer weight)的构造函数引用
Apple a1 = b.apply("green", 100); //调用该BiFunction函数的apply方法,并且给出要求的颜色和重量没奖产生一个新的Apple对象
这就等价于:
BiFunction b = (color, weight) -> new Apple(color, weight);//用要求的颜色和重量创建一个Lambda表达式
Apple a1 = b.apply("green", 100);//调用该BiFunction函数的apply方法,并且给出要求的颜色和重量,将产生一个新的Apple对象
不将构造翻书实例化却能够引用它,这个功能有一些有趣的应用。例如,你可以使用Map来讲构造函数映射到某一个字符串值。你可以创建一个getMeFruit方法,给它key、name、weight,它就可以创建不同名字、不同重量的各种水果:
public class Demo8 {
private static Map> map = new HashMap<>();
static{
map.put("apple", Fruit::new);
map.put("orange", Fruit::new);
}
public static Fruit getMeFruit(String key, String name, Integer weight){
return map.get(key).apply(name, weight);
}
public static void main(String[] args) {
}
}
4.扩展
你已经看到了如何将0个、1个、2个参数的构造参数转变为构造函数引用。那要怎么样才能对三个参数的构造函数,比如Color(int r, int g, int b),使用构造函数引用呢?
构造函数引用的语法是ClassName::new,那么在这个例子里面就是Color::new。但是你需要与构造函数引用的签名匹配的函数式接口。但是语言本身没有并没有提供这样的函数式接口,你可以自己创建一个:
@FunctionalInterface
public interface TriFunction {
R apply(T t, U u, V v);
}
现在你就可以向下面这样使构造函数引用了:
TriFunction f = Color::new;
5.Lambda和方法引用实践
为了给我们之前学习的所有关于Lambda的内容,我们需要继续研究开始的那个问题--用不同的排序策略给一个Apple集合排序,并且需要展示如何把一个原始粗暴的解决方法变得更加的简明。这会用到迄今为止讲到的行为参数化、匿名类、Lambda表达式和方法引用。我们需要实现的最终解决方法是这样的:
List inventory = Arrays.asList( new Apple(100), new Apple(20), new Apple(150), new Apple(10));
inventory.sort(Comparator.comparing(Apple::getWeight));
(1).传递代码
在Java8的API已经为我们提供了List可用的sort方法,所以我们不用自己去实现它。那么最困难的部分搞定了!但是,如何把排序策略传递给sort方法呢?你看sort方法的原型是这样的:
void sort(Comparator super E> c);
它需要一个Comparator对象来比较两个Apple!这就是在Java中传递策略的方式:它们必须包裹在一个对象里。我们说sort的行为被参数化了:传递给它的排序策略不同,其行为也会不同。
第一个解决方法是这样的:
public class Demo10 {
public static void main(String[] args) {
List inventory = Arrays.asList( new Apple(100), new Apple(20), new Apple(150), new Apple(10));
// inventory.sort(Comparator.comparing(Apple::getWeight));
inventory.sort(new AppleComparator());
}
}
class AppleComparator implements Comparator{
@Override
public int compare(Apple o1, Apple o2) {
return o1.compareTo(o2);
}
}
(2).使用匿名类
在这之前,你可以使用匿名类来改进解决方案,而不是实现一个Comparator却只实例化一次:
public class Demo10 {
public static void main(String[] args) {
List inventory = Arrays.asList( new Apple(100), new Apple(20), new Apple(150), new Apple(10));
// inventory.sort(Comparator.comparing(Apple::getWeight));
inventory.sort(new Comparator() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.compareTo(o2);
}
});
}
}
(3).使用Lambda表达式
尽管使用匿名类,但是代码还是非常的啰嗦。Java8引入了Lambda表达式,它提供了一种轻量级语法来实现相同的目标:传递代码。你看到了,在需要函数式接口的地方可以使用Lambda表达式。我们回顾一下:函数式接口就是仅仅定义了一个抽象方法的接口。抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。在这例子中,Comparator代表了函数描述符(T, T) -> int。因为你用的是苹果,它代表的是(Apple, Apple) -> int. 改进后的新解决方法看上去就是这样的
public class Demo10 {
public static void main(String[] args) {
List inventory = Arrays.asList( new Apple(100), new Apple(20), new Apple(150), new Apple(10));
// inventory.sort(Comparator.comparing(Apple::getWeight));
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 c= Comparator.comparing((Apple a1) -> a1.getWeight());
现在你可以把代码再改得紧凑一点:
inventory.sort(Comparator.comparing((a) -> a.getWeight()));
(4).使用方法引用
方法引用就是替代那些转发参数Lambda表达式的语法糖。你可以使用方法引用让你的代码更加简洁
inventory.sort(Comparator.comparing(Apple::getWeight));