Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java18 的新特性

你好,我是看山。

本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。

从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。让我们跟随 Java 的脚步,配合示例讲解,看一看每个版本的新特性,本期是 Java18 的新特性。

概述

Java18 是在 2022 年 3 月 22 日正式发布,有9大特性:

  • JEP 400: 使用 UTF-8 作为默认字符集
  • JEP 408: 简单的网络服务器
  • JEP 413: Java API 文档中的代码片段
  • JEP 416: 使用方法句柄重新实现核心反射机制
  • JEP 417: Vector API(第三次孵化)
  • JEP 418: Internet-Address地址解析 SPI
  • JEP 419: 外部函数和内存 API (第二次孵化)
  • JEP 420: switch 模式匹配(第二次预览)
  • JEP 421: 弃用 Finalization

接下来我们一起看看这些特性。

JEP 400: 使用 UTF-8 作为默认字符集

Java从出生目标就是跨平台使用,“一处编码,处处运行”,所以有一些适配不同平台的逻辑,字符集就是其中一个。在Java 18之前,Java的默认字符集是基于系统环境的。

可以通过命令查看:

java -XshowSettings:properties -version 2>&1 | grep file.encoding

或者:

Charset.defaultCharset()

但是在跨平台使用时,这种特性可能导致字符编码的问题。

比如:

  • 代码的编译字符集是UTF-8,运行字符集是其他的,假设是输出中文字符,比如System.out.println("你好");,在Mac和Windows上表现就会不一致,在Windows上就会出现乱码,这是因为,在Java18之前,Windows默认字符集是GBK;
  • IO文件流操作,比如文件是UTF-8字符集编写,在不同环境读取文件内容,表现就会不一致。最常见的就是乱码神兽“锟斤拷”。

通过将UTF-8作为默认字符集,不再需要设置file.encoding,可以统一字符编码的处理方式,提高国际化应用的兼容性。开发者可以更方便地进行开发和部署,减少了因字符集不一致导致的调试和维护问题。

Java 18中默认使用UTF-8字符集不仅简化了开发流程,还显著提升了代码的可预测性和可移植性,同时增强了对全球化和多语言应用的支持。

这是一个ROI很高的特性。

JEP 408: 简单的网络服务器

JEP 408是为开发者提供一个轻量级、简单易用的 HTTP 服务器,通过命令行工具jwebserver可以启动一个最小化的静态 Web 服务器,这个服务器仅支持静态资源的访问,不支持 CGI(Common Gateway Interface)或类似 servlet 的功能,主要用于原型制作、测试和开发环境中的静态文件托管与共享。

它的应用场景包括:

  • 原型开发:由于其简单易用的特性,jwebserver 可以作为快速原型开发工具,帮助开发者在短时间内搭建起一个可以访问静态资源的 Web 服务。这对于需要验证某个功能或概念的初期阶段非常有用。
  • 快速部署:对于一些小规模的应用或者临时性的项目,使用 jwebserver 可以快速启动并运行一个简单的 Web 服务,而无需复杂的配置和环境搭建。这使得开发者能够迅速将想法转化为实际的可访问服务。
  • 学习与教育:jwebserver 提供了一个直观的平台,让初学者可以轻松上手 Java Web 开发。通过简单的命令行操作,用户可以快速理解 Web 服务器的工作原理及其基本配置。
  • 测试与调试:在进行 Web 应用的测试和调试时,jwebserver 可以作为一个独立的工具来提供静态文件的访问服务,从而方便开发者对应用进行测试和调试。
  • 本地开发环境:在本地开发环境中,jwebserver 可以替代传统的 Web 服务器如 Apache Tomcat 或 Nginx,为开发者提供一个轻量级的选择,以减少系统资源的占用。

我们可以简单试一下,在当前目录编写index.html:

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
    <h2>欢迎参观</h2>
    <h3><a href="https://www.howardliu.cn/">看山的小屋 howardliu.cn</a></h3>
    <p>
        一起<strong>开心</strong>学技术
    </p>
    <p>
        让我们一起<strong>扬帆起航</strong>
    </p>
