主题3:目标类型

目标类型就是lambda表达式实例所要被赋予的类型,通常是接口类型;

类型推导:

    函数式接口的名称并不是 lambda 表达式的一部分。那么问题来了,对于给定的 lambda 表达式,它的目标类型是什么?答案是:它的类型是由其上下文推导而来。例如,下面代码中的 lambda 表达式类型是 ActionListener:

ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());

这也意味着同样的lambda表达式在不同的上下文中,可以作为不同的类型

Callable<String> c = () -> "done"; //Callable类型

PrivilegedAction<String> a = () -> "done"; //PrivilegedAction类型

    编译器负责推导 lambda 表达式类型。它利用 lambda 表达式所在上下文 所期待的类型 进行推导,这个 被期待的类型 被称为 目标类型lambda 表达式只能出现在目标类型为函数式接口的上下文中

lambda作为目标类型的要求:

    lambda 表达式对目标类型也是有要求的。编译器会检查 lambda 表达式的类型和目标类型的方法签名(method signature)是否一致。当且仅当下面所有条件均满足时,lambda 表达式才可以被赋给目标类型 T:

● T 是一个函数式接口

● lambda 表达式的参数和 T 的方法参数在数量和类型上一一对应

● lambda 表达式的返回值和 T 的方法返回值相兼容(Compatible)

● lambda 表达式内所抛出的异常和 T 的方法 throws 类型相兼容

补充:

lambda 表达式并不是第一个使用类型推导的Java 表达式:泛型方法调用和“菱形”构造器调用也通过目标类型来进行类型推导:

List<String> ls = Collections.emptyList();

List<Integer> li = Collections.emptyList();

Map<String, Integer> m1 = new HashMap<>();

Map<Integer, String> m2 = new HashMap<>();

目标类型上下文:

    就是lambda表达式的使用场景;下面给出带有目标类型的上下文:

赋值、return:

    目标类型就是被赋值或返回的类型:

Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);
public Runnable toDoLater() {
   return () -> {
     System.out.println("later");
   }
}

数组初始化器:

    数组初始化器和赋值类似,只是这里的“变量”变成了数组元素,而类型是从数组类型中推导得知。

Runnable[] runnables = new Runnable[]{
         ()-> System.out.println(1),
         ()-> System.out.println(2),
         ()-> System.out.println(3)
};

方法和构造方法的参数:

    方法参数的类型推导要相对复杂些:目标类型的确认会涉及到其它两个语言特性:重载解析(Overload resolution)和参数类型推导(Type argument inference)。

    重载解析会为一个给定的方法调用(method invocation)寻找最合适的方法声明(method declaration)。由于不同的声明具有不同的签名,当 lambda 表达式作为方法参数时,重载解析就会影响到 lambda 表达式的目标类型。编译器会通过它所得知的信息来做出决定。如果 lambda 表达式具有 显式类型(参数类型被显式指定),编译器就可以直接 使用lambda 表达式的返回类型;如果lambda表达式具有 隐式类型(参数类型被推导而知),重载解析则会忽略 lambda 表达式函数体而只依赖 lambda 表达式参数的数量。

    如果在解析方法声明时存在二义性(ambiguous),我们就需要利用转型(cast)或显式 lambda 表达式来提供更多的类型信息。如果 lambda 表达式的返回类型依赖于其参数的类型,那么 lambda 表达式函数体有可能可以给编译器提供额外的信息,以便其推导参数类型。

List<Person> ps = ...

Stream<String> names = ps.stream().map(p -> p.getName());

    在上面的代码中,ps 的类型是 List<Person>,所以 ps.stream() 的返回类型是 Stream<Person>。map() 方法接收一个类型为 Function<T, R> 的函数式接口,这里 T 的类型即是 Stream 元素的类型,也就是 Person,而 R 的类型就map方法的返回类型,是未知的。由于在重载解析之后 lambda 表达式的目标类型仍然未知,我们就需要推导 R 的类型:通过对 lambda 表达式函数体进行类型检查,我们发现函数体返回 String,因此 R 的类型是 String,因而 map() 返回 Stream<String>。绝大多数情况下编译器都能解析出正确的类型,但如果碰到无法解析的情况,我们则需要:

● 使用显式 lambda 表达式(为参数 p 提供显式类型)以提供额外的类型信息

● 把 lambda 表达式转型为 Function<Person, String>

● 为泛型参数 R 提供一个实际类型。

(Stream<String> names = ps.stream().<String>map(p -> p.getName()))

lambda 表达式函数体:

    lambda 表达式本身也可以为它自己的函数体提供目标类型,也就是说 lambda 表达式可以通过外部目标类型推导出其内部的返回类型,这意味着我们可以方便的编写一个返回函数的函数:

Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };

条件表达式(? :):

条件表达式可以把目标类型“分发”给其子表达式:

Callable<Integer> c = flag ? (() -> 23) : (() -> 42);

转型(Cast)表达式:

转型表达式(Cast expression)可以显式提供 lambda 表达式的类型,这个特性在无法确认目标类型时非常有用:

// Object o = () -> { System.out.println("hi"); }; 这段代码是非法的

Object o = (Runnable) () -> { System.out.println("hi"); };

   当重载的方法都拥有函数式接口作为参数时,转型可以帮助解决重载解析时出现的二义性。

posted @ 2020-01-19 14:36  偷懒的绿叶  阅读(357)  评论(0编辑  收藏  举报