Java17-教程-续-九-

157 阅读28分钟

Java17 教程·续(九)

原文:More Java 17

协议:CC BY-NC-SA 4.0

十三、自定义运行时映像

在本章中,您将学习:

  • 什么是自定义运行时映像和 JIMAGE 格式

  • 如何使用jlink工具创建自定义运行时映像

  • 如何指定命令名来运行存储在自定义映像中的应用程序

  • 如何通过jlink工具使用插件

什么是自定义运行时映像?

在 JDK9 之前,Java 运行时映像是一个巨大的整体构件——因此增加了下载时间、启动时间和内存占用。单片 JRE 使得在内存很少的设备上使用 Java 成为不可能。如果您将 Java 应用程序部署到云中,您需要为使用的内存付费;最常见的情况是,单片 JRE 会使用比所需更多的内存,从而让您为云服务支付更多的费用。有了 Java,现在可以通过允许您将 JRE 的一个子集打包到一个定制的运行时映像(称为 compact profile )中来减少 JRE 的大小,从而减少运行时内存的占用。

JDK 本身是模块化的,但是您也可以将您的应用程序代码打包成模块,并将所需的 JDK 模块和应用程序模块合并在一起。实现这一点的方法是创建一个自定义运行时,它将包含您的应用程序模块,并且只包含您的应用程序使用的那些 JDK 模块。您还可以在运行时映像中打包本机命令。创建运行时映像的另一个好处是,您只需向应用程序用户提供一个包——运行时映像。他们不再需要下载和安装单独的 JRE 包来运行您的应用程序。

运行时映像以一种称为 JIMAGE 的特殊格式存储,这种格式针对空间和速度进行了优化。只有在运行时才支持 JIMAGE 格式。它是一种容器格式,用于存储和索引 JDK 中的模块、类和资源。从 JIMAGE 文件中搜索和加载类要比从 JAR 和 JMOD 文件中快得多。JIMAGE 格式是 JDK 内部的,开发人员很少需要直接与 JIMAGE 文件交互。

JIMAGE 格式预计会随着时间的推移而显著发展,因此,它的内部结构不会向开发人员公开。JDK 附带了一个名为jimage的工具,可以用来浏览 JIMAGE 文件。我将在本章的单独一节中详细解释这个工具。

Note

您可以使用 jlink 工具创建一个定制的运行时映像,该映像使用一种称为 JIMAGE 的新文件格式来存储模块。Java 附带了 jimage 工具,可以让您探索 JIMAGE 文件的内容。

创建自定义运行时映像

您可以使用jlink工具创建一个定制的特定于平台的运行时映像。运行时映像将包含指定的应用程序模块及其依赖关系,并且只包含所需的平台模块,从而减小运行时映像的大小。这对于运行在具有少量内存的嵌入式设备上的应用程序非常有用。jlink刀具位于JDK_HOME\bin目录中。运行jlink工具的一般语法如下:

jlink <options> --module-path <modulepath> ^
    --add-modules <mods> --output <path>

这里的<options>包括零个或多个jlink选项,如表 13-1 所列。<modulepath>是平台和应用模块所在的模块路径。模块可以位于模块化 jar、展开的目录和 JMOD 文件中。<mods>是要添加到映像的模块列表,由于对其他模块的传递依赖性,这可能会导致添加其他模块。<path>是存储生成的运行时映像的输出目录。

表 13-1

jlink 工具的选项列表

|

[计]选项

|

描述