</body>
</html>

运行jwebserver命令:

$ ./bin/jwebserver

Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /Users/liuxinghao/Library/Java/JavaVirtualMachines/temurin-18.0.2.1/Contents/Home and subdirectories on 127.0.0.1 port 8000
URL http://127.0.0.1:8000/

打开http://127.0.0.1:8000/就可以直接看到index.html的效果:

看山的小屋 howardliu.cn

jwebserver还支持指定地址和端口等参数,具体使用可以通过命令查看:

$ ./bin/jwebserver -h
Usage: jwebserver [-b bind address] [-p port] [-d directory]
                  [-o none|info|verbose] [-h to show options]
                  [-version to show version information]
Options:
-b, --bind-address    - Address to bind to. Default: 127.0.0.1 (loopback).
                        For all interfaces use "-b 0.0.0.0" or "-b ::".
-d, --directory       - Directory to serve. Default: current directory.
-o, --output          - Output format. none|info|verbose. Default: info.
-p, --port            - Port to listen on. Default: 8000.
-h, -?, --help        - Prints this help message and exits.
-version, --version   - Prints version information and exits.

JEP 413: Java API 文档中的代码片段

JEP 413是Java 18中的一个新特性,旨在简化在Java API文档中嵌入示例源代码的过程。这一特性通过引入一个新的@snippet标签来实现,该标签可以用于标准的JavaDoc生成器。

在JEP 413之前,要在Java代码注释中添加示例代码非常麻烦,通常需要使用复杂的字符转义或预定义的标记(如 {@code ...}标记或<pre>标签),这不仅增加了编写和维护文档的复杂性,还可能导致文档中的代码片段无法正确显示。而JEP 413通过引入@snippet标签,使得在API文档中嵌入示例源代码变得更加简单和直观。

@snippet标签有两种用法:

  1. 内联片段(inline snippet),即代码片段直接包含在标签内;
    /**
    * The following code shows how to use {@code Optional.isPresent}:
    * {@snippet :
    * if (v.isPresent()) {
    *     System.out.println("v: " + v.get());
    * }
    * }
    */
  2. 外部片段(external snippet),即代码片段从单独的源文件中读取。
    /**
    * The following code shows how to use {@code Optional.isPresent}:
    * {@snippet file="ShowOptional.java" region="example"}
    */
    引用代码如下:
    public class ShowOptional {
     void show(Optional<String> v) {
         // @start region="example"
         if (v.isPresent()) {
             System.out.println("v: " + v.get());
         }
         // @end
     }
    }

需要注意的,引入外部片段的时候,file参数指定的文件通常位于与源文件同一层级下的一级目录中的snippet-files目录内。

JEP 413在Java API文档的增强主要是下面几个方面:

  • 引入@snippet JavaDoc标签:JEP 413引入了一个新的标准Doclet标签@snippetJavaDoc,这使得在API文档中嵌入示例源代码变得更加简单。这个标签允许开发者直接在Javadoc注释中包含可编译的源代码片段,从而提高了文档的可读性和实用性。
  • 简化代码示例的嵌入:在JEP 413之前,要在JavaDoc中添加代码示例通常需要使用复杂的标记,如
    标签或通过@code标签包裹代码段。而JEP 413通过简化这些过程,使得开发者可以更方便地在文档中嵌入代码示例。
  • 增强验证和维护能力:通过提供对源代码片段的API访问,JEP 413不仅简化了文档的编写,还增强了源代码片段的验证能力。尽管最终正确性仍由作者负责,但新的工具支持使得实现这一目标变得更加容易。
  • 语法高亮和格式化支持:与之前的版本相比,JEP 413不仅支持代码片段的嵌入,还提供了语法高亮和格式化的功能。这意味着在生成的Javadoc文档中,代码示例会以更清晰、易读的方式展示,从而提高文档的可读性和用户体验。
  • 提升用户体验:JEP 413的引入大大改善了在JavaDoc中嵌入代码示例的体验。例如,在之前版本中,开发者需要手动处理大量的HTML和XML标记,而JEP 413则通过标准化的方式减少了这些工作量。
  • 提高开发效率和代码质量:通过使用规范且更新及时的API文档,开发者可以更快地理解和使用库中的功能。这种清晰的文档有助于减少错误和混淆,从而提高整体的开发效率和代码质量。

