新的Java版本每年发布两次,但每一次新的迭代似乎都只是在前一次的基础上有小的改进。虽然这对Java 17来说可能也是如此,但这个版本具有更深的意义,因为Java 8(目前最常用的Java版本)失去了Oracle高级支持。在这篇文章中,我们将探讨最重要的Java 17功能,这两个版本之间的差异,以及它们对Java软件的影响。你应该把你的应用程序从Java 8迁移到17吗?让我们拭目以待。

免责声明:这篇文章最初发表于2021年10月22日。然而,在2022年12月,它被更新了关于Java 8的Oracle企业性能包的新信息。

2022年3月,Java 8失去了Oracle高级支持。这并不意味着它不会收到任何新的更新,但甲骨文投入到维护它的努力可能会比现在小得多。

这意味着有充分的理由转移到新的版本。特别是在2021年9月14日,Java 17被发布。这是新的长期支持版本,Oracle高级支持将持续到2026年9月(至少)。Java 17会带来什么?迁移会有多困难?它值得吗?我将在本文中尝试回答这些问题。

同样值得注意的是,Java 8仍在得到一些扩展–尽管只针对Oracle Java及其昂贵的Java SE订阅。2022年7月19日,一个针对Java 8的Oracle企业性能包被发布。版本号是8u345-PERF-b31。在文章后面比较Java 8和17的特定功能时,会提到这个版本的新增功能。

Java 8的普及 – 历史的小插曲

2014年3月发布的Java 8,目前有69%的程序员在其主要应用中使用。为什么经过7年多的时间,它仍然是最常用的版本?这有很多原因。

Java 8提供了很多语言功能,使开发者愿意从以前的版本转换过来。Lambdas、流、函数式编程、广泛的API扩展 – 更不用说MetaSpace或G1扩展。这是一个值得使用的Java版本。

3年后的2017年9月,Java 9出现了,对于一个典型的开发者来说,它几乎没有什么变化。一个新的HTTP客户端、进程API、小的钻石运算符和try-with-resources改进。

当然,Java 9确实带来了一个重大的变化,甚至是突破性的–Jigsaw项目。它改变了很多,非常多的东西–但在内部。Java模块化带来了巨大的可能性,解决了很多技术问题,适用于每个人,但实际上只有相对较少的用户群需要深入了解这些变化。由于Jigsaw项目引入的变化,很多库需要额外的修改,新的版本被发布,其中一些不能正常工作。

Java 9的迁移–特别是对于大型的企业应用–往往是困难的、耗时的,并引起回归问题。那么,如果没有什么收获,而且要花费大量的时间和金钱,为什么要这样做呢?

Java开发工具包17(JDK 17)于2021年10月发布。现在是不是从8岁的Java 8转移的好时机?首先,让我们看看Java 17里有什么。与Java 8相比,它能给程序员和管理员或SRE带来什么?

Java 17与Java 8的对比 – 变化

这篇文章只涵盖了我认为足够重要或足够有趣的变化。它们并不是 Java 多年来的所有变化、改进和优化。如果你想看JDK的完整变化列表,你应该知道它们是作为JEP(JDK增强建议)被跟踪的。该列表可以在JEP-0中找到。

另外,如果您想比较不同版本的 Java API,有一个很好的工具叫 Java Version Almanac。Java API有许多有用的、小的补充,如果有人想了解所有这些变化,查看这个网站可能是最好的选择。

至于现在,让我们分析一下Java每次迭代中的变化和新功能,从我们大多数Java开发者的角度来看,这些变化和新功能是最重要的。

新的var关键字

增加了一个新的var关键字,允许以一种更简洁的方式声明局部变量。考虑一下这段代码:

// java 8 way
Map<String, List<MyDtoType>> myMap = new HashMap<String, List<MyDtoType>>();
List<MyDomainObjectWithLongName> myList = aDelegate.fetchDomainObjects();
// java 10 way
var myMap = new HashMap<String, List<MyDtoType>>();
var myList = aDelegate.fetchDomainObjects()

当使用var时,声明要短得多,而且,也许比以前更有可读性。我们必须确保首先考虑到可读性,所以在某些情况下,向程序员隐藏类型可能是错误的。注意正确命名变量。

不幸的是,不可能使用var关键字将lambda分配给一个变量:

// causes compilation error: 
//   method reference needs an explicit target-type
var fun = MyObject::mySpecialFunction;

然而,在lambda表达式中使用var是可能的。请看下面的例子:

boolean isThereAneedle = stringsList.stream()
  .anyMatch((@NonNull var s) -> s.equals(“needle”));

在lambda参数中使用var,我们可以给参数添加注解。

Records

人们可以说Records是Java对Lombok的回应。至少有一部分是这样的。记录是一个用来存储一些数据的类型。让我引用JEP 395中的一个片段,它很好地描述了它。

[……]一个记录会自动获得许多标准成员。
为状态描述的每个组件提供一个私有的最终字段。
为状态描述的每个组件提供一个公共的读访问器方法,其名称和类型与组件相同。
一个公共构造函数,其签名与状态描述相同,它从相应的参数初始化每个字段。
equals和hashCode的实现,如果两条记录的类型相同且包含相同的状态,则这两条记录是相等的;
以及toString的实现,包括所有记录组件的字符串表示,以及它们的名称。

换句话说,它大致上相当于Lombok的@Value。就语言而言,它有点类似于一个枚举。然而,你不是声明可能的值,而是声明字段。Java根据该声明生成一些代码,并能够以更好的、优化的方式处理它。像枚举一样,它不能扩展或被其他类扩展,但它可以实现一个接口并拥有静态字段和方法。与枚举相反,记录可以用new关键字进行实例化。

一个记录可能看起来像这样:

record BankAccount (String bankName, String accountNumber) implements HasAccountNumber {}

而这就是它。很简洁。简洁是优秀的!

任何自动生成的方法都可以由程序员手动声明。一组构造函数也可以被声明。此外,在构造函数中,所有肯定未被赋值的字段都被隐式地赋值给它们相应的构造函数参数。这意味着,在构造函数中可以完全跳过赋值!

record BankAccount (String bankName, String accountNumber) implements HasAccountNumber {
  public BankAccount { // <-- this is the constructor! no () !
    if (accountNumber == null || accountNumber.length() != 26) {
      throw new ValidationException(“Account number invalid”);
    }
    // no assignment necessary here!
  }
}

对于所有的细节,如正式的语法,使用和实现的注意事项,请务必参考JEP 359。你也可以查看StackOverflow上关于Java记录的最多投票的问题

扩展的switch表达式

很多语言中都有switch,但由于它的局限性,多年来它的作用越来越小。Java的其他部分在增长,switch却没有。现在,switch案例可以更容易地分组,而且更容易阅读(注意,没有中断!),switch表达式本身实际上返回一个结果。

DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> false;
    case SATURDAY, SUNDAY -> true;
};

新的yield关键字可以实现更多的功能,它允许从代码块内返回一个值。它实际上是一个在案例块内工作的返回,并将该值设置为其开关的结果。它也可以接受一个表达式而不是一个单一的值。让我们来看看一个例子:

DayOfWeek dayOfWeek = LocalDate.now().getDayOfWeek();
boolean freeDay = switch (dayOfWeek) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> {
      System.out.println("Work work work");
      yield false;
    }
    case SATURDAY, SUNDAY -> {
      System.out.println("Yey, a free day!");
      yield true;
    }
};

Instanceof模式匹配

虽然不是一个突破性的变化,但在我看来,instanceof解决了Java语言中一个比较恼人的问题。你是否曾经不得不使用这样的语法?

if (obj instanceof MyObject) {
  MyObject myObject = (MyObject) obj;
  // … further logic
}

现在,你不必这样做了。Java现在可以在if里面创建一个局部变量,像这样:

if (obj instanceof MyObject myObject) {
  // … the same logic
}

这只是删除了一行,但就代码流程而言,这是完全不必要的一行。此外,声明的变量可以在同一个if条件下使用,像这样:

if (obj instanceof MyObject myObject && myObject.isValid()) {
  // … the same logic
}

封闭类

这是个很难解释的问题。让我们从这个开始–switch中的 “无默认 “警告是否曾经让你感到恼火?你覆盖了领域所接受的所有选项,但警告仍然存在。封闭类让你摆脱了对instanceof类型检查的这种警告。

如果你有一个像这样的层次结构:

public abstract sealed class Animal
    permits Dog, Cat {
}
public final class Dog extends Animal {
}
public final class Cat extends Animal {
}