| | --- | --- | | –add-modules <mod>,<mod>... | 指定要解析的根模块列表。所有已解析的模块都将添加到运行时映像中。 | | –bind-services | 在链接过程中执行完整的服务绑定。如果添加的模块包含uses语句,jlink将扫描模块路径上的所有模块,以将运行时映像中的所有服务提供者模块包含在uses语句中指定的服务中。 | | -c, –compress | 指定输出<0&#124;1&#124;2>[:filter=<pattern-list>]图像中所有资源的压缩级别。0表示常量字符串共享,1表示 ZIP,2表示两者都有。可以指定一个可选的<pattern-list>过滤器来列出要包含的文件模式。 | | –disable-plugin <plugin-name> | 禁用指定的插件。 | | –endian <little&#124;big> | 指定生成的运行时映像的字节顺序。默认值是本机平台的字节顺序。 | | -h, –help | 打印jlink工具的用法说明和所有选项列表。 | | –ignore-signing-information | 当签名的模块化 jar 在映像中链接时,禁止出现致命错误。已签名的模块化 jar 的相关文件的签名不会被复制到运行时映像。 | | –launcher <command>=<module> | 指定模块的启动器命令。<command>是您希望生成的用于启动应用程序的命令的名称,例如runmyapp。该工具将创建一个名为<command>的脚本/批处理文件来运行<module>中记录的主类。 | | –launcher <command>= <module>/<mainclass> | 指定模块和主类的启动器命令。<command>是您希望生成的用于启动应用程序的命令的名称,例如runmyapp。该工具将创建一个名为<command>的脚本/批处理文件来运行<module>中的<main-class>。 | | –limit-modules <mod>,<mod> | 将可观察的模块限制在命名模块的传递闭包中,加上主模块(如果指定的话),以及用–add-modules选项指定的任何其他模块。 | | –list-plugins | 列出可用的插件。 | | -p, –module-path <modulepath> | 指定将平台和应用程序模块添加到运行时映像的模块路径。 | | –no-header-files | 排除本机代码的包含头文件。 | | –no-man-pages | 不包括手册页。 | | –output <path> | 指定生成的运行时映像的位置。 | | –save-opts <filename> | 将jlink选项保存在指定文件中。 | | -G, –strip-debug | 从输出图像中去除调试信息。 | | –suggest-providers [<service-name>,...] | 如果没有指定服务名,它会建议为添加的模块链接的所有服务的提供者的名称。如果指定一个或多个服务名,它会建议指定服务名的提供者。在创建映像之前可以使用此选项,以了解使用–bind-services选项时将包括哪些服务。 | | -v, –verbose | 打印详细输出。 | | –version | 打印jlink工具的版本。 | | @<filename> | 从指定文件中读取选项。 |

让我们创建一个运行时映像,它包含 prime checker 应用程序的四个模块和所需的平台模块,其中只包含java.base模块。prime checker 应用程序是在第七章中创建的,我在其中解释了如何实现服务。我在本书的源代码中包含了 prime checker 应用程序的源代码。模块有jdojo.primejdojo.prime.fasterjdojo.prime.probablejdojo.prime.client。您可以选择任何其他模块来创建自定义运行时映像。

请注意,以下命令只包括主要检查器应用程序中的三个模块。将添加第四个模块,即jdojo.prime模块,因为这三个模块依赖于jdojo.prime模块。该命令假设您已经以 JMOD 格式打包了所有四个模块,并将它们存储在jmods目录中。JMOD 格式的打包模块在第十二章中介绍。命令后面的文本包含解释。

C:\jdk17book>jlink ^
    --module-path jmods;C:\java17\jmods ^
    --add-modules ^
    jdojo.prime.client,jdojo.prime.faster,
    jdojo.prime.probable ^
    --launcher runprimechecker=
    jdojo.prime.client/com.jdojo.prime.client.Main ^
    --output image\primechecker

(在“,”和“=”后没有换行符和空格。)

在我解释这个命令的所有选项之前,让我们验证一下运行时映像是否创建成功。该命令应该将运行时映像复制到C:\jdk17book\image\primechecker目录。运行以下命令,验证运行时映像是否包含这五个模块:

C:\jdk17book>image\primechecker\bin\java ^
    --list-modules

java.base@9
jdojo.prime@1.0
jdojo.prime.client@1.0
jdojo.prime.faster@1.0
jdojo.prime.probable@1.0

如果您得到与此处所示类似的输出,则运行时映像创建正确。显示在输出中的@符号之后的模块版本号可能会因您而异。

–module-path选项指定了两个目录:jmodsC:\java17\jmods。我将 prime checker 应用程序的四个 JMOD 文件保存在C:\jdk17book\jmods目录中。模块路径中的第一个元素让jlink工具找到所有的应用模块。我将 JDK17 安装在C:\java17目录中,所以模块路径中的第二个元素让工具找到平台模块。如果不指定第二部分,将出现错误:

Error: Module java.base not found,
    required by jdojo.prime.probable

–add-modules选项指定了 prime checker 应用程序的三个模块。您可能想知道为什么我们没有用这个选项指定名为jdojo.prime的第四个模块。这个列表包含根模块,而不仅仅是要包含在运行时映像中的模块。jlink工具将为这些根模块解析所有的依赖关系,并将所有解析的依赖模块包含到运行时映像中。这三个模块依赖于jdojo.prime模块,它将通过在模块路径上定位来解析,因此将包含在运行时映像中。该映像还将包含java.base模块,因为所有应用程序模块都隐式依赖于它。

