Java 8 之后,还有哪些进化的功能?

Java 8 之后,还有哪些进化的功能?
2019年08月22日 19:55 CSDN

当年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 myMap = new HashMap(); // Pre Java 7Map myMap = new HashMap(); // Using Diamond operator

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

var myMap = new HashMap();

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

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

var myMap = new HashMap();

另一个问题是在基本数据类型上使用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/19871/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 numbers = new ArrayList();

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

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

List numbers = new ArrayList() {    // ...}

try-with-resources语句中允许使用没有发生实质性改变的变量Java 7引入的另一项改进就是try-with-resources语句,从此程序员无需再担心释放资源的问题。我们来演示一下这个功能。首先,在Java 7之前如果想正确关闭资源,需要这样写:

BufferedReader br = new BufferedReader(...);try {    return br.readLine();} finally {    if (br != null) {        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平台很重要,因为现在的发布节奏很快,每六个月就会发布一个新版本,平台和语言也会发生相应的变化。

财经自媒体联盟更多自媒体作者

新浪首页 语音播报 相关新闻 返回顶部