你现在将能够做到这一点:

if (animal instanceof Dog d) {
    return d.woof();
} 
else if (animal instanceof Cat c) {
    return c.meow();
}

而且你不会得到一个警告。好吧,让我重新表述一下:如果你得到一个类似序列的警告,那么这个警告将是有意义的!这就是为什么你会得到警告。而更多的信息总是好的。

我对这个变化的感觉很复杂。引入一个循环引用似乎不是一个好的做法。如果我在我的生产代码中使用这个,我会尽力把它藏在一个很深的地方,并且永远不向外界展示它–我的意思是,永远不通过API暴露它,而不是说我会为在有效情况下使用它而感到羞耻。

文本块

在Java编程中,声明长字符串的情况并不常见,但一旦发生,就会让人感到厌烦和困惑。Java 13为此提出了一个修复方案,并在以后的版本中进一步改进。现在,一个多行文本块可以按如下方式声明:

String myWallOfText = ”””
______         _   _           
| ___ \       | | (_)          
| |_/ / __ ___| |_ _ _   _ ___ 
|  __/ '__/ _ \ __| | | | / __|
| |  | | |  __/ |_| | |_| \__ \
\_|  |_|  \___|\__|_|\__,_|___/
”””

不需要转义引号或换行。可以转义换行并保持字符串为单行,像这样:

String myPoem = ”””
Roses are red, violets are blue - \
Pretius makes the best software, that is always true
”””

这就相当于:

String myPoem = ”Roses are red, violets are blue - Pretius makes the best software, that still is true”.

文本块可以用来在你的代码中保持一个合理的可读的json或xml模板。外部文件仍然可能是一个更好的主意,但如果有必要,用纯Java来做仍然是一个不错的选择。

译者注:写过 Scala 或者 Python 的程序员应该对此深有体会,避免了一堆+号或append方法。

更好的NullPointerExceptions

所以,我的应用程序中曾经有这样一连串的呼叫。我想你也可能对它感到熟悉:

company.getOwner().getAddress().getCity();

我得到了一个NPE,它准确地告诉我在哪一行遇到了null。是的,就是那一行。没有调试器,我无法知道哪个对象是空的,或者说,哪个调用操作实际上导致了这个问题。现在消息会很具体,它会告诉我们,JVM “无法调用Person.getAddress()”。

实际上,这更像是JVM的变化,而不是Java的变化–因为构建详细消息的字节码分析是在运行时JVM进行的–但它确实对程序员有很大的吸引力。

新的HttpClient

有很多库可以做同样的事情,但在Java中拥有一个合适的HTTP客户端是很好的。你可以在Baeldung中找到关于新的API的一个很好的介绍。

新增Optional.orElseThrow()方法

一个关于Optional的get()方法被用来获取Optional下的值。如果没有值,这个方法会抛出一个异常。就像下面的代码:

MyObject myObject = myList.stream()
  .filter(MyObject::someBoolean)
  .filter((b) -> false)
  .findFirst()
  .get();

Java 10在Optional中引入了一个新方法,叫做orElseThrow()。它的作用是什么?完全一样! 但是考虑到程序员的可读性变化。

MyObject myObject = myList.stream()
  .filter(MyObject::someBoolean)
  .filter((b) -> false)
  .findFirst()
  .orElseThrow();

现在,程序员清楚地知道当对象未被找到时将会发生什么。事实上,我们推荐使用这个方法,而不是简单的、无处不在的get()。

JVM 17与JVM 8的变化

Project Jigsaw

JDK 9的Project Jigsaw大大改变了JVM的内部结构。它改变了JLS和JVMS,增加了几个JEP(可在上面的Project Jigsaw链接中找到列表),最重要的是,引入了一些破坏性的变化,这些变化与以前的Java版本不兼容。

Java 9模块被引入,作为一个额外的、最高级别的jar和类组织。关于这个话题有很多介绍性的内容,比如Baeldung上的这个,或者Yuichi Sakuraba的这些幻灯片。

收益很大,虽然肉眼看不出来。所谓的JAR地狱已经不存在了(你去过吗? 我去过……而且真的是一个地狱),尽管现在模块地狱也是一种可能。