–output选项指定运行时映像将被复制到的目录。该命令会将运行时映像复制到C:\jdk17book\image\primechecker目录。输出目录包含子目录和一个名为release的文件。release文件包含 JDK 版本和链接到该映像的所有 JDK 和用户模块的列表。表 13-2 包含了每个目录的内容描述。

表 13-2

输出目录中的子目录

|

目录

|

描述

| | --- | --- | | bin | 包含可执行文件。在 Windows 上,它还包含动态链接的本地库(.dll文件)。 | | conf | 包含可编辑的配置文件,如.properties.policy文件。 | | include | 包含 C/C++头文件。 | | legal | 包含法律声明。 | | lib | 包含添加到运行时映像的模块以及其他文件。在 Mac、Linux 和 Solaris 上,它还将包含系统的动态链接库。 |

您在jlink命令中使用了–launcher选项。您指定了runprimechecker作为命令名,jdojo.prime.client作为模块名,com.jdojo.prime.client.Main作为模块中的主类名。–launcher选项使jlink创建一个特定于平台的可执行文件,比如在 Windows 的bin目录中的runprimechecker.bat文件。您可以使用这个可执行文件来运行您的应用程序。文件内容只是运行该模块中主类的包装。您可以使用该文件来运行应用程序:

C:\jdk17book>image\primechecker\bin\
    runprimechecker

Using default service provider:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.
Using faster service provider:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.
Using probable service provider:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.

(“bin”后没有换行符和空格。)

您还可以使用java命令来启动您的应用程序,该命令由jlink工具复制到bin目录中:

C:\jdk17book>image\primechecker\bin\java ^
    --module jdojo.prime.client/com.jdojo.prime.client.Main

这个命令的输出将与前一个命令的输出相同。请注意,您不必指定模块路径。创建运行时映像时,链接器,jlink工具负责管理模块路径。当您运行生成的运行时映像的java命令时,它知道在哪里可以找到模块。

绑定服务

在上一节中,您为主要服务客户端应用程序创建了一个运行时映像。您必须使用–add-modules选项指定您想要包含在映像中的所有服务提供者模块的名称。在这一节中,我将向您展示如何使用–bind-services选项和jlink工具自动绑定服务。这一次,您需要将模块,即jdojo.prime.client模块添加到模块图中。jlink工具会处理剩下的事情。jdojo.prime.client模块读取jdojo.prime模块,因此将前者添加到模块图中也会解析后者。以下命令打印运行时映像的建议服务提供者列表。显示了部分输出:

C:\jdk17book>jlink ^
    --module-path jmods;C:\java17\jmods ^
    --add-modules jdojo.prime.client --suggest-providers

...
jdojo.prime file:///C:/jdk17book/jmods/
        jdojo.prime.jmod
    uses com.jdojo.prime.PrimeChecker
jdojo.prime.client file:///C:/jdk17book/
    jmods/jdojo.prime.client.jmod
jdojo.prime.faster file:///C:/jdk17book/
    jmods/jdojo.prime.faster.jmod
jdojo.prime.probable file:///C:/jdk17book/
    jmods/jdojo.prime.probable.jmod
...
Suggested providers:
  jdojo.prime provides com.jdojo.prime.
      PrimeChecker used by jdojo.prime
  jdojo.prime.faster provides com.jdojo.prime.
      PrimeChecker used by jdojo.prime
  jdojo.prime.probable provides com.jdojo.prime.
      PrimeChecker used by jdojo.prime
...

该命令只将jdojo.prime.client模块指定给–add-modules选项。jdojo.primejava.base模块被解析,因为jdojo.prime.client模块读取它们。针对uses语句扫描所有已解析的模块,随后,针对uses语句中指定的服务扫描模块路径中的所有模块的服务提供者。将打印找到的所有服务提供商。

Note

您可以为–suggest-providers选项指定参数。如果使用不带参数的命令,请确保在命令末尾指定它。否则,–suggest-providers选项之后的选项将被解释为它的参数,您将收到一个错误。

以下命令将com.jdojo.prime.PrimeChecker指定为–suggest-providers选项的服务名,以打印为此服务找到的所有服务提供者:

