自Java 8以来的新语言特性
当 Java 8 引入 Streams 和 Lambdas 时,那是一个巨大的变化,使得函数式编程风格可以用更少的样板代码来表达。虽然最近的版本没有添加这样有影响的特性,但是也对该语言进行了许多较小的改进。
本文总结了 Java 8 之后发布的 Java 版本中包含的语言增强。有关所有塑造新平台的 JEP 的概述,请查看这篇文章。
局部变量类型推断
自 Java 8 以来,最显著的语言改进可能是添加了关键词 var。它最初是在 Java 10 中引入的,并在 Java 11 中得到了进一步的改进。
该特性允许我们通过省略显式类型说明来简化局部变量声明的过程:
虽然它看起来类似于 JavaScript 的 var 关键字,但这与动态类型无关。
引用 JEP 的一句话:我们寻求通过减少与编写 Java 代码相关的仪式来改进开发人员的体验,同时保持 Java 对静态类型安全性的承诺。
声明变量的类型在编译时进行推断。在上面的示例中,推断出的类型是 String。使用 var 而不是显式类型可以减少这段代码的冗余,因此更容易阅读。
下面是另一个很适合类型推断的场景:
很明显,在很多情况下,这个特性可以提高代码质量。然而,有时坚持使用显式类型声明会更好。让我们看几个用 var 替换类型声明可能适得其反的例子。
注意可读性
第一种情况是,从源代码中删除显式类型信息会降低可读性。
当然,IDE 可以在这方面提供帮助,但是在代码评审期间,或者在快速扫描代码时,它可能会破坏可读性。例如,考虑下工厂或构建器:你必须找到负责对象初始化的代码来确定类型。
这里有个小谜题。下面这段代码使用 Java 8 的日期 / 时间 API。猜猜下面代码片段中变量的类型:
好了吗?答案是这样的:
第一个非常直观,parse 方法返回一个 LocalDate 对象。但是,对于接下来的两个方法,你需要更熟悉这些 API:dayOfWeek 返回 java.time.DayOfWeek,而 dayOfMonth 只返回一个 int。
另一个潜在的问题是,使用 var,读者不得不更多地依赖上下文。考虑以下代码片段:
根据前面的例子,我敢打赌你一定会猜到它是 java.time.DayOfWeek 类型。但这次,它是一个整数,因为本例中的日期来自 Joda time。它是一个不同的 API,行为略有不同,但你看不到它,因为它是一个比较长的方法,而且你没有阅读所有的代码行。
如果显式类型声明存在,那么弄清楚 dayOfWeek 的类型就很简单了。现在,使用 var,读者首先必须找出 date 变量的类型并检查 getDayOfWeek 做了什么。这在 IDE 中很简单,但在扫描代码时就不那么简单了。
注意保留重要的类型信息
第二种情况是,当使用 var 删除所有可用的类型信息时,甚至都无法推断出来。在大多数情况下,这些情况是由 Java 编译器捕获的。例如,var 不能为 Lambda 表达式或方法引用推断类型,因为对于这些特性,编译器依赖于左侧表达式来推断类型。
然而,也有一些例外。例如,var 不能很好地处理 Diamond 操作符。Diamond 操作符是一个很好的特性,可以在创建泛型实例时消除表达式右侧的一些冗余:
因为它只处理泛型类型,所以仍然有冗余需要删除。让我们试着用 var 使它更简洁:
这个例子是有效的,Java 11 甚至没有在编译器中发出关于它的警告。然而,使用所有这些类型推断,我们最终根本没有指定泛型类型,类型将是 Map<Object, Object>。
当然,这可以通过删除 Diamond 操作符轻松解决:
当 var 与原始数据类型一起使用时,可能会出现另一组问题:
如果没有显式的类型声明,所有这些变量的类型将被推断为 int。在处理基本数据类型时,使用字面类型(例如 1L),或者在这种情况中根本不使用 var。
请务必阅读官方风格指南
最终由你决定何时使用类型推断,并确保它不会损害可读性和正确性。作为一个经验法则,坚持良好的编程实践,比如良好的命名和最小化局部变量的作用域。务必要阅读官方风格指南和 FAQ 中关于 var 的部分。
因为 var 有太多的陷阱,所以它的引入比较保守,并且只能用于局部变量,而局部变量的作用域通常非常有限。
此外,它被谨慎地引入,var 不是一个新的关键字,而是一个保留类型名。这意味着只有当它作为类型名使用时,它才具有特殊的意义,在其他任何地方,var 都将继续作为有效标识符。
目前,var 没有一个不可变对应项(如 val 或 const)来声明一个最终变量,并用一个关键字来推断它的类型。我们希望将来的版本,在那之前,我们可以使用 final var。
来自 Milling Project Coin 的各种改进
Coin 项目(JSR 334)是 JDK 7 的一部分,它带来了一些方便的语言改进:
1.Diamond 操作符
2.Try-with-resources 语句
3.多异常捕获和更准确地异常重抛
4.将 String 用于 switch 语句
5.二进制整数字面值和数字字面值中的下划线
6.简化的 Varargs 方法调用
Java 9 继续沿着这条道路前进,并添加了一些更小的改进。
允许在接口中声明私有方法
因为 Java 8 可以向接口添加默认方法。在 Java 9 中,这些默认方法甚至可以调用私有方法来共享代码,从而在需要时重用,但又不想公开暴露功能。
虽然这不是一个大问题,但它是一个逻辑上的补充,让你可以在默认方法中整理代码。
匿名内部类中的 Diamond 操作符
Java 7 引入了 Diamond 操作符(<>),通过让编译器推断构造函数的参数类型来简化代码:
但是,这个特性以前不能用于匿名内部类。根据项目邮件列表的讨论,这不是作为原始 Diamond 操作符特性的一部分添加的,因为它需要大量的 JVM 更改。
在 Java 9 中,这个小瑕疵得到了完善,使得该操作符更加通用:
允许将有效 final 变量作为 try-with-resources 语句的资源
Java 7 引入的另一个增强是 try-with-resources,它使开发人员不必担心资源的释放。
为了说明它的强大功能,首先考虑下在 Java 7 之前,下面这个典型的例子为正确关闭资源所做的工作:
借助 try-with-resources,资源可以自动释放,大大减少了仪式代码(ceremony):
尽管具有强大的功能,但是 try-with-resources 有一些缺点在 Java 9 中才得以解决。
尽管这个构造可以处理多个资源,但它很容易使代码更难阅读。与通常的 Java 代码相比,在 try 关键字之后在列表中声明这样的变量有点非常规:
同样,在 Java 7 版本中,如果已经有一个你想要处理的变量采用了这个构造,就必须引入一个虚拟变量。(示例参见 JDK-8068948)。
为了减少批评,除了新创建的变量外,经过增强的 try-with-resources 可以处理 final 或有效 final 局部变量:
在本例中,变量的初始化与它们在 try-with-resources 构造的注册分离。
需要注意的是,现在可以引用已经由 try-with-resources 释放的变量,这在大多数情况下会失败:
下划线不再是有效的标识符名称
在 Java 8 中,当使用“_”作为标识符时,编译器会发出警告。Java 9 更进一步,使单个下划线字符作为标识符非法,并保留这个名称以便将来用于特殊的语义:
改进警告信息
最后,让我们简单介绍一下与最新 Java 版本中与编译器警告相关的更改。
现在可以使用 @SafeVarargs 注解一个私有方法来标记“Type safety: Potential heap pollution via varargs parameter”警告为假阳性。(事实上,此更改是前面讨论的 JEP 213:Milling Coin 项目的一部分)。要了解更多关于 Varargs、泛型以及组合这些特性可能出现的潜在问题的信息,请阅读官方文档。
同样,从 Java 9 开始,当导入不推荐使用的类型时,编译器不会针对 import 语句发出警告。由于在实际使用不推荐使用的成员时总是会显示单独的警告,所以这些警告没有提供足够的信息,而且是多余的。