JDK9新特性说明

发布时间 2023-11-28 21:47:11作者: 卡卡一点都不卡

1、Java 模块化

1.1、概念介绍

Java模块化相比是大家听到的最多的,也是JDK9的重大更新之一,关于什么是Java 模块系统?官方是这么解释的:
一个命名的、自我描述的代码和数据集合。
 
该模块系统包含了:
  • 一个新的可选阶段,链接时间,它位于编译时间和运行时间之间,在这个阶段,一组模块可以被组装和优化成一个定制的运行时映像;请参阅 Java Platform, Standard Edition Tools Reference 中的 jlink 工具。
  • 往javac jlink java 命令中增加了选项,可以在这些选项中指定模块路径,用于定位模块的定义。
  • 引入了模块化JAR文件,它是一个在其根目录中包含一个文件的JAR文件.module-info.class
  • 引入了JMOD格式,这是一种类似于JAR的打包格式,除了可以包含本地代码和配置文件之外;请参阅jmod工具。
 
同时,JDK本身已被划分为一组模块。改变化带来以下改变:
  • 能够将 JDK 的模块组合到各种配置中,包括:
    • 对应于 JRE 和 JDK 的配置。
    • 配置在内容上大致等同于 Java SE 8 中定义的每个 Compact Profile
    • 仅包含一组指定模块及其所需模块的自定义配置。
  • 重新构造了JDK和JRE运行时映像,以适应模块并改善性能、安全性和可维护性。
  • 为在运行时映像中命名模块、类和资源定义了一个新的URI方案,而不会揭示映像的内部结构或格式。
  • 删除了 endorsed-standards override(认可标准覆盖)机制和 extension (扩展)机制。

  • 从Java运行时映像中移除了 rt.jar 和 tools.jar

  • 默认情况下,使 JDK 的大多数内部 API 无法访问,但保留一些关键的、广泛使用的内部 API 可访问,直到它们的所有或大部分功能都存在受支持的替代品。运行 jdeps -jdkinternals 命令以确定您的代码是否使用内部 JDK API。

 

上面这部分大多数人看描述也是一头雾水,接下来让我们通过下面的例子来揭开Java模块化的面纱。
 

实战

使用IDEA,创建一个基于jdk9的maven项目

 

 
再创建两个module,一个叫做modulea,另外一个叫做moduleb。

 

在modulea中,创建一个类,名字叫做ModuleA

 

moduleb中,也创建一个叫做ModuleB的类

 

此时,假如我们想要早ModuleB这个类中,调用ModuleA的方法,按照jdk9之前的方式,肯定是先设置好pom版本依赖

 

然后在ModuleA中直接引用就行了。

 

运行后打印:

 

看起来好像没啥差别对吧?
确实,因为JDK的版本升级(最大化)保证了低版本的兼容,在JDK9版本下,这种方式定义的module叫做无名模块,用来兼容老版本升级新jdk时,未定义的情况。

无名模块

什么是无名模块呢?
无名模块有一些默认的行为和限制。它无法显示地声明依赖关系,也无法导出任何包。但是,它仍然可以访问其他模块的公共 API。
那怎么样可以把一个module定义为JDK9的module呢?
从 JDK 9 开始,Java 引入了模块化系统,每个模块都需要一个 module-info.java 文件来声明模块的信息。module-info.java 文件是模块的入口点,它包含了模块的名称、依赖关系和导出的包等信息。如果你想在 JDK 9 或更高版本中使用模块化特性,你必须在每个模块中声明一个 module-info.java 文件。

module-info.java结构和限制

module 模块名称 {    
    // 导出的包声明    
    // 依赖关系声明    
    // 其他模块特定的声明
}
各个声明定义:
  • 导出的包声明(exports):使用 exports 关键字声明模块导出的包,其他模块可以访问这些包下的公共类和接口。
  • 依赖关系声明(requires):使用 requires 关键字声明模块的依赖关系,指定该模块依赖的其他模块。
  • 其他模块特定的声明:还可以在 module-info.java 文件中添加其他模块特定的声明,如使用 provides 和 uses 关键字来实现服务提供者接口。 

module-info.java

同时 module-info.java 也是有一些其他限制
在一个模块中,只能有一个 module-info.java 文件。这个文件定义了模块的名称、依赖关系和其他模块特定的信息。如果在同一个模块中出现多个 module-info.java 文件,编译器将会报错。因此,每个模块只能有一个 module-info.java 文件。
 
module-info.java 文件应该放在一个名为 "module-name/src/main/java/module-info.java" 的目录中,其中 "module-name" 是模块的名称。
 
module-info.java还有一些其他的关键字:
1. requires:用于声明一个模块依赖的其他模块。
2. exports:用于声明一个模块导出的包,使其他模块可以访问该包下的公共类和接口。
3. opens:用于声明一个模块开放(暴露)指定的包,允许其他模块访问该包中的类。
4. provides:用于声明一个模块提供的服务接口的实现。
5. uses:用于声明一个模块使用的服务接口。
6. transitive:用于声明一个模块的依赖是传递性的,使依赖当前模块的其他模块也自动依赖当前模块所依赖的模块。
 

改造

根据定义和限制,我们定义好modulea和moduleb的package-info.java文件

 

 

 
至此,我们就把module和moduleb定义为一个java 模块了。
但此时,再尝试运行ModuleB的main方法,就无法正常运行了

 

可以看到,报错信息提示虽然 com.gonzo.study.jdk9.modulea 已经定义在模块 study.modulea中,但study.moduleb无法读到它。
当然,idea也很贴心的给出了解决建议,按照建议执行后,回到moduleb的package-info.java,看到加了这么一条。

 