我只有在写基础工具的时候才会写这方面的实力,其他时候使用普通注释就行了。所以,这个功能使用的概率比较小,大家了解下就好。🐶

JEP 416: 使用方法句柄重新实现核心反射机制

JEP 416 是 Java18 中的一个重要新特性,其核心内容是使用方法句柄(Method Handles)重新实现 Java 核心反射机制。具体来说,它在 java.lang.invoke 的方法句柄之上重构了 java.lang.reflect.Method 、Constructor 和 Field 等类的实现逻辑。

JEP 416主要目标是把反射API与JVM内部结构解耦,便于维护和更新,降低成本和复杂度,而且需要兼顾性能和兼容性。

这一改动主要体现在以下几个方面:

  1. 性能提升:通过使用方法句柄来处理反射操作,Java18 显著提高了反射操作的性能和速度。这意味着在进行反射操作时,如调用方法、构造函数或访问字段等,可以更快地完成这些操作。
  2. 代码量减少:由于方法句柄的引入,开发者在编写反射相关的代码时,可以更简洁地表达意图,从而减少代码量。这不仅使得代码更加清晰易读,也进一步提升了开发效率。
  3. 无需修改现有 API:这项改动不会影响现有的 Java 反射 API,因此开发者无需对现有的反射相关代码进行修改即可体验到性能上的提升。这对于已经在使用反射功能的项目来说是一个非常友好的特性。
  4. 重构核心反射机制:在 java.lang.invoke 的方法句柄之上,Java 18 对 java.lang.reflect.Method 、Constructor 和 Field 等类进行了重构,使得它们的实现逻辑更加高效。这种重构不仅优化了内部实现,还可能带来一些未预见的性能提升。

这项改动不会改变现有的反射相关 API,因此开发者可以在不修改现有代码的情况下体验到更好的反射性能。总的来说,JEP 416 的引入旨在简化 Java 反射机制的维护工作,并提升其运行效率。

官方给的基准测试数据如下:

基准线(Java18 之前实现)

Benchmark                                     Mode  Cnt   Score  Error  Units
ReflectionSpeedBenchmark.constructorConst     avgt   10  68.049 ± 0.872  ns/op
ReflectionSpeedBenchmark.constructorPoly      avgt   10  94.132 ± 1.805  ns/op
ReflectionSpeedBenchmark.constructorVar       avgt   10  64.543 ± 0.799  ns/op
ReflectionSpeedBenchmark.instanceFieldConst   avgt   10  35.361 ± 0.492  ns/op
ReflectionSpeedBenchmark.instanceFieldPoly    avgt   10  67.089 ± 3.288  ns/op
ReflectionSpeedBenchmark.instanceFieldVar     avgt   10  35.745 ± 0.554  ns/op
ReflectionSpeedBenchmark.instanceMethodConst  avgt   10  77.925 ± 2.026  ns/op
ReflectionSpeedBenchmark.instanceMethodPoly   avgt   10  96.094 ± 2.269  ns/op
ReflectionSpeedBenchmark.instanceMethodVar    avgt   10  80.002 ± 4.267  ns/op
ReflectionSpeedBenchmark.staticFieldConst     avgt   10  33.442 ± 2.659  ns/op
ReflectionSpeedBenchmark.staticFieldPoly      avgt   10  51.918 ± 1.522  ns/op
ReflectionSpeedBenchmark.staticFieldVar       avgt   10  33.967 ± 0.451  ns/op
ReflectionSpeedBenchmark.staticMethodConst    avgt   10  75.380 ± 1.660  ns/op
ReflectionSpeedBenchmark.staticMethodPoly     avgt   10  93.553 ± 1.037  ns/op
ReflectionSpeedBenchmark.staticMethodVar      avgt   10  76.728 ± 1.614  ns/op