C:\jdk17book>jlink ^
    --module-path jmods;C:\java17\jmods ^
    --add-modules jdojo.prime.client ^
    --suggest-providers ^
    com.jdojo.prime.PrimeChecker
Suggested providers:
  jdojo.prime provides com.jdojo.prime.
      PrimeChecker used by jdojo.prime
  jdojo.prime.faster provides com.jdojo.prime.
      PrimeChecker used by jdojo.prime
  jdojo.prime.probable provides com.jdojo.prime.
      PrimeChecker used by jdojo.prime

使用与前面描述的相同的逻辑,找到了所有三个服务提供商。让我们创建一个包含所有三个服务提供者的新运行时映像。以下命令可以完成这项工作:

C:\jdk17book>jlink ^
    --module-path jmods;C:\java17\jmods ^
    --add-modules jdojo.prime.client ^
    --launcher runprimechecker=
        jdojo.prime.client/com.jdojo.prime.client.Main ^
    --bind-services
    --output image\primecheckerservice

(在“=”后没有换行符和空格。)

将此命令与上一节中使用的命令进行比较。这一次,您用–add-modules选项只指定了一个模块。也就是说,您不必指定服务提供者模块的名称。您使用了–bind-services选项,所以添加的模块中的所有服务提供者引用都会自动添加到运行时映像中。您指定了一个名为image\T3 的新输出目录。以下命令运行新创建的运行时映像:

C:\jdk17book>image\primecheckerservice\bin\
    runprimechecker

Using default service provider:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.
Using faster service provider:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime. 

Using probable service provider:
3 is a prime.
4 is not a prime.
121 is not a prime.
977 is a prime.

(“bin”后没有换行符和空格。)

输出证明了模块路径中的所有三个 prime checker 服务提供程序都被自动添加到了运行时映像中。

在前面的命令中使用–bind-services选项时,有一个问题。比较一下image\primecheckerimage\primecheckerservice目录的大小,分别是 173MB 和 36MB。您确实使用了一个较短的命令。然而,运行时映像的大小增加了 280。你不想这样。问题在于使用解决所有服务的–bind-services选项,包括java.base模块。除了在jdojo.prime模块中定义的com.jdojo.prime.PrimeChecker服务之外,您不想解析任何其他服务。您可以通过使用–limit-modules选项将可观察模块的范围限制为以下五个模块来实现这一点:

  • java.base

  • jdojo.prime

  • jdojo.prime.faster

  • jdojo.prime.probable

  • jdojo.prime.client

以下命令是前一个命令的修订版。该命令使用–limit-modules。请注意,您没有将jdojo.prime.client模块包含在–list-modules中,因为该模块已经包含在–add-modules中。将它包含在带有–list-modules的模块列表中不会有任何区别。这一次,您的运行时映像将像第一次一样是 36MB。

C:\jdk17book>jlink ^
    --module-path jmods;C:\java17\jmods ^
    --add-modules jdojo.prime.client ^
    --compress 2 ^
    --strip-debug ^
    --launcher runprimechecker=
      jdojo.prime.client/com.jdojo.prime.client.Main ^
    --bind-services ^
    --limit-modules java.base,jdojo.prime,
      jdojo.prime.faster,jdojo.prime.probable ^
--output image\image\primecheckercompactservice

(在“=”或“,”后没有换行符和空格。)

通过 jlink 工具使用插件

jlink工具使用插件架构来创建运行时映像。它将所有的类、本地库和配置文件收集到一组资源中。它构建了一个转换器管道,这些转换器是被指定为命令行选项的插件。资源被输入管道。管道中的每个转换器对资源进行某种转换,转换后的资源被提供给下一个转换器。最后,jlink将转换后的资源提供给图像生成器。

JDK 发布了带有一些插件的jlink工具。这些插件定义了命令行选项。要使用一个插件,你需要使用命令行选项。您可以运行带有–list-plugins选项的jlink工具来打印所有可用插件的列表及其描述和命令行选项:

C:\jdk17book>jlink --list-plugins

...

下面的命令使用了compressstrip-debug插件。compress插件将压缩图像,这将导致一个较小的图像尺寸。我使用压缩级别 2 来获得最大压缩。strip-debug插件将从 Java 代码中删除调试信息,从而进一步减小图像的大小。

