行为参数化
为了应对多变的需求,难道我们就要因为客户每提出一个需求,我们就要写一个方法去实现吗?
显然这样做很冗余,而且维护性大大降低,这说明代码的设计不够好。好在已经有前人帮我们提出了行为参数化思想(即将一段代码逻辑作为参数,使之可以在不同对象间传递)。
java1.8以前使用匿名类来实现行为参数化,即使用匿名类去实现一个函数式接口中的方法。java1.8之后,推出了Lambda表达式来替代以前匿名类实现行为参数化的繁复过程,使代码更简洁、更优雅。
Lambda初体验
先从简单的例子开始:创建一个thread,需要在Thread()构造方法中传入一个Runnable接口的实现类对象,但一般不会为了这个实现类对象去创建一个实现类,java1.8之前更简洁的更便于维护的方式是在构造方法中创建一个实现了Runnable接口的匿名类对象,只使用一次,代码如下:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类实现Runnable接口,实现功能需要6行代码");
}
}).start();
可以看到,通过匿名类实现Runnable接口,需要编写6行代码,但其实真正实现了我们需要的功能的代码只有一行(黑色加粗),从代码量上来看,这就显得很冗余,“高度问题(height problem)”。
java1.8发布的新特性,lambda表达式,就可以很好的解决这个问题,下面的代码等价上面的代码:
new Thread(() -> {System.out.println("使用Lambda表达式,只需要一行代码");}).start();
注意上面代码中的红色字体部分,这就是Lambda表达式的一个简单演示,lambda表达式充当了这个接口中的抽象方法的具体实现。
Lambda表达式的语法结构
下面我们就来看一下lambda表达式的几种使用语法:
(params) -> expression
(params) -> statement
(params) -> { statement; }
左边第一个括号中的params参数列表根据需要增加;中间是一个箭头,英文半角的-与大于号>组成,这两个符号之间不能有空格,箭头两边可以有空格;箭头的右边是表达式或者语句块。如果是类似“return a+b”这种结构的方法体,可以直接写成(int a, int b) -> a+b ,expresion能够返回该表达式的结果,可以看到lambda表达式把return这种方法退出语句都简化省略掉了。如果只是想通过控制台输出语句打印一段话,可以写成() -> System.out.println("Hello") 语句末尾的分号都可以省略不写。如果是实现方法的逻辑比较复杂,就可以用花括号将一段逻辑代码括起来,比如 () -> { 语句块 }
函数式接口
在进一步说明lambda表达式之前,先做一个知识储备,什么是函数式接口?
只拥有一个方法的接口,称为函数式接口。在以前的版本中,人们常称这种类型为SAM类型,即单抽象方法类型(SAM,Single Abstract Method)
java1.8之后,设计者们对JDK做了全面的改动,为符合函数式接口规范的接口,都加上了@FunctionalInterface注解,通知编译器这些接口是符合函数式接口的规范,虽然可能有的接口中有多个方法,但是方法的签名可以各有不同。
好像还是不太明白?我们找几个JDK的例子来看看,比如:
(1)Callable接口
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
(2)Runnable接口
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
(3)java.util.Comparator接口
@FunctionalInterface
public interface Comparator {
int compare(T o1, T o2);
boolean equals(Object obj);
// java1.8之后还增加了一些default方法,这里就不列出
}
可以发现,Callable和Runnable这两个接口的共性,接口中都只声明了一个方法。符合这种结构规范的interface,java中就称之为函数式接口。而在(3)Comparator接口中有两个方法,为什么呢?因为boolean equals(Object obj)是Object类的public方法,函数式接口中允许定义Object的public方法,像clone()方法就不能定义因为是protected方法,加上了@FunctionalInterface注解告诉编译器,这个接口必须符合函数式接口规范的,如果不符合就会编译报凑。
Lambda表达式的结果类型,目标类型(Target Typing)
在初体验的例子中,好像lambda表达式没有结果值类型,但不代表lambda就没有结果类型,只是我们不需要指定lambda表达式的结果类型。
那lambda表达式的结果类型是什么呢?答案是:它的类型是由其上下文推导而来。也就是说,同一段lambda表达式在不同的上下文环境中,可能会有不同的结果类型,比如:
Callable c =() -> "done.";
PrivilegedAction p =() -> "done.";
虽然c和p等号右边的lambda表达式一样,但是两个lambda表达式的结果却不一样,第一个是Callable类型,第二个是PrivilegedAction类型。
由编译器完成对Lambda表达式的结果类型推导,编译器根据Lambda表达式的上下文推导出一个预期的类型,这个预期的类型就是目标类型。lambda表达式对目标类型也有要求,编译器会检查lambda表达式的推导类型和目标类型的方法签名是否一致。需要满足下列全部条件,lambda表达式才可以被赋给目标类型T:
·T 是一个函数式接口
·lambda表达式的参数与 T 中的方法的形参列表在数量、类型上完全一致
·lambda表达式的返回值与 T 中的方法的返回值相兼容,lambda表达式的返回值类型应该是 T 的实现类或子类
·lambda表达式内所抛出的异常与 T 中的方法throws的异常类型相兼容,同上一条
我个人对目标类型的理解:
目标类型不同于返回值类型,它是对要实现的方法所属的函数式接口的一种参考,待实现方法有返回值类型,也有其所属的接口或类,而这个方法所属的接口或类,就是目标类型。
java设计者要求,lambda表达式只能出现在目标类型为函数式接口的上下文中。
代码高度降低了,宽度呢?
lambda表达式将多行代码浓缩到一行,是解决了“高度问题”,但是过多的信息在一行表述,显然会增加lambda表达式一行的代码量,这就产生了“宽度问题”,java设计者在设计lambda表达式时考虑到这一点,做了优化的设计:
(1)省略形参类型
由于目标类型(函数式接口)已经“知道”lambda表达式的形式参数(Formal parameter)类型,所以没有必要把已知类型再重复写一遍。也就是说,lambda表达式的参数类型可以从目标类型中得出。
举个例子:
Comparator c = (s1, s2) -> s1.compareToIgnoreCase(s2);
其中s1和s2我们虽然没有明确指定其参数类型,但是编译器可以通过上下文推导出其形参类型,Comparator接口中有两个方法,int compare(T o1, T o2)、boolean equals(Object obj),根据lambda表达式的参数列表(2个形参),可以推导出要实现的接口方法是compare(T o1, T o2),又根据目标类型Comparator指定了就是,就可以推导出s1和s2的参数类型就是String。
(2)当lambda参数只有一个且其类型可以被推导出时,参数列表的()括号也可以省略
举个例子:
FileFilter java = f -> f.getName().endsWith(".java");
java.io.FileFilter接口中仅有一个方法,boolean accept(File pathname),可以推导出该lambda表达式的参数列表应该是File类型,也就是说参数f的类型也可以省略了,而且只有这一个参数,那么括号()也可以省略了。
上下文
上面提到很多次lambda表达式只能出现拥有目标类型的上下文中,下面列出带有目标类型的上下文:
·变量声明
·赋值
·返回语句
·数组初始化器
·方法和构造方法的参数
·lambda表达式函数体
·条件表达式(? :)
·转型(Cast)表达式
方法引用
通过上面的例子和说明,我们知道了lambda表达式允许我们自定义一个匿名方法((params) -> {...} 这看起来就像是一个没有名字的方法定义),并能以函数式接口的方式使用这个匿名方法。那现在我们也可以不用自定义方法,直接引用已有的方法也是可以的,这种引用我们称之为方法引用。
方法引用和lambda表达式拥有相同的特性(例如,都需要一个目标类型,并且需要被转换为函数式接口的实例),只不过不需要为已有方法提供方法体,我们直接通过该方法的名字就可以引用这个已有方法。
举个例子:
class Person {
private final String name;
public String getName(){
return this.name;
}
....
}
Person[] people = ...
Comparator byName = Comparator.comparing(p - > p.getName());
Arrays.sort(people, byName);
----------------------------------------
加粗部分可以用方法引用lambda表达式来替代:
Comparator byName = Comparator.coparint(Person::getName);
是不是看起来表义就更清晰了呢?方法引用Person::getName就可以看作是lambda表达式p -> p.getName()的一种简写形式,虽然看起来好像代码量没有减少多少,但是拥有了更明确的语义——如果我们想调用的方法拥有一个名字,那我们就直接用这个名字来调用它吧。
方法引用的种类
下面列出方法引用的几种语法:
·静态方法引用ClassName::staticMethodName
·实例中的实例方法引用instanceReferenceName::methodName
·父类上的实例方法引用super::methodName
·本类上的实例方法引用ClassName::methodName
·构造方法引用Class::new
·数组构造方法引用TypeName[]::new
在类型和方法名之间,加上分隔符“::”
用一个例子融会贯通
首先看实例代码:
List people = ... Collections.sort(people,newComparator() {publicintcompare(Person x, Person y) {returnx.getLastName().compareTo(y.getLastName()); } });
看了lambda表达式的用法之后,是不是感觉冗余代码太多呢?
我们先用lambda表达式去掉冗余的匿名类,精简成一行代码:
Collections.sort(people,
(Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));
现在看起来代码是精简了很多,但是感觉抽象程度还比较差,开发人员仍然需要进行实际的比较操作,我们可以借助java.util.Comparator接口中静态方法comparing() (这也是Java1.8新增的):
Collections.sort(people,
Comparator.comparing((Person p) -> p.getLastName()));
编译器可以帮助我们做类型推导,同时还可以借助静态导入,进一步精简:
Collections.sort(people,comparing(p-> p.getLastName()));
现在看起来,就发现可以将lambda表达式用方法引用来替换:
Collections.sort(people, comparing(Person::getLastName));
使用Collections.sort()的辅助方法也不太妥当,它使代码冗余,也无法针对List接口的数据结构提供特定的高效实现,而且因为Collections.sort()方法不属于List接口,用户在阅读List接口文档的时候可能不会意识到Collections类中有提供一个针对List接口的排序方法sort(),这里可以做一步优化,我们可以为List接口添加一个default方法sort(),然后直接通过List对象调用该sort()方法:
people.sort(comparing(Person::getLastName));
这样即方便调用,也方便代码的阅读和后期维护。将最终结果对比一开始的匿名类的实现方法,是不是要更简短,但语义却更清晰了呢?这就是lambda表达式的好处。