从一个典型的程序员的角度来看,这些变化现在几乎是看不见的。只有最大和最复杂的项目可能会受到某种程度的影响。几乎所有常用的库的新版本都遵守新的规则,并在内部考虑到这些规则。

垃圾回收

从Java 9开始,G1是默认的垃圾收集器。与Parallel GC相比,它减少了暂停时间,尽管它的总体吞吐量可能较低。自从成为默认的垃圾收集器后,它经历了一些变化,包括将未使用的承诺内存返回给操作系统的能力(JEP 346)。

在Java 11中引入了ZGC垃圾收集器,并在Java 15中达到产品状态(JEP 377)。它的目的是进一步减少停顿。从Java 13开始,它还能够将未使用的已承诺内存返回给操作系统(JEP 351)。

在JDK 14中引入了Shenandoah GC,并在Java 15中达到产品状态(JEP 379)。它的目的是保持较低的暂停时间,并且与堆的大小无关。

请注意,在Java 8中,你的选择要少得多,如果你没有手动改变你的GC,你仍然使用并行GC。简单地切换到Java 17可能会使你的应用程序工作得更快,方法运行时间更一致。切换到,然后不可用的ZGC或Shenandoah可能会得到更好的结果。

最后,还有一个新的No-Op垃圾收集器可用(JEP 318),尽管它是一个实验性的功能。这个垃圾收集器实际上不做任何工作–因此允许你精确测量你的应用程序的内存使用情况。如果你想尽可能地保持你的内存操作吞吐量,那就很有用。

如果你想了解更多关于可用选项的信息,我推荐你阅读Marko Topolnik的一系列伟大的文章,对GCs进行了比较。

前面提到的G1垃圾收集器是在Oracle企业性能包8u345版本中添加到Oracle Java 8的。与Compact Strings一起,它可以对Java应用程序的内存消耗产生重大影响。

Container感知

如果你不知道,曾经有一段时间,Java不知道它是在一个容器中运行。它没有考虑到容器的内存限制,而是读取可用的系统内存。因此,当你有一台拥有16GB内存的机器,将你的容器的最大内存设置为1GB,并在上面运行一个Java应用程序时,应用程序往往会失败,因为它将试图分配比容器上可用的更多的内存。Carlos Sanchez的一篇好文章更详细地解释了这一点。

这些问题现在已经成为过去。从Java 10开始,容器集成被默认启用。然而,这对你来说可能不是一个明显的改进,因为在Java 8的更新131中也引入了同样的变化,尽管它需要启用实验性选项并使用-XX:+UseCGroupMemoryLimitForHeap。

PS:使用-Xmx参数指定Java的最大内存通常是个好主意。在这种情况下,问题就不会出现。

CDS档案

为了使JVM的启动速度更快,在Java 8发布后的这段时间里,CDS档案经历了一些变化。从JDK 12开始,在构建过程中创建CDS档案是默认启用的(JEP 341)。JDK 13中的一项改进(JEP 350)允许在每个应用程序运行后更新档案。

类数据共享也在Oracle企业性能包8u345版本中为Java 8实现。然而,目前还不清楚这些变化的意义有多大;描述表明,只增加了JEP 310的范围。然而,我无法确认这一点。

Nicolai Parlog的一篇很好的文章演示了如何使用这个功能来改善你的应用程序的启动时间。

Java Flight Recorder and Java Mission Control

Java Flight Recorder(JEP 328)允许以较低的(目标1%)性能成本对运行中的Java应用程序进行监控和分析。Java Mission Control允许摄取和可视化JFR数据。请看Baeldung的教程,大致了解如何使用它以及可以从中得到什么。

你应该从Java 8迁移到Java 17吗?

简而言之:是的,你应该这样做。如果你有一个大型的、高负荷的企业应用,并且仍在使用Java 8,你肯定会在迁移后看到更好的性能、更快的启动时间、更低的内存占用率。从事该应用的程序员也应该更高兴,因为语言本身有很多改进。

然而,这样做的成本是很难估计的,而且根据所使用的应用服务器、库和应用程序本身的复杂性(或者说是它使用/重新实现的低级功能的数量)而有很大差异。

如果你的应用是微服务,很可能你只需要将基础docker镜像改为17-alpine,将maven中的代码版本改为17,一切就能正常工作了。一些框架或库的更新可能会派上用场(但无论如何你都会定期进行更新,对吧?)