C:\jdk17book>jlink ^
    --module-path jmods;C:\java17\jmods ^
    --add-modules jdojo.prime.client,
      jdojo.prime.faster,jdojo.prime.probable ^
    --compress 2 ^
    --strip-debug ^
    --launcher runprimechecker=
      jdojo.prime.client/com.jdojo.prime.client.Main ^
    --output image\primecheckercompact

(在“=”或“,”后没有换行符和空格。)

输出被复制到image\primecheckercompact目录。新映像的大小是 33MB,而在image\primechecker目录中创建的映像的大小是 36MB。由于你使用了两个插件,这是大约 39%更紧凑的图像。

jimage 工具

Java 运行时在一个 JIMAGE 文件中提供了 JDK 运行时映像。文件名为modules,位于JAVA_HOME\lib,这里的JAVA_HOME可以是你的JDK_HOMEJRE_HOME。JDK9 还附带了一个jimage工具,用于探索 JIMAGE 文件的内容。该工具可以

  • 从 JIMAGE 文件中提取条目

  • 打印存储在 JIMAGE 文件中的内容摘要

  • 打印条目列表,如名称、大小、偏移量等。

  • 验证类文件

jimage刀具存放在JDK_HOME\bin目录中。该命令的一般格式如下:

jimage <subcommand> <options> <jimage-file-list>

这里,<subcommand>是表 13-3 中列出的子命令之一。<options>是表 13-4 中列出的一个或多个选项;<jimage-file-list>是要浏览的以空格分隔的 JIMAGE 文件列表。

表 13-4

与 jimage 工具一起使用的选项列表

|

[计]选项

|

描述

| | --- | --- | | –dir <dir-name> | 指定extract子命令的目标目录,JIMAGE 文件中的条目将被提取到该目录。 | | -h, –help | 打印jimage工具的使用信息。 | | –include <pattern-list> | 指定用于过滤条目的模式列表。模式列表的值是逗号分隔的元素列表,每个元素都使用以下形式之一:<glob-pattern>``glob:<glob-pattern>``regex:<regex-pattern> | | –verbose | 与list子命令一起使用时,打印条目细节,如大小、偏移量和压缩级别。 | | –version | 打印jimage工具的版本信息。 |

表 13-3

与 jimage 工具一起使用的子命令列表

|

子命令

|

描述

| | --- | --- | | Extract | 将指定的 JIMAGE 文件中的所有条目提取到当前目录。使用–dir选项为提取的条目指定另一个目录。 | | Info | 打印指定图像文件头中包含的详细信息。 | | List | 打印指定的 JIMAGE 文件中所有模块及其条目的列表。使用–verbose选项包含条目的详细信息,如大小、偏移量以及条目是否被压缩。 | | verify | 打印指定 JIMAGE 文件中未验证为类的.class条目列表。 |

我展示了几个使用jimage命令的例子。示例使用存储在我的计算机上的C:\java17\lib\modules的 JDK 运行时映像。当您运行这些示例时,您需要用您的位置替换这个图像位置。在这些例子中,您还可以使用由jlink工具创建的任何自定义运行时映像。

下面的命令从运行时映像中提取所有条目,并将它们复制到extracted_jdk目录中。该命令需要几秒钟才能完成:

C:\jdk17book>jimage extract ^
    --dir extracted_jdk C:\java17\lib\modules

以下命令将 JDK 运行时映像中扩展名为.png的所有映像条目提取到extracted_images目录中:

C:\jdk17book>jimage extract ^
    --include regex:.+\.png ^
    --dir extracted_images ^
    C:\java17\lib\modules

以下命令列出运行时映像中的所有条目。显示了部分输出:

C:\jdk17book>jimage list C:\java17\lib\modules

jimage: C:\java17\lib\modules
Module: java.activation
    META-INF/mailcap.default
    META-INF/mimetypes.default
...
Module: java.annotations.common
    javax/annotation/Generated.class
...

以下命令列出了运行时映像中的所有条目以及条目的详细信息。注意–verbose选项的使用。显示了部分输出:

C:\jdk17book>jimage list ^
    --verbose ^
    C:\java17\lib\modules

jimage: C:\java17\lib\modules
Module: java.activation
Offset     Size   Compressed Entry
34214466    292            0 META-INF/mailcap.default
34214758    562            0 META-INF/mimetypes.default
...
Module: java.annotations.common
Offset     Size   Compressed Entry
34296622    678            0 javax/annotation/
                                            Generated.class
