3.5 类型检查、类型推断以及限制

Lambda表达式可以为函数式接口生成一个实例,实际的类型与映射接口的类型相同。

3.5.1 类型检查

Lambda表达式的目标类型是从使用Lambda的上下文推断出来的。

其中上下文是指:

  1. 传递方法的参数;
  2. 赋值的局部变量;
Lambda表达式类型检查

类型检查过程可以分解为如下所示:

  1. 查看上下文,例如传递给filter方法的第二个参数;
  2. 匹配目标类型,例如参数为Predicate<Apple>;
  3. 匹配抽象方法,例如函数式接口Predicate<Apple>的抽象方法是test;
  4. 查看函数描述符,例如Predicate<Apple>#test的函数描述符为Apple - > boolean
  5. 匹配函数描述符,例如(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);
  1. 局部变量分配在栈,实例变量分配在堆,堆是在线程之间共享的,而栈隐式地表示线程独占,当调用栈被回收,而Lambda表达式/匿名类获取局部变量时,会引发线程不安全问题,因此限制局部变量事实final;(JVM逃逸分析,将本要分配到堆的对象转换为局部变量分配在栈)
  2. 存在栈被回收,Lambda表达式/匿名类获取局部变量执行的情况,因此希望局部变量保持不变,仅限于避免引发线程不安全获取局部变量的副本值执行;
  3. 不鼓励使用改变外部变量的命令式编程模式,易产生并发问题;

闭包(closure):一个函数的实例,可以无限制地访问和修改这个函数的非本地变量。

Java 8的匿名类和Lambda表达式对值封闭,而不是对变量封闭。可以作为参数传递给方法,并且访问其作用域之外的变量,但不能修改Lambda表达式中的局部变量。

posted @ 2023-06-28 17:43  夜是故乡明  阅读(9)  评论(0编辑  收藏  举报