前言
我看了了一些 Lambda 表达式的文章,发现有些文章讲的很好,比如:
- 如果想入门 Lambda 表达式,则可以首先看《Java核心技术》作者 Horstmann 写的 Lambda Expressions in Java 8,中文版请看这里JAVA 8:LAMBDA表达式(一), JAVA 8:LAMBDA表达式(二) 和 JAVA 8:LAMBDA表达式(三)
- Java 8 Tutorial 也清晰地介绍了 Lambda 表达式
- 如果想系统学习 Lambda 表达式,则可以看 State of the Lambda(中文版请点这里)、State of the Lambda: Libraries Edition、Translation of Lambda Expressions
我也不想重复造轮子,因此本篇文章的侧重点在于总结 Lambda 表达式的核心概念以及使用 Lambda 表达式的场景举例。
Functional Interface
只包含一个方法的接口称为 Functional Interface(这里的只包含一个方法
不是只有一个方法,因为可能有 default 方法或 static 方法(这些后面会介绍),这里指的是只有一个普通抽象方法)。
|
比如 Comparator 接口:
|
解释:
@FunctionalInterface
: 此注解只是为了表明 Comparator 接口是一个 Functional Interface(不加也没关系),当创建的接口违反了要成为 Functional Interface 的规则,则会编译错误,因此这个注解只是为了验证代码的正确性而已。因此如果你以后自己定义了一个 Functional Interface,建议在最开头写上注解@FunctionalInterface
。- Comparator 接口只新定义了一个普通抽象方法,就是
int compare(T o1, T o2);
其他方法要么是default
或static
方法(我们发现 Comparator 接口既有 default 方法,又有 static 方法,这些后面会介绍,这是 Java8 对于接口的新特性),要么是把继承自 Object 类的方法重写了一下(equals
方法) Function<? super T, ? extends U>
: Function 接口是 Java8 新增的 Functional Interface 之一(java.util.function
包中包含了很多内置的 Functional Interface),Function<T,R>
提供了R apply(T t)
函数,即给定一个输入t,经过一系列转换返回一个R实例(可以想象成一个名为apply
的盒子,输入是T类型的实例,输出是R类型的实例,具体盒子的实现可以自定义)。comparing
函数中加了泛型。其中<? super T, ? extends U>
表示 Function 中的 A 必须要是 T 的父类,B 必须是 U 的子类。
default 方法
default 方法的出现是为了降低未来对于已有接口修改的难度,即如果想对一个已有接口新加一个或多个方法,因为以前不允许在接口中加入函数实现,因此你首先必须要在接口中定义一些抽象方法,并且在实现该接口的所有类中都实现新加的方法,很麻烦对不对(比如世界上有千千万万 Java 程序员使用了 Comparator 接口,如果新版本 JDK 在 Comparator 中新定义了一个抽象方法,那是毁灭性的!千千万万的程序员就要崩溃了,估计用 Java 的人也会大大减少…);default 帮你解决这个问题,你如果想在接口中新加方法,并且不想影响实现该接口的类,只需要将他声明为 default 并实现他即可,当然实现该接口的类也可以 Overwrite 该 default 方法,当然也可以什么都不做(就像 Comparator 接口中也新加了很多 default 方法,你完全不需要改动写过的现有代码)’
这里举个例子说明 default 在 JDK 中的应用:
- List 接口中新增了 sort 方法,自从有了这个方法,就不需要调用
Arrays.sort()
方法了。对 ArrayList 类型的实例 list 排序,只调用list.sort()
即可。 - Iterable 接口中新增了
forEach
方法,我们可以直接使用list.forEach()
对每个元素做一些操作,不必再自己 for 循环了。
static 方法
Java8 开始在接口中能够声明静态方法,这里举个例子说明 static 方法出现的原因:想在接口中定义一些辅助方法,比如原本创建一个实现 Comparator 接口的对象需要 new,现在只需要 Comparator.comparing()
方法就可以创建这样一个对象,很方便。
Lambda 表达式初窥
Lambda 表达式基本形式
Lambda 表达式以 (ParamList) -> { 函数体 }
或 (ParamList) -> 单个表达式
形式存在。前者等价于创建一个实现某个 Functional Interface 的对象,并实现该接口的方法,(ParamList)
必须和 Functional Interface 需要实现的方法的参数一一对应,{ 函数体 }
就是实现方法的函数体。当接口方法的函数体中代码很简单,只需要一个 return 语句就可以搞定时,使用 Lambda 表达式的后者表现形式更简洁,单个表达式
就是 return 语句,只是少了 return 这个单词。
当
(ParamList)
中只有一个参数时,()
可省略,即(Param)
等价于Param
例子:
为了创建一个用于整数之间比较的 Comparator 对象,需要以下6行代码:
|
实际上用 Lambda 表达式1行解决:
|
Lambda 表达式是一个匿名方法,为了解决实现 Functional Interface 的匿名内部类太过麻烦的问题(本来1行代码能解决的问题用了6行)
Target Type
Lambda 表达式有一个 Target Type 的概念,意思是这个 Lambda 表达式将返回的类型。同一个 Lambda 表达式在不同的上下文中的 Target Type 是不一样的(Target Type 是通过类型推导得出),比如:
|
和
|
虽然 Lambda 表达式一样,但是前者的 Target Type 是 Callable,后者的 Target Type 是 PrivilegedAction。
我们也能够通过强制类型转换的方式帮助编译器确定 Target Type:
|
Lambda 和使用匿名内部类实现的区别
在 Lambda 表达式中使用 this 关键字表示的意思是外部类,而内部类中使用 this 关键字表示的意思是内部类。Lambda 表达式虽然看上去像内部类,但是本身并不依赖外部类,是独立存在的(内部类的生存依赖于外部类,从内部类的命名本身就能看出)
比如:
|
r1 和 r2 都是输出 “Hello”
,r3 输出 Hello$1@5b89a773
。可以看出 Lambda 表达式和一般匿名内部类有区别。
接下去再举一个例子:
|
可以看出 Lambda 表达式的 block statement 的 scope 和外部类是一个 Level 的,而内部类不是。
Java 是 Lexical Scoping 的,程序语言可能有两种 scoping 方式:Lexical Scoping 和 dynamic Scoping。
|
在上面的代码中,如果语言是 lexical scoping 的,则f()
函数中不能访问g()
的局部变量a
,如果是 dynamic scoping 的,则f()
函数能够访问g()
的局部变量a
。
Effectively Final 变量
原本如果内部类要使用外部的变量,则外部变量一定要定义成 final 的,Java8 以后,如果 Lambda 表达式和内部类要使用外部的变量,则不需要一定定义成 final 的,只需要是 “Effectively final” 即可,意思是你虽然不定义成 final,但是你实际上是 final 的,即使你加了个 final 也不会编译错误。
|
和原来的 final 变量一样,如果要向 final ArrayList arr
的 arr 变量中加入几个元素,这是允许的,但是要改变 arr 的引用,这是不允许的。
Method Reference
这是 Lambda 表达式的特殊情况,当接口实现方法的函数体中只包含 return 语句,并且 return 语句只是调用了其他方法,没有其他额外的代码,则这时候我们”可能”可以更加简化语法。
Method Reference 的语法有很多种,因为方法有静态方法、常规方法、构造方法、父类的常规方法等,对于不同的方法,Method Reference 的语法也是不同的。
- 静态方法:ClassName::MethodName
- 实例上的一般方法:instance :: MethodName
- 超类上的方法:super::MethodName
- 任意对象的一般方法:ClassName::MethodName
- 类的构造函数:ClassName::new
- 数组构造函数:TypeName[]::new
比如
|
等价于
|
我们能够看到上面的 Method Reference 方式(这里是任意对象的一般方法)比 Lambda 表达式更简单(虽然不那么容易理解),那么问题来了,compareTo 怎么连参数都没有指定,那怎么确定啊?其实 Method Reference 有个规则:
functional interface 的方法的参数一一按顺序对应于 Method Rerference 的方法参数。
因此 Integer::compareTo
等价于 o1.compareTo(o2)
。
个人观点:Method Reference 语法比较晦涩,而且给别人看也不太好理解,所以我觉得用 Lambda 表达式也挺好…是不是太没追求了…
例子
这里就以一个例子讲解下 Lambda 表达式。
|
上面给了5种方法创建一个 Comparator 对象,目的是对多个 Person 排序,比较的 key 是 name 属性。
- 第一种方法是 Java8 以前的方法。
- 第二种方法是使用 Lambda 表达式,
(Person o1, Person o2)
表示实现方法的参数;o1.name.compareTo(o2.name)
等价于return o1.name.compareTo(o2.name)
; - 第三种编译通过的原因是因为根据类型推导,编译器能够知道 Lambda 表达式的类型是 Comparator\
,因此方法参数 Person 不写,编译器也知道。 - 第四种方法使用了 Java8 中 Comparator 接口新加的 static 方法
comparing
,参数接收一个Function<T,R>
对象,根据Comparator<Person>
得知T
就是Person
,根据p.name
得知R
就是String
- 第五种方法使用了 Method Reference