Java™8 是第一个支持类型推断的 Java 版本,并且它仅对 lambda 表达式支持此功能。在 lambda 表达式中使用类型推断具备强大的做用,它将帮助您作好准备以应对将来的 Java 版本,在从此的版本中还会将类型推断用于变量等更多可能。这里的诀窍在于恰当地命名参数,相信 Java 编译器会推断出剩余的信息。html
大多数时候,编译器彻底可以推断类型。在它没法推断出来的时候,就会报错。java
了解 lambda 表达式中的类型推断的工做原理,至少查看一个没法推断类型的示例。即便如此,也有解决办法。ide
假设您询问某我的“您叫什么名字?”,他会回答“我名叫约翰”。这种状况常常发生,但简单地说“约翰”会更高效。您须要的只是一个名称,因此该句子的剩余部分都是多余的。函数
不幸的是,咱们老是在代码中作这类多余的事情。Java 开发人员可使用 forEach
迭代并输出某个范围内的每一个值的双倍值,以下所示:测试
IntStream.rangeClosed(1, 5) .forEach((int number) -> System.out.println(number * 2));
rangeClosed
方法生成一个从 1 到 5 的 int
值流。lambda 表达式的惟一职责就是接收一个名为 number
的 int
参数,使用PrintStream
的 println
方法输出该值的双倍值。从语法上讲,该 lambda 表达式没有错,但类型细节有些冗余。code
当您从某个数字范围中提取一个值时,编译器知道该值的类型为 int
。不须要在代码中显式声明该值,尽管这是目前为止的约定。htm
在 Java 8 中,咱们能够丢弃 lambda 表达式中的类型,以下所示:对象
IntStream.rangeClosed(1, 5) .forEach((number) -> System.out.println(number * 2));
因为 Java 是静态类型语言,它须要在编译时知道全部对象和变量的类型。在 lambda 表达式的参数列表中省略类型并不会让 Java 更接近动态类型语言。可是,添加适当的类型推断功能会让 Java 更接近其余静态类型语言,好比 Scala 或 Haskell。blog
若是您在 lambda 表达式的一个参数中省略类型,Java 须要经过上下文细节来推断该类型。接口
返回到上一个示例,当咱们在 IntStream
上调用 forEach
时,编译器会查找该方法来肯定它采用的参数。IntStream
的 forEach
方法指望使用函数接口 IntConsumer
,该接口的抽象方法 accept
采用了一个 int
类型的参数并返回 void
。
若是在参数列表中指定了该类型,编译器将会确认该类型符合预期。
若是省略该类型,编译器会推断出预期的类型 —在本例中为 int
。
不管是您提供类型仍是编译器推断出该类型,Java 都会在编译时知道 lambda 表达式参数的类型。要测试这种状况,能够在 lambda 表达式中引入一个错误,同时省略参数的类型:
IntStream.rangeClosed(1, 5) .forEach((number) -> System.out.println(number.length() * 2));
编译此代码时,Java 编译器会返回如下错误:
Sample.java:7: error: int cannot be dereferenced .forEach((number) -> System.out.println(number.length() * 2)); ^1 error
编译器知道名为 number
的参数的类型。它报错是由于它没法使用点运算符解除对某个 int
类型的变量的引用。能够对对象执行此操做,但不能对 int
变量这么作。
在 lambda 表达式中省略类型有两个主要好处:
(number)
比 (int number)
简单得多。此外,通常来说,若是咱们仅有一个参数,省略类型意味着也能够省略 ()
,以下所示:
IntStream.rangeClosed(1, 5) .forEach(number -> System.out.println(number * 2));
请注意,您将须要为采用多个参数的 lambda 表达式添加括号。
lambda 表达式中的类型推断违背了 Java 中的常规作法,在常规作法中,会指定每一个变量和参数的类型。尽管一些开发人员辩称 Java 指定类型的约定让代码变得更可读、更容易理解,但我认为这种偏好反映出一种习惯而不是必要性。
以一个包含一系列转换的函数管道为例:
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());
在这里,咱们首先提供了一组 Car
实例和相关的注册信息。咱们获取每辆车的车主和车主姓名,并将该姓名转换为大写。最后,将结果放入一个列表中。
这段代码中的每一个 lambda 表达式都为其参数指定了一个类型,但咱们为参数使用了单字母变量名。这在 Java 中很常见。但这种作法不合适,由于它丢弃了特定于域的上下文。
咱们能够作得比这更好。让咱们看看使用更强大的参数名重写代码后发生的状况:
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
不仅是一我的,仍是这辆车的车主。
这个示例中的每一个 lambda 表达式都比它所取代的表达式更好。在读取 lambda 表达式(例如 (Person owner) -> owner.getName()
)时,咱们知道咱们得到了车主的姓名,而不仅是随便某我的的姓名。
Scala 和 TypeScript 等一些语言更加剧视参数名而不是类型。在 Scala 中,咱们在定义类型以前定义参数,例如经过编写:
def getOwner(registration: String)
而不是:
def getOwner(String registration)
类型和参数名都颇有用,但在 Scala 中,参数名更重要一些。咱们用 Java 编写 lambda 表达式时,也能够考虑这一想法。请注意咱们在 Java 中的车辆注册示例中丢弃类型细节和括号时发生的状况:
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
类:
class Car { public String getRegistration() { return null; }}
接下来,咱们将建立一个 Comparator
,以便基于 Car
实例的注册信息对它们进行比较:
public static Comparator<Car> createComparator() { return comparing((Car car) -> car.getRegistration());}
用做 comparing
方法的参数的 lambda 表达式在其参数列表中包含了类型信息。咱们知道 Java 编译器很是擅长类型推断,那么让咱们看看在省略参数类型的状况下会发生什么,以下所示:
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
调用后面添加了一个新调用。在本例中,咱们还为 lambda 表达式的参数从新引入显式类型:
public static Comparator<Car> createComparator() { return comparing((Car car) -> car.getRegistration()).reversed();}
借助显式类型,此代码没有编译问题,但如今让咱们丢弃类型信息,看看会发生什么:
public static Comparator<Car> createComparator() { return comparing(car -> car.getRegistration()).reversed();}
如您下面所见,进展并不顺利。Java 编译器抛出了错误:
Sample.java:21: error: cannot find symbol return comparing(car -> car.getRegistration()).reversed(); ^ symbol: method getRegistration() location: variable car of type ObjectSample.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 表达式,而是尝试使用方法引用:
public static Comparator<Car> createComparator() { return comparing(Car::getRegistration).reversed();}
编译器对此解决方案很是满意。它在方法引用中使用 Car::
来推断类型。
Java 8 为 lambda 表达式的参数引入了有限的类型推断能力,在将来的 Java 版本中,会将类型推断扩展到局部变量。如今应该学会省略类型细节并信任编译器,这有助于您轻松步入将来的 Java 环境。
依靠类型推断和适当命名的参数,编写简明、更富于表达且更少杂质的代码。只要您相信编译器能自行推断出类型,就可使用类型推断。仅在您肯定编译器确实须要您的帮助的状况下提供类型细节。
原做者:Venkat Subramaniam
原文连接: Java 8 习惯用语
原出处: IBM Developer