现在,所有流行的服务器和框架都支持Java 9的Jigsaw项目。它是生产级的,经过了大量的测试,并在多年后修复了错误。许多产品提供了迁移指南,或者至少为Java 9兼容的版本提供了广泛的发布说明。请看OSGI的一篇不错的文章或Wildfly 15的一些发布说明,其中提到了模块支持。

如果你使用Spring Boot作为你的框架,有一些文章可以提供迁移技巧,比如spring-boot wiki中的这篇,Baeldung上的这篇,以及DZone上的另一篇。infoq也有一个有趣的案例研究。将Spring Boot 1迁移到Spring Boot 2是一个不同的话题,可能也值得考虑。Spring Boot本身有一个教程,Baeldung上也有一篇文章涉及这个话题。

如果你的应用程序没有自定义类加载器,没有严重依赖Unsafe,没有大量使用sun.misc或sun.security,那么你可能会没事。请参考JDEP关于Java依赖性分析工具的这篇文章,了解你可能需要做的一些改变。

有些东西从第8版开始就从Java中删除了,包括Nashorn JS引擎、Pack200 APIs和工具、Solaris/Sparc端口、AOT和JIT编译器、Java EE和Corba模块。有些东西仍然存在,但已被废弃删除,如Applet API或安全管理器。由于有很好的理由将其删除,你应该重新考虑在你的应用程序中使用它们。

我询问了我们Pretius的项目技术负责人关于他们从Java 8到Java 9+迁移的经验。有几个例子,没有一个是有问题的。在这里,一个库不工作,不得不更新;在那里,需要一些额外的库或配置,但总的来说,这根本不是一个糟糕的经历。

总结

Java 17 LTS将在未来几年内得到支持。另一方面,Java 8的支持已经结束。这当然是考虑转移到新版本的Java的坚实理由。在这篇文章中,我介绍了第8版和第17版之间最重要的语言和JVM变化(包括一些关于Java 8到Java 9+迁移过程的信息),这样就更容易理解它们之间的差异–以及评估迁移的风险和收益。

如果你碰巧是你公司的决策者,要问自己的问题是:会不会有 “好时机 “把Java 8留下来?有些钱总是要花的,有些时间总是要消耗的,有些需要做的额外工作的风险总是存在的。如果永远没有 “好时机”,那么现在很可能是一个好时机,就像永远不会有一样。

Java 17 特性的常见问题

Java 8是什么时候发布的?

Java 8 是在 2014 年 3 月发布的。

Java 17是什么时候发布的?

Java 17于2021年9月15日发布。

Java的最新版本是什么?

Java的最新版本是Java 19,于2022年9月发布。

我有什么版本的Java?

您可以在Java控制面板的 “常规 “选项卡中的 “关于 “部分查看您当前的Java版本。您也可以在您的bash/cmd中输入以下命令:

java -version

什么是Java 17?

它是具有长期支持的Java SE platform 的最新版本。

如何更新到Java 17?

安装软件只需运行可执行文件即可,但让您的系统为这一变化做好准备可能更复杂。

JDK 17 有哪些新功能?

JDK 17 是一个大型的 Java 更新,有大量的改进和新东西。它提供了以下新功能:

  1. 增强的伪随机数生成器
  2. 恢复始终严格的浮点运算语义
  3. macOS/AArch64支持
  4. 新的macOS Rendering pipelines
  5. 强化封装的JDK内部结构
  6. 废弃Applet API,以便于将来删除
  7. Switch 的模式匹配(预览)
  8. 封闭的类(Sealed Classes)
  9. 移除RMI的激活
  10. 撤销安全管理器
  11. 外来函数和内存API(孵化)
  12. 移除实验性AOT和JIT编译器
  13. 特定上下文的反序列化过滤器
  14. 矢量API(第二孵化)

Java 17和Java 18之间有什么区别

Java 17是一个长期支持版本–它至少会被支持8年。另一方面,Java 18只是一个较小的更新,有一些额外的功能,支持期为6个月。

JDK 17中包括JRE吗?

是的,与所有 JDK 版本一样,JDK 17 包括 Java 17 JRE。

什么是 Java 8 的企业性能包?

这是一种付费订阅,您可以通过购买它在 Java 8 中获得一些 Java 17 的功能,如 G1 垃圾收集器。