...

以下命令打印无效的类文件列表。您可能想知道如何使一个类文件无效。通常,您不会有无效的类文件——但是黑客会有!然而,要运行这个例子,我需要一个无效的类文件。我使用了一个简单的想法——获取一个有效的类文件,在文本编辑器中打开它,并随机删除它的部分内容,使它成为一个无效的类文件。我将一个已编译的类文件的内容复制到了Main2.class文件中,并删除了它的一些内容,使它成为一个无效的类。我将Main2.class文件添加到jdojo.prime.client模块中,与Main.class在同一个目录下。对于这个例子,我使用 prime checker 应用程序的前一个命令重新创建了运行时映像。如果使用 JDK 附带的 Java 运行时映像,您将看不到任何输出,因为 JDK 运行时映像中的所有类文件都是有效的。

C:\jdk17book>jimage verify ^
    image\primechecker\lib\modules

jimage: primechecker\lib\modules
Error(s) in Class: /jdojo.prime.client/com/jdojo/prime/
    client/Main2.class

摘要

在 Java 中,运行时映像以一种称为 JIMAGE 的特殊格式存储,这种格式针对空间和速度进行了优化。只有在运行时才支持 JIMAGE 格式。它是一种容器格式,用于存储和索引 JDK 中的模块、类和资源。从 JIMAGE 文件中搜索和加载类要比从 JAR 和 JMOD 文件中快得多。JIMAGE 格式是 JDK 内部的,开发人员很少需要直接与 JIMAGE 文件交互。

JDK 附带了一个名为jlink的工具,它允许您为您的应用程序创建一个 JIMAGE 格式的运行时映像,该映像将包含应用程序模块,并且只包含您的应用程序使用的那些平台模块。jlink工具可以从存储在模块 jar、展开的目录和 JMOD 文件中的模块创建运行时映像。JDK 附带了一个叫做jimage的工具,可以用来探索 JIMAGE 文件的内容。

练习

练习 1

什么是自定义 Java 运行时映像?

练习 2

什么是 JIMAGE 格式?

运动 3

什么是jlink工具?

演习 4

为什么要将–launcher选项与jlink工具一起使用?

锻炼 5

使用或不使用jlink工具的–bind-services选项有什么影响?

锻炼 6

jlink工具有哪些插件?

锻炼 7

如何列出jlink可用的插件?

运动 8

说出两个jlink插件。

演习 9

可以用jlink自定义插件吗?

运动 10

什么是jimage工具?描述jimage工具的以下四个子命令的使用:extractinfolistverify

十四、杂项

在本章中,您将学习:

  • 与前一版相比,章节的选择

  • JDK9 之后的各种增强

删除了以前版本中的章节

如果你将这本书与之前的版本进行比较,即 Java 语言特性,第二版,和Java API,扩展和库,第二版,你可能会错过一些没有进入新版本的章节。这样做的主要原因是我们不希望这本新书太大,它是上述书籍的合并。

问题当然是:你如何决定省略哪些章节?两个第二版书中的章节没有一个是真正过时的,或者是无趣的。决策点不容易找出来,但是作者决定考虑两个方面。首先,如果一个主题太属于开发人员的标准知识,并且关于它的信息可以很容易地在 Oracle 的文档(包括教程)中找到,那么第三版中有一章将被删除。第二,如果一个主题不会在开发人员的日常工作中经常出现,因此属于可能的主题的角落类型,那么新版本中的一章也会被删除。

具体来说,章节选择的基本原理是

  • 没有内部类:非常标准——读者可以很容易地在 Oracle 文档和网上其他地方找到细节。

  • 没有输入/输出,也不处理归档文件:对于服务器端开发来说,这并不重要。应用程序和信息的标准内容可以很容易地在 Oracle 文档和网络上的其他地方找到。

  • 没有垃圾收集,也没有关于堆栈审核的细节:很有趣,但是对于日常工作来说,这样的细节并不重要。介绍性的文章很容易在网上找到。

  • 不收集:重要,但相当标准的东西和许多介绍性文本的主题。

  • 没有模块 API:开发人员很少需要使用它。

  • 无反应流:虽然有点“in”,但它实际上是一个 Java 库主题,在 Java 标准中只有几个螺栓。如果你想了解它,最好参考 reactive streams 库项目。

  • 没有 JDBC:一个连贯的话题,在网上有很好的记录。企业项目开发人员几乎从不直接使用 JDBC。

  • 没有 Java 原生接口:在某种程度上违背了 Java 哲学(编写一次,在任何地方运行),因此这是一个死角。

  • 没有 Swing 就没有 JavaFX:前端开发是一个如此庞大的主题,最好在专业书籍中处理。

