初识Lambda已经大致了解Lambda的使用。
其实可以把Java Lambda表达式简单地看做是一个匿名方法,所以他的语法构成就很像一个方法的定义。
public class Calculator {
interface IntegerMath {
int operation(int a, int b);
}
public int operateBinary(int a, int b, IntegerMath op) {
return op.operation(a, b);
}
public static void main(String... args) {
Calculator myApp = new Calculator();
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
System.out.println("40 + 2 = " + myApp.operateBinary(40, 2, addition));
System.out.println("20 - 10 = " + myApp.operateBinary(20, 10, subtraction));
}
}
addition
和subtraction
就可以看做是IntegerMath
省略了operation
名称的方法。
表达式组成
- 括号以及括号里用逗号分隔的参数列表
仅有一个参数的可以省略括号。
- ->符号
- 花括号以及花括号里的语句
仅有一条语句时可以省略花括号,并且这条语句的值将作为return返回值。
更严谨的语法规范可以参考Java8语言规范:Lambda表达式。
作用域
Lambda的作用域与类和其它匿名类基本一致,也可以进行变量捕捉,但是Lambda不存在类和匿名类的Shadowing问题。
import java.util.function.Consumer;
public class LambdaScopeTest {
public int x = 0;
class FirstLevel {
public int x = 1;
void methodInFirstLevel(int x) {
Consumer<Integer> myConsumer = (y) -> {
System.out.println("x = " + x);
System.out.println("y = " + y);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " +
LambdaScopeTest.this.x);
};
myConsumer.accept(9);
}
}
public static void main(String... args) {
LambdaScopeTest st = new LambdaScopeTest();
LambdaScopeTest.FirstLevel fl = st.new FirstLevel();
fl.methodInFirstLevel(23);
}
}
执行结果:
x = 23
y = 9
this.x = 1
LambdaScopeTest.this.x = 0
Lambda可以访问LambdaScopeTest
的成员变量、FirstLevel
的成员变量、methodInFirstLevel
的参数变量。如果使用Consumer
的匿名类,那么内部类FirstLevel
成员变量无法直接访问;如果匿名类方法的参数与外部的局部作用域某个变量同名,那么局部作用域的同名变量同样无法访问。
Consumer<Integer> a = new Consumer<Integer>() {
@Override
public void accept(Integer x) {
System.out.println("x = " + x);
// System.out.println("this.x = " + this.x); 编译错误,无法访问
System.out.println("LambdaScopeTest.this.x = " + LambdaScopeTest.this.x);
}
};
在局部作用域中,Lambda表达式的参数列表是不能与局部作用域中的变量同名,因为Lambda没有像匿名类那样产生新的类作用域。
在myConsumer这个表达式中定义参数名称为x
void methodInFirstLevel(int x) {
Consumer<Integer> myConsumer = (x) -> {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("LambdaScopeTest.this.x = " + LambdaScopeTest.this.x);
};
myConsumer.accept(9);
}
由于参数列表中变量名称与局部作用域变量同名,就产生一个编译错误:
variable x is already defined in method methodInFirstLevel(int)
如何确定表达式类型
可以把表达式赋值给指定类型的变量
IntegerMath addition = (a, b) -> a + b;
IntegerMath subtraction = (a, b) -> a - b;
或者直接作为参数传入调用方法
myApp.operateBinary(40, 2, (a, b) -> a + b)
addition
和operateBinary
方法中的参数都需要一个IntegerMath
类型,变量或者方法参数期待得到的类型称作目标类型(Target Type)。编译器可以通过上下文环境确定Lambda表达式的类型,上述就是通过表达式中的参数变量定义和返回值推导表达式的类型。
编译器通过以下上下文识别Lambda表达式类型:
- 变量声明
- 赋值
- return语句
- 数组初始化
- 方法或构造方法参数
- Lambda表达式体
- 条件语句?:
- 类型转换
对于在方法参数中出现的表达式,编译器需要在以下两个特性中确定表达式目标类型:
- 方法重载
- 类型参数接口
invoke方法重载,一个参数为Runnable另一个为Callable
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
假如传入一个Lambda表达式调用invoke
String s = invoke(() -> "done");
那么这个时候到底是调用invoke(Runnable)
还是invoke(Callable)
。
最终会调用invoke(Callable)
,因为Runnable.run
没有返回值。所以表达式() -> "done"
的类型为Callable<T>
。
Lambda表达式序列化
目标类型和捕捉到的参数是可序列化的,那么Lambda也就可以序列化。不过和匿名类一样,Lambda序列化的坑不小。