requires study.modulea;
随后继续运行ModuleB的main方法成功:

 

 

思考

通过这个例子,想必大家已经初步理解了模块化的概念,同时也会新增很多疑问,例如:
  • 如果不定义package-info.java,能不能引入定义了package-info.java的模块?
  • 这么定义有什么作用?
 
针对第一个问题,通过我的验证,可以从下表中找出答案:
模块 modulea moduleb 是否可以引用到
是否定义了 package-info.java 文件 可以
不可以,报错:
Package 'com.gonzo.study.jdk9.modulea' is declared in the unnamed module, but module 'study.moduleb' does not read it
可以
可以,只要在moduleb中生命对modulea的requires
而第二个问题,经过我的资料查找,得到如下的说法:
1. 更好的封装和隔离:模块化系统通过将代码组织成模块,强制实施模块间的明确边界和依赖关系。这样可以更好地封装和隔离代码,减少模块之间的耦合,提高代码的可维护性和可重用性。
2. 显式的依赖管理:模块化系统要求在每个模块中明确声明其依赖关系。这样可以更清晰地了解代码的依赖关系,减少意外的依赖问题,并提供更好的版本管理和冲突解决机制。
3. 更小的运行时环境:模块化系统允许在构建应用程序时只包含所需的模块,从而减少了运行时环境的大小。这可以减少应用程序的启动时间和内存占用,并简化应用程序的部署和分发。
4. 安全性增强:模块化系统通过明确的模块边界和访问控制,提供了更好的安全性。只有明确导出的包才能被其他模块访问,其他模块无法直接访问未导出的包,从而减少了潜在的安全漏洞。
5. 更好的可维护性和可读性:模块化系统鼓励开发者将代码组织成独立的模块,每个模块都有明确的目的和职责。这样可以提高代码的可读性和可维护性,使开发者更容易理解和修改代码。
抛开哪些轱辘话,我觉得第三点是最大的进步。
Java早些时候一直被诟病吃内存,其中一个原因就是因为一个大型项目要依赖的第二、第三方包是不计其数的。但实际上引入一个包可能只需要里面的某一小部分类,在JDK9之前只能全部导入。
但有了模块化之后,可以指定需要的包的导入,这样依赖,针对不需要的包就排除在外了。编译运行时,这些被排除的包节省下来的时间和内存也是很客观的。
 

新版本字符串方案

JDK 9 引入了一种新的字符串实现方案,称为Compact Strings(紧凑字符串),与 JDK 8 的字符串实现方案有一些区别。 在 JDK 8 中,Java 字符串由 char 数组和一个 int 字段(offset)组成,用于存储字符串的字符数据和偏移量。这种实现方式在大多数情况下效果很好,但对于包含大量ASCII字符的字符串,会浪费一些内存空间,因为每个字符都需要占用两个字节。
JDK 9 中的 Compact Strings 方案旨在减少这种内存浪费。在 Compact Strings 中,Java 字符串使用 byte 数组来存储字符数据,而不是 char 数组。对于只包含ASCII字符的字符串,每个字符只需要占用一个字节,这样就可以节省一半的内存空间。
这种改进对于许多应用程序来说是透明的,因为它只是在内部对字符串的实现进行了优化。大多数情况下,应用程序的行为和性能不会受到影响。但对于那些处理大量ASCII字符的应用程序,Compact Strings 可能会带来显著的内存节省和性能提升。 需要注意的是,Compact Strings 方案只适用于默认的编码方案(如UTF-16),对于其他编码方案(如UTF-8),仍然使用传统的 char 数组实现。此外,Compact Strings 的具体实现细节可能因不同的JVM实现而有所不同。 总的来说,JDK 9 的 Compact Strings 方案通过优化字符串的内存占用,提供了更高效的字符串表示方式,特别是对于包含大量ASCII字符的字符串。
 
JDK8的String源码:

 
JDK9的String源码:

 

 

Java Shell

简介:
一个交互式命令行工具。
它允许开发人员在命令行中直接执行和测试Java代码片段,而无需编写完整的Java程序。
Java Shell提供了一个交互式的环境,类似于Python的交互式解释器或Ruby的IRB。它可以用于快速验证代码、尝试新的API功能、学习和教学等场景。
使用Java Shell,你可以逐行输入和执行Java代码,立即查看结果。它支持Java的语法和特性,包括变量定义、表达式计算、方法调用等。你还可以在Java Shell中定义和操作变量,以便在后续的代码片段中使用。
 
使用方式:
打开命令行,输入 jshell,就进入了java shell的运行环境

 

 

简单调试下

 

 

关于其他更多的交互,可以参考java shell的官方文档。
 

JVM 部分

  • 将 G1 设为默认垃圾回收器
  • 弃用并发标记扫描 (CMS) 垃圾回收器
  • 删除 JDK 8 中不推荐使用的 GC 组合
    • DefNew + CMS 
    • ParNew + SerialOld 
    • Incremental CMS
  • 并发标记扫描 (CMS) 的“前台”模式也已被删除。删除了以下命令行标志:

    • -Xincgc
    • -XX:+CMSIncrementalMode
    • -XX:+UseCMSCompactAtFullCollection
    • -XX:+CMSFullGCsBeforeCompaction
    • -XX:+UseCMSCollectionPassing

命令行标志不再起作用。ParNew 只能与 CMS 一起使用,而 CMS 需要 ParNew。因此,该标志已被弃用,可能会在将来的版本中删除。-XX:+UseParNewGC-XX:+UseParNewGC

以上就是JDK9新增的主要功能,当然还有很多其他功能,没有一一列出来,具体可以参考官方文档查看更多:

Java Platform, Standard Edition What’s New in Oracle JDK 9, Release 9