JDK8 的 Lambda 表达式 -- 词法范围和变量捕获

Lambda 表达式的词法范围,一言以蔽之就是没有引入新的词法范围。这里的词法范围要研究的课题是 this 的指向有没有在变。我们知道在匿名类内部 this 指向的匿名类的实例,这种关系是在编译期就确定的。而在 Lambda 表达式中的 this 与外部的 this 没有差别,也就是说你可以把 Lambda 表达式当成一般的语句来看待,多简单啊,不像 JavaScript 中的 this 被搞的那么魔幻。

可以这么测试:

输出的是同一个地址里的东西:

cc.unmi.testjdk8.TestLambda@28a418fc
cc.unmi.testjdk8.TestLambda@28a418fc

接下来瞧瞧 Lambda 表达式对外层变量的捕获。Lambda 表达式的有个好处就是它是轻量级,可重用单元,并且可捕获外层变量。

如果要你说出下面的代码会有什么编译错误的话

只要有一点 Java 经验的人一眼就能看出,在匿名类的  System.out.println(name) 不能引用 non-final 的外部变量。是的,因为大概很多和我一样,经常是先收到这个错误提示,然后再把需要的外部变量改为 final 的,很少会是事先声明为 final,这是一种很奇怪的编程方式。

幸好,我们的苦处得到了体谅,这已成为了过去时,自 JDK8 起匿名类访问的外部变量不需要是 final 的。但是你依然不能去修改它的值(不管是引用类型还是原始类型,我都会把值理解为同一个概念,直接用等号赋予它的就是它的值,非内容),也就是故有约束仍然没有变。

思考一下在 JDK8 之前匿名类里引用外层的变量,则这个变量为什么要求是 final 的呢?我的理解是因为 Java 允许在匿名类内部引用外层的变量,但不希望外层的变量的值莫名其妙的在自己的内部类中被改变了,就像方法不能修改掉它的参数值一样。

进一步把匿名类中的代码想像为是在执行一个方法,则被匿名类引用的外部变量就是方法参数。对于真正的方法,你试图让方法改变参数值是合法的,但跳出方法后参数值又归位。类似的匿名类内部也是不能改变外部变量的值,另一方面又没有更好的约束办法,所以就要求外面声明为 final。

JDK8 之中被匿名类或访问的外层变量更象是个方法参数 -- 虽未显示声明为 final 的,但其实在被访问时(作为方法参数角色时)就是个 final 的类型,只是隐式的罢了。

同样的,在 JDK8 的 Lambda 表达式,如果 Lambda 表达式内部想要读取外层的变量时,也不要求那个外层的变量是 final 修饰的,例如,下面的可编译通过

上面是读取外层变量值的情况。想要改写外层变量值会出现什么情况呢?就是没门,在 JDK8 下无论是在匿名类还是 Lambda 表达式都不允许修改外层变量的值,所以下面两段代码都无法通过编译

如果你也试图去改变外层变量的值,写成下面那样的代码也是无法通过编译的

它们报的错误是一样,说的都是 names 必须是 final 的或者类似 final 的,但你要跟着错误提示走给 names 加上个 final 修饰,又会收到不能给 final 类型再赋值的错误,这是无解的,这其实是绕进去了。

不过函数式编程,或是在并发的环境之下,尽可能的去使用不可变的类型是不会错的,所以尽管 JDK8 对被访问的外层变量未显示的要求是个 final, 但在由匿名类或 Lambda 访问的那一刻它就是个隐式 final 的类型。

小结:Lambda 表达式中的 this 还是它外层的那个 this;JDK8 之前在匿名类中只能引用外部 final 的变量,JDK8 后,匿名类和 Lambda  表达式一样都可能读取外部 final 或 non-final 型变量,且都不允许改变外部变量的值。

本文链接 https://yanbin.blog/jdk8-lambda-lexical-scoping-variable-capture/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments