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