当前(Java18 实现)

Benchmark                                     Mode  Cnt    Score   Error  Units
ReflectionSpeedBenchmark.constructorConst     avgt   10   32.392 ± 0.473  ns/op
ReflectionSpeedBenchmark.constructorPoly      avgt   10  113.947 ± 1.205  ns/op
ReflectionSpeedBenchmark.constructorVar       avgt   10   76.885 ± 1.128  ns/op
ReflectionSpeedBenchmark.instanceFieldConst   avgt   10   18.569 ± 0.161  ns/op
ReflectionSpeedBenchmark.instanceFieldPoly    avgt   10   98.671 ± 2.015  ns/op
ReflectionSpeedBenchmark.instanceFieldVar     avgt   10   54.193 ± 3.510  ns/op
ReflectionSpeedBenchmark.instanceMethodConst  avgt   10   33.421 ± 0.406  ns/op
ReflectionSpeedBenchmark.instanceMethodPoly   avgt   10  109.129 ± 1.959  ns/op
ReflectionSpeedBenchmark.instanceMethodVar    avgt   10   90.420 ± 2.187  ns/op
ReflectionSpeedBenchmark.staticFieldConst     avgt   10   19.080 ± 0.179  ns/op
ReflectionSpeedBenchmark.staticFieldPoly      avgt   10   92.130 ± 2.729  ns/op
ReflectionSpeedBenchmark.staticFieldVar       avgt   10   53.899 ± 1.051  ns/op
ReflectionSpeedBenchmark.staticMethodConst    avgt   10   35.907 ± 0.456  ns/op
ReflectionSpeedBenchmark.staticMethodPoly     avgt   10  102.895 ± 1.604  ns/op
ReflectionSpeedBenchmark.staticMethodVar      avgt   10   82.123 ± 0.629  ns/op

从基准测试来看,新实现有好有坏,效果差别不大,基本符合新扩展便于维护又对性能没有明显影响的预期。

JEP 418: Internet-Address 地址解析 SPI

JEP 418旨在改进Internet-Address 地址解析的灵活性和可配置性。具体来说,它引入了一个服务提供者接口(SPI),使得java.net.InetAddress可以使用平台内置解析器之外的第三方解析器。

在Java18之前版本中,Internet-Address 地址解析主要依赖于内置的解析器,这些解析器通常会结合本地的hosts文件和DNS来解析主机名和IP地址。然而,这种设计限制了开发者对解析器的选择和定制能力。通过引入SPI,JEP 418允许开发者自定义网络地址解析策略,从而提高了系统的灵活性和可扩展性。

例如,开发者可以通过实现SPI接口,创建自己的解析器,并将其注册到系统中,这样java.net.InetAddress.getByName()方法就可以使用这些自定义解析器来进行地址解析了。这不仅增强了系统的灵活性,还为特定需求提供了更多的选择和控制。

预览功能

JEP 420: switch 模式匹配(第二次预览)

switch 模式匹配第一次预览是在Java17中(JEP 406,参见Java17 的新特性),旨在通过引入模式匹配来增强Java编程语言的表达能力,使得在switch语句和表达式中可以更灵活地进行条件判断和分支选择。

JEP 420允许在switch的选择器表达式中使用任何引用类型,并且case标签可以包含模式,包括条件模式。这意味着开发者可以在switch语句中直接匹配变量的不同状态或类型,从而简化代码并提高其可读性和健壮性。

基于这个特性,我们可以将一个复杂的if-else结构转换为更加简洁的switch模式匹配结构,这不仅提高了代码的可维护性,还减少了出错的可能性。

此外,JEP 420还扩展了模式匹配的语言特性,使得开发者能够对数据进行更复杂、更直观的查询和操作。这种改进特别适用于处理多样的数据结构和复杂的数据查询场景,有助于提升开发效率和代码质量。