更多 JDK17 新奇事物

在这一节中,我将展示一些有用的或有趣的新奇事物,这些新奇事物是在本书之前版本的基础上进入 Java 世界的。或者,在版本号中,它描述了自 JDK9 以来发生的事情。这个列表并不详尽——一些涉及内部的或者在开发人员的日常工作中不太重要的变更被忽略了。

具有自动类型的局部变量

脚本语言,包括那些最终在 JVM 上运行的语言,通常允许简化的未定义的类型变量声明,以简化代码编写。例如,在 Groovy 中,这将是def,如

// This is Groovy
def a = "Hello"
def b = 3
def c = 5.9

在 JavaScript 中,您可以编写

// This is JavaScript
var a = "Hello";
var b = 3;
var c = 5.9;

Java 语言开发人员长期以来坚持为 Java 提供精确类型的变量声明。只有在后来的 Java 版本中,这一点有所放松,并且可以使用var作为局部变量的类型占位符。所以你可以写

// This is Java, inside a method
var a = "Hello";
var b = 3;
var c = 5.9;

必须初始化var变量,不能对类字段使用var,也不能切换值类型:

public class Car {
  private var a = "Hello";
  // <- Won't compile
}

...

var a;
a = 3;
// <- Won't compile

...

var b = 3;
b = 5.9;
// <-Won't compile

对于较长的类型,var局部变量语法非常方便,您也可以将它用于 lambda 参数:

var x = new ArrayList<String>();
   // <-x has type ArrayList

   Function<String,Integer> fsi = (var s) -> s.length();

Caution

不要过度使用var局部变量语法。毕竟,干净的代码编程状态也是关于表达性的,隐藏类型信息使得复杂的代码几乎不可读。

启动单文件源代码程序

对于非常短的程序,只包含一个带有static void main(String[] args)方法的类,您可以绕过编译步骤,直接编写

java HelloTest.java
  # or, if we need args
  java HelloTest.java arg1 arg2 ...

这将执行内存编译,然后运行main()方法。

增强的switch语句

古老可敬的switch声明

int x = ...;
  switch(x) {
    case 1:
    case 2:
      System.out.println("1 or 2");
      break;
    case 3:
      System.out.println("3");
      fbreak;
    default:
      System.out.println("default");
  }

缺少许多其他编程语言都包含的特性:在 Java 中,不能使用switch{ }作为表达式。此外,虽然在某些场景中很有用,但是如果不小心忘记了break,这种失败机制(前面例子中case 1:缺少的break)更容易导致错误。出于这个原因,Java 现在有了一个语法略有不同的新变体,用->代替了case子语句中的::

var a = 5;
switch(a) {
    case 4 -> System.out.println("4");
    default -> System.out.println("default: " + a);
}
var b = switch(a) {
    case 4 -> -1;
    default -> a;
};
var c = switch(a) {
    case 4 -> -1;
    default-> {
        var x9 = a*2;
        yield x9; // goes to c
    }
};

因此,如果没有break,就不再存在失败。事实上,break s 已经过时了,不能用了。使用这种语法,switch可以有一个值,从而被用作表达式。新的yield定义了switch的结果,以防你需要{ ... }模块进行更长时间的计算。

当然,如果不需要新的行为,您仍然可以使用旧的语法。

文本块

在 Java 中,多行字符串总是令人讨厌。大多数开发人员使用类似

  String s = "This is the first line\n" +
      "This is the second line\n" +
      "This is the third line";

输入多行字符串。一个新特性允许更简洁地输入多行字符串:

String s = """
      This is the first line
      This is the second line
      This is the third line
  """;

然而,还有一个问题。如果您将最后一个字符串写入控制台,您将看到如下内容

This is the first line
This is the second line
This is the third line

每行前面有六个空格。

当然,你可以这样写

String s = """
This is the first line
This is the second line
This is the third line
  """;

以避免不必要的压痕。然而,这个解决方案打破了你的源代码的缩进结构。作为补救措施,方法stripIndent()被添加到了String类中:

  String s = """
      This is the first line
      This is the second line
      This is the third line
  """.stripIndent();

