抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

JDK8 新特性-类型推断

概述

​ 某些情况下,用户需要手动指明类型,建议大家根据自己或项目组的习惯,采用让代码最便于阅读的方法。有时省略类型信息可以减少干扰,更易弄清状况;而有时却需要类型信息帮助理解代码。经验证发现,一开始类型信息是有用的,但随后可以只在真正需要时才加上类型信息。下面将介绍一些简单的规则,来帮助确认是否需要手动声明参数类型。Lambda表达式中的类型推断,实际上是Java 7就引入的目标类型推断的扩展。读者可能已经知道Java7中的菱形操作符,它可使javac推断出泛型参数的类型。

变量类型做推断

使用菱形操作符,根据变量类型做推断

1
2
Map<String, Integer> one = new HashMap<String, Integer>();//1
Map<String, Integer> two = new HashMap<>();//2

​ 我们为变量one明确指定了泛型的类型, 而变量two 则使用了菱形操作符。不用明确声明泛型类型,编译器就可以自己推断出来,这就是它的神奇之处!当然,这并不是什么魔法,根据变量two 的类型可以推断出HashMap的泛型类型,但用户仍需要声明变量的泛型类型。如果将构造函数直接传递给-一个方法, 也可根据方法签名来推断类型。我们传入了HashMap,根据方法签名已经可以推断出泛型的类型。

方法签名做推断

使用菱形操作符,根据方法签名做推断

1
2
3
4
5
public static void main(String[] args) {
testM(new HashMap<>()); //1
}
public static void testM(HashMap<String,String> hashMap){ //2
}

​ Java7中程序员可省略构造函数的泛型类型,Java8更进–步,程序员可省略Lambda表达
式中的所有参数类型。再强调一次,这并不是魔法,javac 根据Lambda表达式上下文信息
就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再显
式声明类型罢了。这就是所谓的类型推断。

Lambda类型推断

下面将举例详细分析类型推

​ 下面两个例子将变量赋给-一个函数接口,这样便于理解。第一个例子 使用Lambda表达式检测-一个Integer是否大于5。这实际,上是一个Predicate一用 来判断真假的函数接口。

1
Predicate<Integer> atLeast5 = x -> x > 5;

​ Predicate也是一一个Lambda表达式,和前文中ActionListener不同的是,它还返回一个值。在例1-3中,表达式x > 5是Lambda表达式的主体。这样的情况下,返回值就是Lambda表达式主体的值(boolean)。

代码分析

Predicate接口的源码,接受-一个对象,返回一个布尔值

1
2
3
public interface Predicate<T> {
boolean test(T t);
}

​ 从例子中可以看出,Predicate 只有一一个泛型类型的参数,Integer 用于其中。Lambda表达式实现了Predicate接口,因此它的单一参数被推断为Integer类型。javac 还可检查Lambda表达式的返回值是不是boolean,这正是Predicate方法的返回类型。

java8 中的类型推断

当您在某个数字范围内提取一个值时,编译器知道该值的类型为 int。不需要在代码中显示的声明。

在 Java8 中我们可以丢弃 lambda 表达式中的类型:

1
2
IntStream.rangeClosed(1, 5)
.forEach((number) -> System.out.println(number * 2));

​ 由于 Java 是静态类型语言,它需要在编译时知道所有的对象和变量的类型。在 lambda 表达式的参数列表中省略类型并不会让 Java 更接近动态类型语言。但是,添加适当的类型推断功能会让 Java 更接近其他静态类型语言,比如 Haskel 等。

信任编译器

​ 如果您在 lambda 表达式的一个参数中省略类型,Java 需要通过上下文细节来推断该类型。返回上一个示例,当我们在 IntStream 上调用 forEach 时,编译器会查找该方法来确定它采用的参数。IntStream 的 forEach 方法期望使用函数接口 IntConsumer,该接口的抽象方法 accept 采用了一个 int 类型的参数并返回 void。

​ 如果在参数列表中指定了该类型,编译器将会确认该类型符合预期。如果省略了该类型,编译器会推断出预期的类型。

