48

自 Java 8 以来的新语言特性

 4 years ago
source link: https://www.tuicool.com/articles/jeueIvE
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

BjqaQvJ.jpg!web

本文介绍了自 Java 8 以来与 Java 语言相关的改进。密切关注 Java 平台是很重要的,因为按照新的快速发布节奏,每六个月就会发布一个新的 Java 版本对平台和语言进行更改。

当 Java 8 引入 StreamsLambdas 时,那是一个巨大的变化,使得函数式编程风格可以用更少的样板代码来表达。虽然最近的版本没有添加这样有影响的特性,但是也对该语言进行了许多较小的改进。

本文总结了 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。

相关资源:

来自 Milling Project Coin 的各种改进

Coin 项目(JSR 334)是 JDK 7 的一部分,它带来了一些方便的语言改进:

  • Diamond 操作符

  • Try-with-resources 语句

  • 多异常捕获和更准确地异常重抛

  • 将 String 用于 switch 语句

  • 二进制整数字面值和数字字面值中的下划线

  • 简化的 Varargs 方法调用

Java 9 继续沿着这条道路前进,并添加了一些更小的改进。

允许在接口中声明私有方法

因为 Java 8 可以向接口添加默认方法。在 Java 9 中,这些默认方法甚至可以调用私有方法来共享代码,从而在需要时重用,但又不想公开暴露功能。

虽然这不是一个大问题,但它是一个逻辑上的补充,让你可以在默认方法中整理代码。

匿名内部类中的 Diamond 操作符

Java 7 引入了 Diamond 操作符(<>),通过让编译器推断构造函数的参数类型来简化代码:

复制代码

Listnumbers= new ArrayList<>();

但是,这个特性以前不能用于匿名内部类。根据项目邮件列表的讨论,这不是作为原始 Diamond 操作符特性的一部分添加的,因为它需要大量的 JVM 更改。

在 Java 9 中,这个小瑕疵得到了完善,使得该操作符更加通用:

复制代码

List<Integer> numbers =newArrayList<>() {
// ...
}

允许将有效 final 变量作为 try-with-resources 语句的资源

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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK