在Java 8之后,哪些功能得到了进一步的优化?

发表时间: 2019-08-22 20:30

通过本文了解Java8以来这门语言的发展。

作者 | Dávid Csákvári

译者 | 弯月,责编 | 郭芮

出品 | CSDN(ID:CSDNnews)

以下为译文:

当年Java 8引进的Stream和Lambda是一项重大改进,编写函数式编程风格的代码不再需要写大量的样板代码。虽然近期的版本并没有引入如此重大的改进,但Java还是引入了很多小改进。

这篇文章总结了Java 8之后引入的语言改进。如果你想了解新平台背后的JEP,请参考这篇文章:

  • https://advancedweb.hu/2019/02/19/post_java_8/

局部变量类型推断

var关键字可能是Java 8之后最重要的语言改进了。该关键字最初在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。但这次是个整型,因为本例中的date是Joda时间。这是个不同的API,行为也略有不同,但你没有发现,因为这个方法非常长,而你并没有阅读所有代码。

如果这里给出了显式类型定义,那么确定dayOfWeek就非常容易。而使用var时,阅读者首先要找到date变量的类型,并检查其getDayOfWeek的行为。在IDE中很容易理解,但快速阅读代码时就没那么容易了。

注意保留重要的类型信息

第二种情况是,使用var会丧失所有类型信息,甚至导致无法推断。大多数情况下这个问题会被Java编译器捕获。例如,var不能推断lambda或方法引用,因为在这些特性中,编译器依赖左侧的表达式来确定类型。

但是有一些例外。例如,var不能很好地用于菱形操作符。在创建泛型的实例时,菱形操作符可以让表达式右侧不那么繁琐:

Map<String, String> myMap = new HashMap<String, String>; // Pre Java 7
Map<String, String> myMap = new HashMap<>; // Using Diamond operator

由于该运算符只处理泛型类型,所以我们依然可以去掉一些冗余。我们可以通过var进一步简化:

var myMap = new HashMap<>;

这个例子是合法的,而且Java 11编译器甚至都不会发出警告。但是,我们没有为泛型类型指定任何类型,导致所有类型都必须推断,所以最后的类型是Map<Object, Object>。

当然,只需去掉菱形运算符就可以解决这个问题:

var myMap = new HashMap<String, String>;

另一个问题是在基本数据类型上使用var:

byte b = 1;
short s = 1;
int i = 1;
long l = 1;
float f = 1;
double d = 1;

如果不给出显式类型定义,那么所有变量都会被推断为int。所以,使用基本数据类型时要使用类型字面量(例如1L),或者不要使用var。

务必阅读官方的风格指南

