[

Rahul Gite

](https://.com/@rahul.gite11?source=post_page—–6d13290e4e1a——————————–)[

Javarevisited

](https://.com/javarevisited?source=post_page—–6d13290e4e1a——————————–)

照片由 米歇尔·卢恩斯 on Unsplash

Java 17 是 Java 编程语言的最新 LTS(长期支持)版本,于 2021 年 9 月 14 日发布。如果您当前使用的是 Java 11,那么可能是时候考虑迁移到 Java 17 以利用其新功能和改进了。

在本文中,我们将讨论 Java 17 中的新功能,尽管这里讨论的一些特性是在 Java 11 的 Java 17 之后的版本中引入的。

我们为什么要从 Java 11 迁移?

尽管 Java 11 也是一个 LTS 版本,并且被许多应用程序使用,但我们可能想要转向 Java 17 有一些主要原因。

  1. 终止对 Java 11 的支持:Java 11 将支持到 2023 年 9 月,扩展支持将持续到 2026 年 9 月。这意味着在支持结束后,我们将没有补丁(甚至没有安全补丁)。
  2. Spring 6:最新版本的 Spring,Spring 6 将需要 Java 17 才能工作,并且由于有许多库与它们一起工作,它们也将迁移到 Java 17。如果你的应用程序依赖于 Spring Framework,你绝对应该考虑迁移到 Java 17。
  3. Java 17 的免费 Oracle JDK:Java 17 根据新的 NFTC(Oracle 免费条款和条件)许可发布。因此,再次允许将 Oracle JDK 版本免费用于生产和商业用途。(Java 11 不允许)。

Java 17 有哪些新功能?

在 Java 17 中,引入了一些改进和新功能,这些改进和新功能将得到长期支持。

 文本块

Java 引入了文本块,使代码更具可读性并避免不必要的字符串格式。现在,我们可以将文本放在三引号之间,并在其中放置多个双引号字符串,而无需使用转义字符。示例如下所示:

<span id="ba95" data-selectable-paragraph=""><span>private</span> <span>static</span> <span>void</span> <span>jsonBlock</span><span>()</span> {<br>    <span>String</span> <span>text</span> <span>=</span> <span>"""<br>            {<br>              "name": "John Doe",<br>              "age": 45,<br>              "address": "Doe Street, 23, Java Town"<br>            }<br>          """</span>;<br>    System.out.println(text);<br>}</span>

正如我们所看到的,这使得编写 Json 和类似的字符串变得非常容易,这需要大量使用转义字符。

此外,结尾的三个双引号表示文本块的开头或其在输出中的缩进。在上面的示例中,输出中的每一行都有两个空格,因为双引号的位置在最后一个字符后面两个空格。

引入了两个新的转义字符用于文本块内,“\s”用于添加空格,“\”用于删除换行符。在编写长 SQL 语句时特别有用。

<span id="0932" data-selectable-paragraph=""><span>private</span> <span>static</span> <span>void</span> <span>sqlStatement</span><span>()</span> {<br>    <span>String</span> <span>sql</span> <span>=</span> <span>"""<br>    SELECT id, firstName, lastName\s\<br>    FROM Employee<br>    WHERE departmentId = "IT" \<br>    ORDER BY lastName, firstName"""</span>;<br>    System.out.println(text);<br>}</span>

改进的 Switch 语句

开关表达式将允许您从开关大小写中返回值,并在赋值等中使用这些返回值。Java 允许使用运算符 ->(箭头)而不是 :(冒号)来表示返回表达式。在此表达式中使用 switch 返回时,不需要 break 关键字,但默认大小写是必需的。

<span id="cab9" data-selectable-paragraph=""><span>private</span> <span>static</span> <span>void</span> <span>improvedSwitch</span><span>(Fruit fruit)</span> {<br>    <span>String</span> <span>text</span> <span>=</span> <span>switch</span> (fruit) {<br>        <span>case</span> APPLE, PEAR -&gt; {<br>            System.out.println(<span>"the given fruit was: "</span> + fruit);<br>            yield <span>"Common fruit"</span>;<br>        }<br>        <span>case</span> ORANGE, AVOCADO -&gt; <span>"Exotic fruit"</span>;<br>        <span>default</span> -&gt; <span>"Undefined fruit"</span>;<br>    };<br>    System.out.println(text);<br>}</span>

如果在开关案例中执行了多个操作,我们可以有一个案例块并使用 yield 关键字表示返回值。这里的 yield 是一个与上下文相关的关键字,即您可以在函数内的其他地方有一个变量名称 yield。

 “record”类型

记录类是一种特殊的不可变类,旨在替换数据传输对象 (DTO)。通常,如果我们想在类或方法中使用一些 POJO,我们必须声明该类以及定义所有 getter、settter、equals 和 hashcode 函数。例如,要在其他地方使用示例 Fruit 类,我们必须以某种方式定义我们的类,如下所示:

<span id="913a" data-selectable-paragraph=""><span>public</span> <span>class</span> <span>Fruit</span> {<br>    <span>private</span> String name;<br>    <span>private</span> <span>int</span> price;<br><br>    <br>}<br></span>

虽然我们可以通过使用像 lombok 这样的库来减少大部分样板代码,但我们仍然可以在记录的帮助下进一步减少它。对于记录,相同的代码变为:

<span id="0bb1" data-selectable-paragraph=""><span>public</span> <span>static</span> <span>void</span> <span>doSomething</span><span>()</span> {<br>  <span>record</span> <span>Fruit</span><span>(String name, <span>int</span> price)</span> {}<br>  <span>Fruit</span> <span>fruit</span> <span>=</span> <span>new</span> <span>Fruit</span>(<span>"Apple"</span>, <span>100</span>);<br>  System.out.println(fruit.getPrice());<br>}</span>

正如我们所看到的,我们甚至可以定义方法本地记录对象。records 对象自动为其所有字段提供 getter、equals 和 hashcode 方法。

记录内的字段无法更改,只能通过声明记录时给出的参数来定义,如上所示(但我们可以定义静态变量)。我们还可以定义一个自定义构造函数来验证字段。建议我们不要覆盖记录的 getter 和 settter,这可能会影响其不变性。下面显示了具有多个构造函数以及静态变量和方法的记录示例:

<span id="e18f" data-selectable-paragraph=""><span>public</span> <span>record</span> <span>Employee</span><span>(<span>int</span> id, String firstName,<br>                       String lastName)</span><br>{<br><br>   <span>static</span> <span>int</span> empToken;<br><br>    <br>    <span>public</span> Employee<br>    {<br>        <span>if</span> (id &lt; <span>100</span>) {<br>            <span>throw</span> <span>new</span> <span>IllegalArgumentException</span>(<br>                <span>"Employee Id cannot be below 100."</span>);<br>        }<br>        <span>if</span> (firstName.length() &lt; <span>2</span>) {<br>            <span>throw</span> <span>new</span> <span>IllegalArgumentException</span>(<br>                <span>"First name must be 2 characters or more."</span>);<br>        }<br>    }<br><br>    <br>    <br>    <span>public</span> <span>Employee</span><span>(<span>int</span> id, String firstName)</span><br>    {<br>        <span>this</span>(id, firstName, <span>null</span>);<br>    }<br><br>    <br>    <span>public</span> <span>void</span> <span>getFullName</span><span>()</span><br>    {<br>        <span>if</span> (lastName == <span>null</span>)<br>            System.out.println(firstName());<br><br>        <span>else</span><br>            System.out.println(firstName() + <span>" "</span><br>                               + lastName());<br>    }<br><br>    <br>    <span>public</span> <span>static</span> <span>int</span> <span>generateEmployeeToken</span><span>()</span><br>    {<br>        <span>return</span> ++empToken;<br>    }<br>}</span>

记录类的更多品质是:

1. 您可以在记录中使用嵌套类和接口。

2. 您也可以有嵌套记录,这将是隐式静态的。

3.记录可以实现接口。

4. 您可以创建通用记录类。

5. 记录是可序列化的。

有关记录的更多信息,请参见此处:

 “密封”类

密封类将使我们能够更好地控制允许哪些类扩展我们的类。在 Java 11 中,类可以是 final 类,也可以是 extended。如果要控制哪些类可以扩展超类,可以将所有类放在同一个包中,并为超类包提供可见性。但是,无法再从软件包外部访问超类。

例如,请参阅以下代码:

<span id="4ac7" data-selectable-paragraph=""><span>public</span> <span>abstract</span> <span>class</span> <span>Fruit</span> {<br>}<br><span>public</span> <span>final</span> <span>class</span> <span>Apple</span> <span>extends</span> <span>Fruit</span> {<br>}<br><span>public</span> <span>final</span> <span>class</span> <span>Pear</span> <span>extends</span> <span>Fruit</span> {<br>}</span>
<span id="d388" data-selectable-paragraph=""><span>private</span> <span>static</span> <span>void</span> <span>problemSpace</span><span>()</span> {<br>    <span>Apple</span> <span>apple</span> <span>=</span> <span>new</span> <span>Apple</span>();<br>    <span>Pear</span> <span>pear</span> <span>=</span> <span>new</span> <span>Pear</span>();<br>    <span>Fruit</span> <span>fruit</span> <span>=</span> apple;<br>    <span>class</span> <span>Avocado</span> <span>extends</span> <span>Fruit</span> {};<br>}</span>

在这里,我们不能阻止鳄梨扩展水果类。如果我们将 Fruit 类设为默认值,则不会编译将 apple 分配给 fruit 对象。因此,现在我们可以使用密封类来只允许特定类扩展我们的超类。下面给出了一个示例:

<span id="315d" data-selectable-paragraph=""><span>public</span> <span>abstract</span> <span>sealed</span> <span>class</span> <span>FruitSealed</span> permits AppleSealed, PearSealed {<br>}<br><span>public</span> <span>non-sealed</span> <span>class</span> <span>AppleSealed</span> <span>extends</span> <span>FruitSealed</span> {<br>}<br><span>public</span> <span>final</span> <span>class</span> <span>PearSealed</span> <span>extends</span> <span>FruitSealed</span> {<br>}</span>

正如我们所看到的,我们使用一个新的关键字 sealed 来表示这是一个密封类。我们定义可以使用 allows 关键字扩展的类。任何扩展密封类的类都可以是最终的,如 PearSealed,也可以通过使用 AppleSealed 声明类时使用 nonsealed 关键字由其他类扩展。

此实现将允许将 AppleSealed 分配给 FruitSealed 类,但不允许任何其他未由 allows 关键字定义的类来扩展 FruitSealed 类。有关密封课程的更多信息,请点击此处。

与“instance of”的模式匹配

在 Java 11 中,我们通常使用 operator 的实例来检查一个对象是否属于某个类。如果我们想在 check 的实例返回 true 后对对象执行一些操作,我们需要将对象显式转换为该特定类。示例如下所示:

<span id="99a9" data-selectable-paragraph=""><span>private</span> <span>static</span> <span>void</span> <span>oldStyle</span><span>()</span> {<br>    <span>Object</span> <span>o</span> <span>=</span> <span>new</span> <span>Grape</span>(Color.BLUE, <span>2</span>);<br>    <span>if</span> (o <span>instanceof</span> Grape) {<br>        <span>Grape</span> <span>grape</span> <span>=</span> (Grape) o;<br>        System.out.println(<span>"This grape has "</span> + grape.getPits() + <span>" pits."</span>);<br>    }<br>}</span>

在这里,我们需要显式地将对象转换为 Grape 类型,然后找出凹坑的数量。在 Java 17 中,我们可以将其更改为:

<span id="8a1f" data-selectable-paragraph=""><span>private</span> <span>static</span> <span>void</span> <span>patternMatchingInJava17</span><span>()</span> {<br>     <span>Object</span> <span>o</span> <span>=</span> <span>new</span> <span>Grape</span>(Color.BLUE, <span>2</span>);<br>     <span>if</span> (o <span>instanceof</span> Grape grape) {<br>         System.out.println(<span>"This grape has "</span> + grape.getPits() + <span>" pits."</span>);<br>     }<br>}</span>

我们可以将 check 的实例与 &&(and) 条件配对,但不能将 ||(或) 与“或”条件一样,即使检查实例返回 false ,语句也可以到达另一个条件

如果 check 的实例返回 true,则变量 grape 的范围甚至可以扩展到 if 块之外。在下面的示例中,如果对象不是 Grape 类型,则将引发运行时异常,因此编译器将确定 grape 对象在到达 print 语句时应该存在。可以在此处找到有关模式匹配实例的更多信息。

<span id="02ca" data-selectable-paragraph=""><span>private</span> <span>static</span> <span>void</span> <span>patternMatchingScopeException</span><span>()</span> {<br>    <span>Object</span> <span>o</span> <span>=</span> <span>new</span> <span>Grape</span>(Color.BLUE, <span>2</span>);<br>    <span>if</span> (!(o <span>instanceof</span>  Grape grape)) {<br>        <span>throw</span> <span>new</span> <span>RuntimeException</span>();<br>    }<br>    System.out.println(<span>"This grape has "</span> + grape.getPits() + <span>" pits."</span>);<br>}</span>

有用的 NullPointerException

在 Java 11 中,当我们得到 NullPointerException 时,我们只得到发生异常的行号,但我们没有得到解析为 null 的方法或变量。

在 Java 17 中,消息传递得到了改进,因为 NullPointerException 消息还告诉我们导致 NullPointerException 的确切方法调用。

<span id="1073" data-selectable-paragraph=""><span>public</span> <span>static</span> <span>void</span> <span>main</span><span>(String[] args)</span> {<br>    HashMap&lt;String, Grape&gt; grapes = <span>new</span> <span>HashMap</span>&lt;&gt;();<br>    grapes.put(<span>"grape1"</span>, <span>new</span> <span>GrapeClass</span>(Color.BLUE, <span>2</span>));<br>    grapes.put(<span>"grape2"</span>, <span>new</span> <span>GrapeClass</span>(Color.white, <span>4</span>));<br>    grapes.put(<span>"grape3"</span>, <span>null</span>);<br>    <span>var</span> <span>color</span> <span>=</span> ((Grape) grapes.get(<span>"grape3"</span>)).getColor();<br>}</span>

正如我们在这里看到的,我们试图获得一个 “grape3” 对象的颜色,它是空的。当我们比较 Java 11 和 Java 17 中的错误消息时,我们看到了错误消息的差异,因为现在我们确切地知道对映射中存在的 null 对象调用 get 方法会导致异常。

<span id="74c1" data-selectable-paragraph=""><br>Exception in thread <span>"main"</span> java.lang.NullPointerException<br>        at com.rg.java17.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:<span>13</span>)</span>
<span id="56e0" data-selectable-paragraph=""><br>Exception in thread <span>"main"</span> java.lang.NullPointerException: Cannot invoke <span>"com.rg.java17.Grape.getColor()"</span> because the <span>return</span> value of <span>"java.util.HashMap.get(Object)"</span> is <span>null</span><br>    at com.rg.java17.HelpfulNullPointerExceptions.main(HelpfulNullPointerExceptions.java:<span>13</span>)</span>

 更多改进