​ 无论您是提供类型还是编译器推断出该类型,Java 都会在编译时知道 lambda 表达式参数的类型。要测试这种情况,可以在 lambda 表达式中引入一个错误,同时省略参数的类型:

1
IntStream.rangeClosed(1,5).forEach((num)-> System.out.println(num.length()*2));

​ 编译器直接就会报错。编译器知道名为 number 的参数的类型。它报错是因为它无法使用点运算符解除对某个 int 类型的变量的引用。不能对 int 变量执行这个操作。

类型推断的好处

在 lambda 表达式中省略类型有两个主要好处:

  • 键入的内容更少。无需输入类型信息,因为编译器自己能轻松确定该类型。
  • 代码杂质更少,更简单。

此外,一般来讲,如果我们仅有一个参数,省略类型意味着也可以省略 (),如下所示:

1
IntStream.rangeClosed(1,5).forEach(num-> System.out.println(num*2));

请注意,您将需要为采用多个参数的 lambda 表达式添加括号。

类型推断和可读性

lambda 表达式中的类型推断违背了 Java 中的常规做法,在常规做法中,会指定每个变量和参数的类型。

看下面这个示例:

1
2
3
4
5
6
7
List<String> result =
cars.stream()
.map((Car c) -> c.getRegistration())
.map((String s) -> DMVRecords.getOwner(s))
.map((Person o) -> o.getName())
.map((String s) -> s.toUpperCase())
.collect(toList());

​ 这段代码中的每个 lambda 表达式都为其参数指定了一个类型,但我们为参数使用了单字母变量名。这在 Java 中很常见。但这种做法不合适,因为它丢弃了特定于域的上下文。

我们可以做得比这更好。让我们看看使用更强大的参数名重写代码后发生的情况:

1
2
3
4
5
6
7
List<String> result =
cars.stream()
.map((Car car) -> car.getRegistration())
.map((String registration) -> DMVRecords.getOwner(registration))
.map((Person owner) -> owner.getName())
.map((String name) -> name.toUpperCase())
.collect(toList());

​ 这些参数名包含了特定于域的信息。我们没有使用 s 来表示 String,而是指定了特定于域的细节,比如 registration 和 name。类似地,我们没有使用 p 或 o,而是使用 owner 表明 Person 不只是一个人,还是这辆车的车主。

命名参数

Scala 和 TypeScript 等一些语言更加重视参数名而不是类型。在 Scala 中,我们在定义类型之前定义参数,例如通过编写:

1
def getOwner(registration: String)

而不是:

1
def getOwner(String registration)

​ 类型和参数名都很有用,但在 Scala 中,参数名更重要一些。我们用 Java 编写 lambda 表达式时,也可以考虑这一想法。请注意我们在 Java 中的车辆注册示例中丢弃类型细节和括号时发生的情况:

1
2
3
4
5
6
7
List<String> result =
cars.stream()
.map(car -> car.getRegistration())
.map(registration -> DMVRecords.getOwner(registration))
.map(owner -> owner.getName())
.map(name -> name.toUpperCase())
.collect(toList());

​ 因为我们添加了描述性的参数名,所以我们没有丢失太多上下文,而且显式类型(现在是冗余内容)已悄然消失。结果是我们获得了更干净、更朴实的代码。

类型推断的局限性

尽管使用类型推断可以提高效率和可读性,但这种技术并不适合与所有的场所。在某些情况下,完全无法使用类型推断。幸运的是,您可以依靠 Java 编译器来获取何时出现这种情况。

​ 我们首先看一个编译器成功的例子,然后看一个测试失败的例子。

扩展类型推断

在我们的第一个示例中,假设我们想创建一个 Comparator 来比较 Car 实例。我们首先需要一个 Car 类:

1
2
3
class Car {
public String getRegistration() { return null; }
}

接下来,我们将创建一个 Comparator,以便基于 Car 实例的注册信息对它们进行比较:

1
2
3
public static Comparator<Car> createComparator() {
return comparing((Car car) -> car.getRegistration());
}

​ 用作 comparing 方法的参数的 lambda 表达式在其参数列表中包含了类型信息。我们知道 Java 编译器非常擅长类型推断,那么让我们看看在省略参数类型的情况下会发生什么,如下所示:

1
2
3
public static Comparator<Car> createComparator() {
return comparing(car -> car.getRegistration());
}

​ comparing 方法采用了 1 个参数。它期望使用 Function<? super T, ? extends U> 并返回 Comparator<T>。因为 comparing 是 Comparator<T> 上的一个静态方法,所以编译器目前没有关于 T 或 U 可能是什么的线索。

​ 为了解决此问题,编译器稍微扩展了推断范围,将范围扩大到传递给 comparing 方法的参数之外。它观察我们是如何处理调用 comparing 的结果的。根据此信息,编译器确定我们仅返回该结果。接下来,它看到由 comparing 返回的 Comparator<T> 又作为 Comparator<Car> 由 createComparator 返回 。

​ 注意了!编译器现在已明白我们的意图:它推断应该将 T 绑定到 Car。根据此信息,它知道 lambda 表达式中的 car 参数的类型应该为 Car。

​ 在这个例子中,编译器必须执行一些额外的工作来推断类型,但它成功了。接下来,让我们看看在提高挑战难度,让编译器达到其能力极限时,会发生什么。

推断的局限性

首先,我们在前一个 comparing 调用后面添加了一个新调用:

1
2
3
public static Comparator<Car> createComparator() {
return comparing((Car car) -> car.getRegistration()).reversed();
}

借助显式类型,此代码没有编译问题,但现在让我们丢弃类型信息,看看会发生什么:

1
2
3
public static Comparator<Car> createComparator() {
return comparing(car -> car.getRegistration()).reversed();
}

Java 编译器抛出了错误:

1
2
3
4
5
6
7
8
9
Sample.java:21: error: cannot find symbol
return comparing(car -> car.getRegistration()).reversed();
^
symbol: method getRegistration()
location: variable car of type Object
Sample.java:21: error: incompatible types: Comparator<Object> cannot be converted to Comparator<Car>
return comparing(car -> car.getRegistration()).reversed();
^
2 errors

​ 像上一个场景一样,在包含 .reversed() 之前,编译器会询问我们将如何处理调用 comparing(car -> car.getRegistration()) 的结果。在上一个示例中,我们以 Comparable<Car> 形式返回结果,所以编译器能推断出 T 的类型为 Car。

​ 但在修改过后的版本中,我们将传递 comparable 的结果作为调用 reversed() 的目标。comparable 返回 Comparable<T>,reversed() 没有展示任何有关 T 的可能含义的额外信息。根据此信息,编译器推断 T 的类型肯定是 Object。遗憾的是,此信息对于该代码而言并不足够,因为 Object 缺少我们在 lambda 表达式中调用的 getRegistration() 方法。

​ 类型推断在这一刻失败了。在这种情况下,编译器实际上需要一些信息。类型推断会分析参数、返回元素或赋值元素来确定类型,但在上下文提供的细节不足时,编译器就会达到其能力极限。

能否采用方法引用作为补救措施?

在我们放弃这种特殊情况之前,让我们尝试另一种方法:不使用 lambda 表达式,而是尝试使用方法引用:

1
2
3
public static Comparator<Car> createComparator() {
return comparing(Car::getRegistration).reversed();
}

由于直接说明了 Car 类型,编译器对此非常满意。

总结

​ Java 8 为 lambda 表达式的参数引入了有限的类型推断能力,在未来的 Java 版本中,会将类型推断扩展到局部变量。现在应该学会省略类型细节并信任编译器,这有助于您轻松步入未来的 Java 环境。

​ 依靠类型推断和适当命名的参数,编写简明、更富于表达且更少杂质的代码。只要您相信编译器能自行推断出类型,就可以使用类型推断。仅在您确定编译器确实需要您的帮助的情况下提供类型细节。

评论