《Java 8 in Action》【03】----Lambda表达式(二)

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的语法糖,表示同样的事情,方法引用代码更少。
在这里插入图片描述
 方法引用主要有三类:

  1. 指向静态方法的方法引用(如Integer的parseInt方法,写作Integer::parseInt)。
  2. 指向任意类型的实例方法的方法引用(例如String的length方法,写作String::length)。
  3. 指向现有对象的实例方法的方法引用(假设一个局部变量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中的很多函数式接口如ComparatorFunctionPredicate都提供了复合的方法,这意味着可以将多个简单的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包括三个方法:nagateandor,可以在已有的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()));

 由简单表达式构建出复杂的表达式,读起来仍然跟问题的陈述差不多,需要注意的是andor方法是按照在表达式链中的位置,从左到右确定优先级的,因此a.or(b).and(c)可以看作(a || b)&&c

3.3 函数复合

 可以将Function接口所代表的是Lambda表达式复合起来。Function接口为此配了andThencompose两个默认方法,它们都会返回一个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

 下图说明了andThencompose之间的区别。
在这里插入图片描述

4.总结
  1. 方法引用让开发人员重复使用现有的方法实现并直接传递它们。
  2. ComparatorPredicateFunction 等函数式接口都有几个可以用来结合 Lambda 表达式的默认方法。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值