当我们第一次提到Lambda表达式时,说它可以为函数式接口生成一个实例。
然而,Lambda表达式本身并不包含它在实现哪个函数式接口的信息。
为了全面了解Lambda表达式,应该知道Lambda的实际类型是什么。

一、类型检查

Lambda的类型是从使用Iambda的上下文推断出来的。
上下文中(比如,接受它传递的方法的参数,或接受它的值的局部变量)Lambda表达式需要的类型称为目标类型。
通过一个例子,看看使用Lambda表达式时背后发生了什么。
图3-4概述了下列代码的类型检查过程。
H(8P2YHW$CM~%L@(K(5UJ@X.png
类型检查过程可以分解为如下所示。.
1)首先,要找出filter方法的声明。
2)其次,要求它是Predicate (目标类型)对象的第二个正式参数。
3)然后,Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
4)然后,test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
5)最后,filter的任何实际参数都必须匹配这个要求。
这段代码是有效的,因为我们所传递的Lambda表达式也同样接受Apple为参数,并返回一个boolean。
请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语向也必须与之匹配。

二、同样的Lambda不同的函数式接口

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要它们的抽象方法签名能够兼容。
比如,Callable和PrivilegedAction,这两个接口都代表着什么也不接受且返回个泛型T的函数。
因此,下面两个赋值是有效的:

  1. Callable<Integer> c = () -> 42;
  2. PrivilegedAction<Integer> p = () -> 42;

这里,第一个赋值的目标类型是Callable,第二个赋值的目标类型是PrivilegedAction
`KA_@KV}V77H(AQJ4Q9CRWX.png
]59~$AP9C)N_COH}}KC9W7Q.png

三、类型推断

还可以进一步简化你的代码。
Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。
这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。
换句话说,Java编译器会像下面这样推断Lambda的参数类型:
$LK8JHP)POV%FZ4XKFAU[O4.png](https://cdn.nlark.com/yuque/0/2021/png/12773302/1633577281581-cfdc2c2a-c184-4028-9ca6-ea7da90acf5b.png#clientId=uffc3f8dc-03bc-4&from=drop&id=IHsSd&margin=%5Bobject%20Object%5D&name=%24LK8JHP%29POV%25FZ4XKFAU%5BO4.png&originHeight=89&originWidth=988&originalType=binary&ratio=1&size=18343&status=done&style=none&taskId=u51a4219d-fbc3-4bc6-8190-3798875e540)<br />请注意,当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略。<br />Lambda表达式有多个参数,代码可读性的好处就更为明显。<br />例如,可以这样来创建一个Comparator对象:<br />![@})JJ]YI@KVTXUIYP0@NZCT.png

四、使用局部变量

我们迄今为止所介绍的所有Lambda表达式都只用到了其主体里面的参数。
但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。
它们被称作捕获Lambda。
例如,下面的Iambda捕获了portNumber变量:

  1. int portNumber = 1337;
  2. Runnable r = () -> System. out . pr intln (portNumber) ;

尽管如此,还有一点点小麻烦:关于能对这些变量做什么有一些限制。
Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。
但局部变量必须显式声明为final,或事实上是final。
换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。)
例如,下面的代码无法编译,因为portNumber变量被赋值两次:
ZG7)L3N3ICXCP86WX]4A]7S.png

对局部变量的限制

你可能会问,为什么局部变量有这些限制。
第一,实例变量和局部变量背后的实现有一个关键不同。
实例变量都存储在堆中,而局部变量则保存在栈上。
如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程可能会在分配该变量的线程将这个变量收回之后,去访问该变量。
因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。
如果局部变量仅仅赋值一次那就没有什么区别了,因此就有了这个限制。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)。