输出:

This is the first line
This is the second line
This is the third line

如果结果字符串中不需要换行符,可以使用反斜杠字符对行尾进行转义:

  String s = """
      This is the first line \
      Still inside the same line \
      Still inside the same line
  """;

只要确保每个反斜杠是每个输入行的最后一个字符。

增强型instanceof运算符

通常的instanceof操作符有一个新的样板代码——避免变量,允许立即将有问题的对象赋给正确类型的变量。所以,与其写作

Object s = "Hello";
...
if(s instanceof String) {
    String str = (String) s;
    if(str.equalsIgnoreCase("hello")) {
        System.out.println("Hello String!");
    }
}

你可以更简洁地写

Object s = "Hello";
...
if(s instanceof String str) {
    // use local variable 'str', which
    // is of type String
}
...
if(s instanceof String str &&
        str.equalsIgnoreCase("hello")) {
    System.out.println("Hello String!");
}

值分类:记录

值对象是主要目的是保存一堆值的对象。在传统的 Java 中,您应该编写如下的类

package jdk17;

import java.time.LocalDate;
import java.util.Objects;

public class Person {
    private String firstName;
    private String lastName;
    private LocalDate birthDay;
    private String socialSecurityNumber;

    @Override
    public int hashCode() {
        return Objects.hash(birthDay, firstName, lastName,
            socialSecurityNumber);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        return Objects.equals(birthDay, other.birthDay)
                && Objects.equals(firstName,
                       other.firstName)
                && Objects.equals(lastName, other.lastName)
                && Objects.equals(socialSecurityNumber,
                       other.socialSecurityNumber);
    }

    @Override
    public String toString() {
        return "Person [firstName=" + firstName +
            ", lastName=" + lastName +
            ", birthDay=" + birthDay +
            ", socialSecurityNumber=" +
                    socialSecurityNumber + "]";
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public LocalDate getBirthDay() {
        return birthDay;
    }

    public void setBirthDay(LocalDate birthDay) {
        this.birthDay = birthDay;
    }

    public String getSocialSecurityNumber() {
        return socialSecurityNumber;
    }

    public void setSocialSecurityNumber(String
          socialSecurityNumber) {
        this.socialSecurityNumber =
            socialSecurityNumber;
    }
}

对于一个值的对象。这个类包含许多样板代码—实际上,这个类包含的所有信息都是由它的字段给出的:

private String firstName;
private String lastName;
private LocalDate birthDay;
private String socialSecurityNumber;

其他一切都是派生的(实际上,我让我的 Eclipse IDE 生成它)。

为了简化这些值对象的使用以及不变性的限制,在 Java 中可以使用记录:

// File Person.java
record Person(
    String firstName,
    String lastName,
    LocalDate birthDay,
    String socialSecurityNumber) {}

就是这样!其他所有东西,getters、equals()hashCode()toString()和一个构造函数,都是自动提供的。没有定义 Setters,因为记录是不可变的。

要使用这样的记录,你只需写

Person p1 = new Person(
    "John",
    "Smith",
    LocalDate.of(1997,Month.DECEMBER,30),
    "000-00-1234");
System.out.println("Name: " + p1.firstName + " " + p1.lastName);

注意,您只是使用点符号来访问成员;没有提供getXXX() getter 方法。

记录声明中的{ }块可用于在构造期间对参数施加约束。所以你可以写

// File Person.java
record Person(
    String firstName,
    String lastName,
    LocalDate birthDay,
    String socialSecurityNumber)
{
    public Person {
      if(lastName == null ||
            "".equals(lastName.trim()))
        throw new IllegalArgumentException(
          "lastName must not be empty");
    }
}

密封类

有时,您希望限制可以从给定基类继承的类的可能集合。添加关键字sealed作为修饰符,并将permits Class1, Class2, ...添加到类声明中,如

// Circle.java
final class Circle extends Shape {
    ...
}

// Rectangle.java
final class Rectangle extends Shape {
    ...
}

// Shape.java
sealed class Shape
     permits Circle, Rectangle {
    // only Circle or Rectangle can
    // inherit from Shape
    ...
}

如果需要库内部的类继承,但不希望用户类从库类继承,通常使用密封类。

摘要

如果与上一版相比,省略章节的基本原理,以及自 JDK10 以来有用或有趣的新奇事物的集合,是本书的结论。