何时使用类型推断、怎样做不会破坏易读性和正确性,这些问题最终都需要你自己判断。经验法则是:遵循优秀的编程实践,比如良好的命名规则、尽力减小局部变量作用域等都会有很大帮助。请务必阅读官方有关var的风格指南(
https://openjdk.java.net/projects/amber/LVTIstyle.html)和FAQ(
https://openjdk.java.net/projects/amber/LVTIFAQ.html)。

虽然var有如此多的陷阱,但很幸运它的引入相当保守,现在只能用于作用域有限的局部变量。

而且,var的引入也十分谨慎,var并不是新的关键字,而是保留类型名。这就意味着,只有当作为类型名使用时才有特殊含义。任何其他位置出现的var依然只是个合法的标识符。

目前,var没有相应的不可修改版本(如val或const)来定义常量并推断类型。希望以后的版本能够添加这个关键字,在那之前我们可以先使用final var。

参考资料:

  • 与Java 10的var的第一次亲密接触(
    https://blog.codefx.org/java/java-10-var-type-inference/)

  • Java局部变量类型推断详解(https://dzone.com
    /articles/var-work-in-progress)

  • Java 10:局部变量推断(https://www.journaldev.com/198
    71/java-10-local-variable-type-inference)

Project Coin带来的多项改进

Project Coin(JSR 334,
https://jcp.org/en/jsr/detail?id=334)是JDK 7的一部分,它带来了许多方便的语言改进:

  • 菱形运算符

  • try-with-resources语句

  • 多catch和更精确的重新throw

  • 在switch语句中使用字符串

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

  • 简化的varargs方法调用

Java 9继续做出了许多小改进。

接口支持私有方法

从Java 8起可以给接口添加默认方法。在Java 9中,这些默认方法甚至可以调用私有方法,这样无需公开就可以复用代码。

尽管算不上重大改进,但能够让默认方法中的代码更简洁。

匿名内层类的菱形操作符

Java 7引入了菱形操作符(<>),让编译器推断构造函数的参数类型,来减少繁琐:

List<Integer> numbers = new ArrayList<>;

但是,以前该功能不能用于匿名内层类上。根据项目的邮件列表中的讨论(
http://mail.openjdk.java.net/pipermail/coin-dev/2011-June/003283.html)可知,该功能没有作为菱形运算符的最初特性实现的原因是它需要JVM做出重大变更。

在Java 9中这个边缘情况终于解决了,因此现在的菱形运算符更通用:

List<Integer> numbers = new ArrayList<> {
// ...
}

try-with-resources语句中允许使用没有发生实质性改变的变量

Java 7引入的另一项改进就是try-with-resources语句,从此程序员无需再担心释放资源的问题。

我们来演示一下这个功能。首先,在Java 7之前如果想正确关闭资源,需要这样写:

BufferedReader br = new BufferedReader(...);
try {
return br.readLine;
} finally {
if (br != ) {
br.close;
}
}

有了try-with-resources语句,资源就可以自动释放,省却了许多繁文缛节:

try (BufferedReader br = new BufferedReader(...)) {
return br.readLine;
}

尽管这个功能非常强大,但它有几个缺点(Java 9解决了这些缺点)。

虽然这种方法能处理多个资源,但很容易让代码丧失可读性。像这样在try关键字之后以列表的方式定义变量,看起来非常不符合常见的Java编程习惯:

try (BufferedReader br1 = new BufferedReader(...);
BufferedReader br2 = new BufferedReader(...)) {
System.out.println(br1.readLine + br2.readLine);
}

而且,在Java 7之前,如果你想用这种写法来处理已有的变量,就必须定义一个临时变量。(例如JDK-8068948中的例子:
https://bugs.openjdk.java.net/browse/JDK-8068948。)

为了解决这些问题,Java增强了try-with-resources,现在不仅能够处理新创建的变量,还能够处理局部常量,或者实际上不可变的局部变量:

BufferedReader br1 = new BufferedReader(...);
BufferedReader br2 = new BufferedReader(...);
try (br1; br2) {
System.out.println(br1.readLine + br2.readLine);
}

在这个例子中,变量初始化不需要跟try-with-resources的初始化部分写在一起。

不过需要注意的一个陷阱是,现在允许访问已经被try-with-resources释放的资源,绝大部分情况下这种访问都会失败:

BufferedReader br = new BufferedReader(...);
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 Project Coin中的一部分)。

有关Varargs的更详细内容可以看这里(https://docs.oracle.com
/javase/8/docs/technotes/guides/language/varargs.html)。组合使用官方文档中提到的这些功能可能会造成泛型(https://docs.oracle
.com
/javase/8/docs/technotes/guides/language/generics.html)及其潜在问题(https://docs.oracle
.com
/javase/tutorial/java/generics/nonReifiableVarargsType.html)。

此外,从Java 9开始,编译器不会再因为导入被弃用的类型的import语句而产生警告。这些警告没有提供有用的信息,而且完全是多余的,因为在实际使用被弃用的类型成员时必然会产生警告。

文本讨论了Java 8之后的版本中这门语言本身的改进。随时关注Java平台很重要,因为现在的发布节奏很快,每六个月就会发布一个新版本,平台和语言也会发生相应的变化。

原文:

https://advancedweb.hu/2019/08/08/post_java_8_language_features

作者:Dávid Csákvári,全栈工程师,在Java和Web技术方面有十多年的经验。

【END】