Lambda表达式本质
一个Lambda表达式本质上是一个匿名函数
Lambda表达式出现的原因
匿名类型最大的问题就在于其冗余的语法,而Lambda表达式提供了轻量级的语法,解决了匿名内部类代码冗余的问题。
Lambda表达式语法
lambda表达式的语法由参数列表、箭头符号->和函数体组成。函数体既可以是一个表达式,也可以是一个语句块:
- 表达式:表达式会被执行然后返回执行结果。
- 语句块:语句块中的语句会被依次执行,就像方法中的语句一样——
- return语句会把控制权交给匿名方法的调用者
- break和continue只能在循环中使用
- 如果函数体有返回值,那么函数体内部的每一条路径都必须返回值
表达式函数体适合小型lambda表达式,它消除了return关键字,使得语法更加简洁。
lambda表达式也会经常出现在嵌套环境中,比如说作为方法的参数。为了使lambda表达式在这些场景下尽可能简洁,去除了不必要的分隔符。不过在某些情况下我们也可以把它分为多行,然后用括号包起来,就像其它普通表达式一样。
栗子:
1 | (int x, int y) -> x + y |
Lambda表达式的目标类型
Lambda表达式的目标类型是由其上下文推导而来
编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导,这个被期待的类型被称为目标类型。lambda表达式只能出现在目标类型为函数式接口的上下文中。
lambda表达式对目标类型也是有要求的。编译器会检查lambda表达式的类型和目标类型的方法签名(method signature)是否一致。当且仅当下面所有条件均满足时,lambda表达式才可以被赋给目标类型T:
- T是一个函数式接口
- lambda表达式的参数和T的方法参数在数量和类型上一一对应
- lambda表达式的返回值和T的方法返回值相兼容(Compatible)
- lambda表达式内所抛出的异常和T的方法throws类型相兼容
由于目标类型(函数式接口)已经“知道”lambda表达式的形式参数(Formal parameter)类型,lambda表达式的参数类型可以从目标类型中得出。
栗子:
1 | ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers()); |
Lambda表达式词法作用域
在内部类中使用变量名(以及this)非常容易出错。内部类中通过继承得到的成员(包括来自Object的方法)可能会把外部类的成员掩盖(shadow),此外未限定(unqualified)的this引用会指向内部类自己而非外部类。
相对于内部类,lambda表达式的语义就十分简单:它不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。lambda表达式基于词法作用域,也就是说lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括lambda表达式的形式参数)。此外,’this’关键字及其引用在lambda表达式内部和外部也拥有相同的语义。
也就是说在内部类中的 this 是有可能有歧义的,而Lambda表达式中的 this 和表达式外部 this 予以相同
1 | public class Hello { |
变量捕获
在Java SE 7中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:如果捕获的变量没有被声明为final就会产生一个编译错误。对于lambda表达式和内部类,允许在其中捕获那些符合有效只读(Effectively final)的局部变量,尽管放宽了对捕获变量的语法限制,但试图修改捕获变量的行为仍然会被禁止。
简单的说在内部类中捕获的外部变量必须是final修饰的变量,儿Lambda表达式内捕获外部变量时不要求变量的final属性,但是依然不能修改。
栗子:
1 | Callable<String> helloCallable(String name) { |
方法引用
方法引用和lambda表达式拥有相同的特性,不过我们并不需要为方法引用提供方法体,我们可以直接通过方法名称引用已有方法。
1 | class Person { |
方法引用的种类
方法引用有很多种,它们的语法如下:
- 静态方法引用:ClassName::methodName
- 实例上的实例方法引用:instanceReference::methodName
- 超类上的实例方法引用:super::methodName
- 类型上的实例方法引用:ClassName::methodName
- 构造方法引用:Class::new
- 数组构造方法引用:TypeName[]::new