Java中Lambda表达式之Lambda语法与作用域解析
接上一篇:初探Lambda表达式/Java多核编程【2】并行与组合行为
本节是第二章开篇,前一章已经浅显地将所有新概念点到,书中剩下的部分将对这些概念做一个基础知识的补充与深入探讨实践。
本章将介绍Lambda表达式基础知识。
前言
把上一张书中的结语放到这里作为本章学习内容的开头,以此来概括Lambda表达式的优点:
- 提升性能、自动的并行化
- 更棒的API(
comparing(...)
细粒度的方法将成为标准) - 编码风格得到改进、代码简化
反观前面几篇文章中的代码实践,以上三个优点全部得到了验证。
Lambda语法
前文中我们已经提到,Java中无法声明独立的纯函数,但是Lambda的出现提供了一种与独立函数更为近似的实现方式。就只看Lambda形式,的确与很多精简语法的脚本语言中所声明的函数高度相似:
# CoffeeScript eat = (x) -> alert("#{x} has been eatten!")
总之光看上去就像那么回事:)
那么Lambda表达式的语法又是怎样的呢?
- 参数列表
- Lambda体
两部分之间使用->分割,看几个例子:
p -> p.translate(); i -> new Point(); (a, b) -> return a + b; () -> "Ha!"; (x, y, z) -> { x += y; y += z; z += x; }
箭头左边接收任意数量的参数,右边则为表达式体,描述所需的行为。
显而易见,在一般情况下无需显式地指定参数类型,除非上下文的信息无法是编译器推断出相应的类型:
(int x, int y) -> x + y;
参数可以声明为final,也可以添加注解(@Nullable, etc.)。
表达式体部分可以为方法的调用,如str.length()
等等,也可以是表达式,如加减乘除等等,即“语句Lambda”与“表达式Lambda”这两种形式。
另外关于返回值,有则用return sth_to_return
;,没有则用return;或直接不写返回语句。
最后,需要注意的是Lambda表达式不需要也不允许使用throws关键字来声明可能产生并需要向上抛出的异常。
Lambda与匿名内部类
前几篇文章中常常将Lambda与匿名内部类做粗浅的类比与对比,现在我们将就这一点做具体深入的分析。
语法
首先在语法层面,Lambda表达式有时候被称为匿名内部类的“语法糖”,这表明了二者之间存在语法繁简的明显区别。
无标识性问题
其次便是标识性问题,我们知道Java中为了区分对象,每一个对象(即使是匿名内部类的实例)都具有唯一标识,而依赖于对象而存在的行为(即我们所说的方法)也会与此标识相关联。
例如:
String bar = "bar"; String foo = "foo"; System.out.println(bar.hashCode()); // => 97299 System.out.println(foo.hashCode()); // => 101574
但是对于Lambda表达式而言,情况便不是如此的明朗,根据具体情况的不同,Lambda自身可能拥有标识也可能没有。
况且,Lambda为的就是表示一种行为,趋向于纯函数,因此一般情况下是不需要使用标识加以区分的。
作用域规则
再者就是两者的作用域大小的区别。
对于匿名内部类而言,显而易见,在类内可以沿用父类型(即函数接口)的名字。
而对于Lambda,则不能。
我们用Runnable接口来举一个例子:
public interface LetsRun extends Runnable { String aString = "Big brother is watching."; } new Thread( new LetsRun() { @Override public void run() { System.out.println(aString); } }).run();
显然,匿名内部类能够直接沿用我们在LetsRun这个函数式接口中声明的aString。
写完这段代码的同时,IDE给了我一个可以将匿名内部类折叠为Lambda的提示,现在就让它帮我们自动折叠一下:
new Thread((LetsRun) () -> System.out.println(LetsRun.aString)).run();
注意此时需要打印的内容也同时自动变成了LetsRun.aString
,印证了上述特征,即Lambda不能直接访问父类型中的名字。
关于对外部变量的访问(后面书中将此称为“变量捕获”),不论是匿名内部类还是Lambda,对于域外部变量的权限都是有限的。
在匿名内部类中,可以读取外部量,但是不允许有修改变量的倾向。
也就是说,没有严格的限制规定被访问的外部量必须被声明为final:
// This is OK String anotherString = "WAR IS PEACE / FREEDOM IS SLAVERY / IGNORANCE IS STRENGTH"; // Also OK final String finalString = "Nineteen Eighty-Four"; new Thread(new LetsRun() { @Override public void run() { System.out.println(aString + "\n" + anotherString + "\n" + finalString); } }).run();
倘若一旦在方法内修改了anotherString的值,编译就无法通过。
同样,折叠为Lambda后,依然是合法的:
new Thread((LetsRun) () -> System.out.println(LetsRun.aString + "\n" + anotherString + "\n" + finalString)).run();
关于变量捕获的问题是下一小节的重点内容,在此暂时不做深究。
Lambda表达式在定义时,参数部分与表达式体内的命名可以暂时屏蔽掉字段的名称:
public class Foo { String x, y; BinaryOperator binaryOperator = (x, y) -> x.hashCode() + y.hashCode(); // ... }
另外,Lambda相当于语句块,因此表达式体内持有和外部相同的语境,即this与super拥有相同含义:
public class MySuperClass { public static final String aString = "Father"; } public class MyClass extends MySuperClass { public static final String aString = "Son"; public void aMethod() { new Thread((LetsRun) () -> { System.out.println("--- Lambda ---"); System.out.println(super.aString); System.out.println(this.aString); }).run(); System.out.println("--- Outside ---"); System.out.println(super.aString); System.out.println(this.aString); } }
运行结果:
--- Lambda --- Father Son --- Outside --- Father Son
Lambda无法引用自身,因此可以用一种尴尬的方式递归调用自己:
intUnaryOperator = i -> i == 0 ? 1 : i * intUnaryOperator.applyAsInt(i - 1);
小结
Lambda不从父类型中继承任何名字,包括:
接口的静态final字段
接口的静态嵌套类
默认方法(将在后续介绍)
将全部被排除在作用域之外。
Lambda参数与表达式体中的局部声明可以屏蔽字段名。
Lambda中的this和super的含义完全同外部一致。
而若在匿名内部类访问外部对象的当前实例须用OuterClass.this,非常笨拙:
new Thread((LetsRun) () -> System.out.println(Foo.this.getClass().toString()) ).run();
递归Lambda时须注意Lambda变量无法被初始化,只能直接调用相应函数式接口中的方法。
本章代码:
Foo.java
import java.util.function.BinaryOperator; public class Foo { String x, y; BinaryOperator binaryOperator = (x, y) -> x.hashCode() + y.hashCode(); public static void main(String[] args) { String bar = "bar"; String foo = "foo"; System.out.println(bar.hashCode()); System.out.println(foo.hashCode()); new Thread((LetsRun) () -> System.out.println(LetsRun.aString)).run(); String anotherString = "WAR IS PEACE / FREEDOM IS SLAVERY / IGNORANCE IS STRENGTH"; final String finalString = "Nineteen Eighty-Four"; new Thread((LetsRun) () -> System.out.println(LetsRun.aString + "\n" + anotherString + "\n" + finalString)).run(); new MyClass().aMethod(); new Foo().accessOuterClassInAnnoymousInnerClass(); } public void accessOuterClassInAnnoymousInnerClass() { new Thread((LetsRun) () -> System.out.println(Foo.this.getClass().toString()) ).run(); } }
LetsRun.java
public interface LetsRun extends Runnable { String aString = "Big brother is watching."; }
MyClass.java
import java.util.function.IntUnaryOperator; public class MyClass extends MySuperClass { public static final String aString = "Son"; IntUnaryOperator intUnaryOperator = null; public void aMethod() { new Thread((LetsRun) () -> { System.out.println("--- Lambda ---"); System.out.println(super.aString); System.out.println(this.aString); }).run(); System.out.println("--- Outside ---"); System.out.println(super.aString); System.out.println(this.aString); } public void factorial() { intUnaryOperator = i -> i == 0 ? 1 : i * intUnaryOperator.applyAsInt(i - 1); } }
MySuperClass.java
public class MySuperClass { public static final String aString = "Father"; }
以及运行结果:
97299 101574 Big brother is watching. Big brother is watching. WAR IS PEACE / FREEDOM IS SLAVERY / IGNORANCE IS STRENGTH Nineteen Eighty-Four --- Lambda --- Father Son --- Outside --- Father Son class Foo
以上所述是小编给大家介绍的Java中Lambda表达式之Lambda语法与作用域解析,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!