总结而言,JEP 420通过引入switch模式匹配,进一步丰富了Java的控制流结构,使代码编写更加高效和易懂。

在JEP 420中,switch语句的模式匹配语法如下:

switch (表达式) {
case 模式1 -> 操作1;
case 模式2 -> 操作2;
// 可以有多个case子句
default -> 默认操作;
}

孵化功能

JEP 417: Vector API(第三次孵化)

Vector 向量计算 API 是在 Java16 引入(JEP 338,参见 Java16 的新特性),可以在运行时借助 CPU 向量运算指令,实现更优的计算能力。在 Java17 第二次孵化(JEP 414,参见Java17 的新特性),针对性能和实现进行了改进,包括字节向量与布尔数组之间进行转换。

Vector 向量计算 API是一个为了高性能计算设计的API,旨在利用CPU的向量指令集来优化性能,特别是在科学计算领域。其目标是提供一种更高效的编程方式,为Java语言的未来发展奠定基础。

Vector 向量计算 API在最新的Java23中还在孵化,一时半会还用不了,我们就持续关注着吧。

JEP 419: 外部函数和内存 API (第二次孵化)

外部函数和内存 API第一次孵化是在Java17中(JEP 412,参见Java17 的新特性)。

JEP 419旨在使Java程序能够更安全、高效地与本地代码和数据进行互操作。通过这个API,开发者可以调用JVM之外的外部函数,并且能够安全地访问不受JVM管理的外部内存。

具体来说,该API定义了一系列类和接口,使得客户端代码可以在库和应用程序中分配外部内存(如MemorySegment、MemoryAddress和SegmentAllocator)、操作和访问结构化外部内存(如MemoryLayout和VarHandle),并管理外部资源的生命周期。这使得Java程序无需使用JNI(Java Native Interface)即可直接调用本地库和处理本地数据,从而避免了JNI带来的潜在风险和复杂性。

通过更加优雅的方式访问外部函数是从 Java14 开始的,经历了多个孵化版本:

  • Java14 的 JEP 370:外部存储器访问 API(孵化)
  • Java15 的 JEP 383:外部存储器访问 API(第二版孵化功能)
  • Java16 的 JEP 389:外部链接器 API(孵化功能)
  • Java16 的 JEP 393:外部存储器访问 API(第三版孵化功能)
  • Java17 的 JEP 412:外部函数和内存 API

可以看出来,虽然一直在孵化,但是功能越来越强大了。

弃用和删除

JEP 421: 弃用 Finalization

将 Finalization 标记为过期:@Deprecated(forRemoval=true),具体API包括:

  • java.lang.Object.finalize()
  • java.lang.Enum.finalize()
  • java.awt.Graphics.finalize()
  • java.awt.PrintJob.finalize()
  • java.util.concurrent.ThreadPoolExecutor.finalize()
  • javax.imageio.spi.ServiceRegistry.finalize()
  • javax.imageio.stream.FileCacheImageInputStream.finalize()
  • javax.imageio.stream.FileImageInputStream.finalize()
  • javax.imageio.stream.FileImageOutputStream.finalize()
  • javax.imageio.stream.ImageInputStreamImpl.finalize()
  • javax.imageio.stream.MemoryCacheImageInputStream.finalize()

了解过JVM GC逻辑的都知道,finalize虽然提供了资源回收,但其实并不可靠和安全,为了规范使用,开始主键淘汰这些过期的设计方案。

关于资源回收,推荐使用 try-with-resources 或者 java.lang.ref.Cleaner,这些都是简单直接高效的回收方式。

我们应该在日常开发中,应该有意识的写优雅的代码。代码功力,没有武功秘笈可以在短时间快速提升,都是日常一行一行积攒的,量变引起质变,低质量的编码,是无法引起质变的。

你我共勉。

文末总结

本文介绍了 Java18 新增的特性,完整的特性清单可以从 https://openjdk.org/projects/jdk/18/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。

青山不改,绿水长流,我们下次见。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java18 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java18 的新特性

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