3.5 类型检查、类型推断以及限制
Lambda表达式可以为函数式接口生成一个实例,实际的类型与映射接口的类型相同。
3.5.1 类型检查
Lambda表达式的目标类型是从使用Lambda的上下文推断出来的。
其中上下文是指:
- 传递方法的参数;
- 赋值的局部变量;
类型检查过程可以分解为如下所示:
- 查看上下文,例如传递给filter方法的第二个参数;
- 匹配目标类型,例如参数为Predicate<Apple>;
- 匹配抽象方法,例如函数式接口Predicate<Apple>的抽象方法是test;
- 查看函数描述符,例如Predicate<Apple>#test的函数描述符为
Apple - > boolean
; - 匹配函数描述符,例如(Apple a) -> a.getWeight() > 150的函数描述符也为
Apple - > boolean
;
类型检查过程完成,匹配成功。
注意:若目标类型的方法声明了受检异常,则Lambda表达式throws语句需相应匹配,如测验3.4:函数式接口所示。
3.5.2 同样的Lambda,不同的函数式接口
Lambda表达式与函数描述符匹配,而函数描述符依赖于函数式接口的抽象方法。
因此当抽象方法声明兼容时,同一个Lambda表达式可以关联不同的函数式接口。
public interface PrivilegedAction<T> {
T run();
}
Callable<Integer> c = () -> 42;
PrivilegedAction<Integer> p = () -> 42;
Comparator<Apple> c1 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
联想到了鸭子类型。
鸭子类型(英語:duck typing)在程序设计中是动态类型的一种风格。 在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由「当前方法和属性的集合」决定。
Java 7中已经引入了菱形运算符(<>),利用泛型推断从上下文推断类型,一个类实例表达式可以出现在不同的上下文中。
List<String> listOfStrings = new ArrayList<>();
List<Integer> listOfIntegers = new ArrayList<>();
如果一个Lambda的主体是一个语句表达式,既可以返回执行结果类型,又可以兼容void。
// Predicate返回了一个boolean
Predicate<String> p = s -> listOfStrings.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> listOfStrings.add(s);
测验3.5:类型检查——为什么下面的代码不能编译呢?
你该如何解决这个问题呢?
Object o = () -> {
System.out.println("Tricky example");
};
当前上下文为Object,而Lambda表达式的函数描述符为() -> {}
,需要一个函数式接口,因此可以将目标类型改为Runnable。
Runnable r = (() -> {
System.out.println("Tricky example");
});
r.run();
// 表述目标类型更改Runnable,实际不建议使用
Object o = (Runnable) (() -> {
System.out.println("Tricky example");
});
((Runnable)o).run();
3.5.3 类型推断
Java编译器根据上下文匹配抽象方法和Lambda表达式,因此可以根据抽象方法的签名推断Lambda表达式的函数描述符。
Lambda语法可以省去标注参数类型,当只有一个参数时还可以省略括号,代码更加简化。
List<Apple> greenApples = filter(apples, apple -> "green".equals(apple.getColor()));
Comparator<Apple> c =
(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
我试了下,同一个lambda表达式不能混用没有类型推断和类型推断。
// Cannot resolve symbol 'a2'
Comparator<Apple> c =
(a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
有时候显式写出类型更易读,有时候去掉它们更易读。程序员根据团队规范和个人习惯选择代码更易读的方式。
3.5.4 使用局部变量
与匿名类一样,Lambda表达式允许使用变量,被称作捕获Lambda。
例如,下面的Lambda捕获了portNumber变量:
int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
但对局部变量有限制:必须显示声明为final,或者事实上为final。
否则会导致编译器报错
int portNumber = 1337;
portNumber = 31337; // Variable used in lambda expression should be final or effectively final
Runnable r = () -> System.out.println(portNumber);
- 局部变量分配在栈,实例变量分配在堆,堆是在线程之间共享的,而栈隐式地表示线程独占,当调用栈被回收,而Lambda表达式/匿名类获取局部变量时,会引发线程不安全问题,因此限制局部变量事实final;(JVM逃逸分析,将本要分配到堆的对象转换为局部变量分配在栈)
- 存在栈被回收,Lambda表达式/匿名类获取局部变量执行的情况,因此希望局部变量保持不变,仅限于避免引发线程不安全获取局部变量的副本值执行;
- 不鼓励使用改变外部变量的命令式编程模式,易产生并发问题;
闭包(closure):一个函数的实例,可以无限制地访问和修改这个函数的非本地变量。
Java 8的匿名类和Lambda表达式对值封闭,而不是对变量封闭。可以作为参数传递给方法,并且访问其作用域之外的变量,但不能修改Lambda表达式中的局部变量。