支持紧凑数字格式

工厂方法被添加到 NumberFormat 类中,以便根据 Unicode 标准将数字格式化为紧凑的、人类可读的形式。有 SHORT 和 LONG 格式可用,示例如下所示:

<span id="1a6e" data-selectable-paragraph=""><span>NumberFormat</span> <span>shortFormat</span> <span>=</span> NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.SHORT);<br>System.out.println(shortFormat.format(<span>1000</span>))<br><br><span>NumberFormat</span> <span>longFormat</span> <span>=</span> NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);<br>System.out.println(shortFormat.format(<span>1000</span>))</span>
<span id="34d4" data-selectable-paragraph=""><br>1K<br><span>1</span> thousand</span>

 添加了日间支持

新模式“B”将添加到 DateTime 模式中,允许它指定一天中的时间。

<span id="c5e6" data-selectable-paragraph=""><span>DateTimeFormatter</span> <span>timeOfDayFomatter</span> <span>=</span> DateTimeFormatter.ofPattern(<span>"B"</span>);<br>System.out.println(timeOfDayFomatter.format(LocalTime.of(<span>8</span>, <span>0</span>)));<br>System.out.println(timeOfDayFomatter.format(LocalTime.of(<span>13</span>, <span>0</span>)));<br>System.out.println(timeOfDayFomatter.format(LocalTime.of(<span>20</span>, <span>0</span>)));<br>System.out.println(timeOfDayFomatter.format(LocalTime.of(<span>23</span>, <span>0</span>)));<br>System.out.println(timeOfDayFomatter.format(LocalTime.of(<span>0</span>, <span>0</span>)));</span>
<span id="948b" data-selectable-paragraph=""><br>in the morning<br>in the afternoon<br>in the evening<br>at night<br>midnight</span>

 性能基准

Java 17 在内存使用和时间复杂度方面也比 Java 11 有所改进。一个这样的基准测试已经完成,他们通过让它们执行一系列任务来计算在两个版本中编写的代码的性能。

完整的结果和任务描述可以在这里找到。

已经注意到的一些一般结果是:

  1. 对于 G1GC(默认垃圾回收器),Java 17 比 Java 11 快 8.66%,比 Java 16 快 2.41%。

2. Java 17 比 Java 11 快 6.54%,比 Java 16 快 0.37% 用于 ParallelGC(Parallel Garbage Collector)。

3. 并行垃圾回收器(Java 17 可用)比 G1 垃圾回收器(Java 11 使用)快 16.39%。

从 Java 11 迁移到 Java 17 可以带来许多好处,包括新功能和改进的性能。但是,必须了解迁移过程中可能出现的潜在瓶颈。

许多库也将升级到更新版本以支持 Java 17。因此,如果我们在项目中使用外部库,我们应该非常小心。

通过了解这些潜在问题并采取必要的步骤来解决这些问题,您可以确保顺利成功地迁移到 Java 17。祝您编码愉快!