Brandon's Blog

Brandon's Blog

现代 Java 新特新

736
2023-12-03
现代 Java 新特新

前言

Java 8 自 2014 年 3 月18 日发布至今(2024), 这么多年过去了依然是国内使用最广泛的 JDK 版本, 正所谓 "他发任他发, 我用 Java 8", 突出一个字啊! 先来康一康 Java SE RoadMap:

Java 8 之后的10年里 Oracle 先后发布了13个版本, 其中3个 LTS 版本, 从里面的 Release 的功能可以看出来 Java 一直紧跟时代, 变化非常大, 总的来说就是迈向更轻(体积), 更快(性能), 更小(内存占用). 作为一个有灵魂码农, 也不能落后, 可以不用, 但不能不了解.

关于 Java 8 的介绍可以看我的老文章: Java8 Noob Tutorial

Java 9 - 11

Java 9

主要语言变化

新增

  • 模块化系统(Module System): JSR 376

    • Project Jigsaw 的一部分

    • 按需加载, 解决臃肿

    • module-info.java

      • 通过 exports, requires 关键字声明作用域(感觉像 nodejs?)

  • 新版本定义机制: $MAJOR.$MINOR.$SECURITY.$PATCH

更新

  • try-with-resources 语法允许变量使用 final 修饰, 语法升级

  • diamond 语法允许匿名类(如果类型推断的参数类型可表示的话)

  • 接口允许定义 private 方法

  • @SafeVarargs 允许声明在实例 private 方法上

主要 API 变化

引入

  • 进程(Process): JEP 102, 全新 API ProcessHandle 提供更好的管控操作系统

  • 内存(Memory): JEP 193, VarHandle 作为正式 API 替代 Unsafe, 对变量执行原子和内存屏障操作

  • 日志(Logging): JEP 264, 全新日志 API 和服务

    • 现在基本都用 Slf4j 了吧...

  • XML: JEP 268, 添加标准的 XML Catalog API

  • 栈(Stack): JEP 259, 全新栈跟踪工具, StackWalker 替代老的 StackTraceElement 体系

更新

  • 字符串(String): JEP 254, String 底层存储char[] 替换为 byte[]

    • 内存优化, 时间换空间

  • 集合(Collections): JEP 269, 集合接口提供便利的工厂方法, 如, Set.of(...)

  • 并发(Concurrency): JEP 266, CompletableFuture 以及其他并发组件提升

    • Reactive Streams: java.util.concurrent.Flow

  • 编译器(Compiler): JEP 274, 提升 MethodHandle 通用性以及更好地编译优化

  • 注解(Annotation): JEP 277, @Deprecated 注解增加 sinceforRemoval 属性, 丰富 API 淘汰策略

  • 线程(Threading): JEP 285, 新增自选方法 Thread.onSpinWait

  • 对象序列化(Serialization): JEP 290, 新增 API ObjectInputFilter 过滤 ObjectInputStream

  • XML: JEP 255, 更新 Xerces 2.11.0 解析 XML

  • Java Management Extensions (JMX): 支持远程诊断命令

  • 脚本(Scripting):

    • JEP 236, Nashorn 解析器 API 引入

    • JEP 292, 实现 ECMAScript 6 功能

  • 国际化(Internationalization):

    • JEP 267, 支持 Unicode 8.0

    • JEP 252, JDK 8 引入的 XML 形式的 Common Locale Data Repository (CLDR) 作为默认选项

    • JEP 226, 支持 UTF-8 Properties 文件

  • Java Database Connectivity (JDBC):

    • JDBC-ODBC 桥接移除

    • JDBC 4.2 升级

主要 JVM 变化

新增

更新

  • 垃圾回收(Garbage Collection)

  • 统一 JVM 日志: JEP 158

  • 输入/输出(I/O):

    • 减少 <JDK_HOME>/jre/lib/charsets.jar 文件大小

  • 性能提升(Performance)

    • java.lang.String 字节数组性能优化

  • 工具(Tools)

    • Java Plug-in 标记为不推荐使用, 未来版本移除

    • jshell: JEP 222, 增加 Read-Eval-Print Loop

    • jcmd: JEP 228, 增加更多诊断命令

    • jlink: JEP 282, 组装和优化模块以及依赖

    • 多版本发布 JAR 文件: JEP 238

    • 移除指定版本 JRE 启动

    • 移除 HProf Agent: JEP 240

Java 10

主要语言变化

新增

主要 API 变化

更新

  • 通用: Optional 新增方法

    • orElseThrow()方法来在没有值时抛出指定的异常

  • 集合增强

    • List, Set, Map 提供了静态方法copyOf()返回入参集合的一个不可变拷贝

  • java.util.stream.Collectors 中新增了静态方法, 用于将流中的元素收集为不可变的集合

  • Collectors.toUnmodifiableList(), Collectors.toUnmodifiableSet()

  • 安全(Security):

主要 JVM 变化

新增

更新

Java 11(LTS)

主要语言变化

新增

主要 API 变化

引入

更新

主要 JVM 变化

新增

更新

Java 12 - 17

Java 12

主要语言变化

新增

主要 API 变化

  • String 新增了 indent 方法处理缩进

  • Files 新增了 mismatch 来对比两个文件

  • NumberFormat 新增了对复杂的数字进行格式化的支持: getCompactNumberInstance

主要 JVM 变化

新增

更新

Java 13

主要语言变化

新增

主要 API 变化

更新

主要 JVM 变化

更新

Java 14

主要语言变化

新增

主要 API 变化

引入

主要 JVM 变化

更新

Java 15

主要语言变化

引入

更新

主要 API 变化

引入

更新

主要 JVM 变化

更新

Java 16

主要语言变化

引入

更新

引入

主要 JVM 变化

引入

更新

Java 17(LTS)

主要语言变化

引入

更新

主要 API 变化

引入

更新

主要 JVM 变化

引入

更新

Java 18 - 21

Java 18

主要语言变化

更新

主要 API 变化

更新

主要 JVM 变化

更新

Java 19

主要语言变化

更新

主要 API 变化

更新

主要 JVM 变化

引入

Java 20

主要语言变化

引入

主要 API 变化

更新

Java 21(LTS)

主要语言变化

引入

更新

主要 API 变化

引入

更新

主要 JVM 变化

更新

升级Java版本的潜在问题与挑战

个人觉得现代 Java 的使用越来越偏底层, 越来越难, 很多黑科技的出现, 比如指令优化等, 大部分是高阶技能, 所以了解这些高级特性是提升个人竞争力的有效途径.

随着Java从8升级到更新的版本, 我们见证了性能、内存管理以及底层支持等多方面的显著提升. 例如, 字符串压缩、ZGC(Z Garbage Collector)和GraalVM等新特性的引入, 极大地增强了Java平台的能力. 然而, 这种进化并非没有代价, 在决定是否进行版本升级时, 开发者必须仔细考虑以下几项关键因素:

  • 废弃的API和包: 一些旧版API如 sun.misc.BASE64Encoder 已经被删除, 而像 javax 这样的包也经历了迁移或弃用. 这意味着依赖这些组件的应用程序可能需要重构以适应新的标准.

  • 内部API限制: 对某些内部API(如 Unsafe)的访问权限变得更加严格, 这可能影响那些直接使用这些API实现特定功能的应用程序.

  • 垃圾回收机制的变化: 垃圾回收策略的更新可能会改变应用程序的运行行为, 特别是对于那些高度依赖于特定GC行为的应用而言.

  • 第三方库的兼容性: 随着Java版本的迭代, 第三方库也会相应地更新. 虽然它们会添加新特性, 但同时也会停止支持旧的功能或配置. 例如, Spring Boot 3.0之后不再使用 spring.factories 文件来加载自动配置类, 如果使用的Spring Boot Starter还未适配新版, 则可能导致应用无法正常启动.

  • 注解和工具类的变动: 如 @PostConstruct 等注解的处理方式发生变化, 或者新的StackTrace API的引入, 都可能要求第三方框架做出相应的调整, 否则将导致不兼容的问题.

  • 项目复杂度的影响: 对于依赖较少的小型项目来说, 升级到更高版本的Java相对简单. 但对于大型项目, 尤其是那些包含多个公共模块的系统, 升级过程可能会非常复杂. 不仅因为内部API的变化, 还因为外部依赖关系的调整需求.

  • 生态系统的演进: 以Spring为例, 其庞大的生态系统使得全面兼容所有JDK版本变得几乎不可能. 因此, 最新的Spring版本已经明确表示不再支持Java 8, 转而拥抱更先进的Java版本, 以减轻历史包袱并推动技术进步.

为了顺利过渡到更新的Java版本, 开发团队应当提前规划, 并确保充分理解上述各项挑战. 此外, 建议对现有代码库进行全面评估, 识别出可能受到版本变更影响的部分, 并制定详细的迁移策略. 对于新的项目开发, 可以充分利用最新Java版本带来的性能优化和其他改进, 但同时也应该意识到随之而来的额外工作量和技术要求.

版本升级兼容常用技巧

添加被移除的包

比较经典就是 javax.xxx 这个包, 有些三方库底层还是用了 javax 的类, 在 JDK 8 可以运行, 升级到 9 以上时就不能用了, 因为这个 javax 已经被移除, 这时候手动引入 javax 依赖, 比如:

<dependency>
    <groupId>com.sun.mail</groupId>
    <artifactId>javax.mail</artifactId>
    <version>1.6.2</version>
</dependency>

代码层面逻辑判断兼容

如果使用了将来被移除或者重命名的类, 比如 BASE64Encoder 在 JDK 9 被移除, 为了兼容功能完整性, 需要判断当前运行的 JDK 版本从而在高版本运行环境中切换到其他实现.

String version = System.getProperty("java.version");
if (version.startsWith("1.8")) {
    // 使用 BASE64Encoder
} else {
    // 高版本实现
}

打包时通过 Profile 区别不同版本

如果一个公共类库比如 common-util 中引用了 util-a:1.0, common-util 需要在 JDK 8 及以上版本运行, 而 util-a:1.0 只能在 JDK 8 下运行, 只有 util-a:2.0 才支持更高的版本, 但是 util-a:2.0 需要 JDK 11+ 的编译环形, 所以 common-util 不能在 JDK 8 的环境中升级 util-a 到 2.0 版本.

这个 Case 有两个解决方案:

  • 方案1: 在高版本的项目中引入 common-util, 然后手动排除 util-a:1.0 并引入 2.0 版本.

    • 优点: 简单

    • 缺点: 一般一个 common-util 不可能只使用了一个三方库, 也不可能只被一两个项目引用, 所以采用此方案需要每个引用到 common-util 的项目都手动操作一边, 而且对于未来新增的类库管理不方便.

  • 方案2: 通过 Maven Profile 打包两个版本, 一个给 JDK 8用, 另外一个给更高版本使用. 下面是一个例子, 通过 mvn -Pjdk8 clean install 命令打包出一个 classifierjdk8 的版本.

<profiles>
    <profile>
        <id>default</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <JAVA_VERSION>21</JAVA_VERSION>
            <JAVA_HOME>~/.jdks/azul-21.0.1</JAVA_HOME>
        </properties>
        <dependencies>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>1.5.12</version>
            </dependency>
        </dependencies>
    </profile>
​
    <profile>
        <id>jdk8</id>
        <properties>
            <JAVA_VERSION>1.8</JAVA_VERSION>
            <JAVA_HOME>~/.jdks/azul-1.8.0_432</JAVA_HOME>
        </properties>
        <build>
            <plugins>
                <plugin>
                    <artifactId>maven-jar-plugin</artifactId>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>jar</goal>
                            </goals>
                            <configuration>
                                <classifier>jdk8</classifier>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
        <dependencies>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>1.3.14</version>
            </dependency>
        </dependencies>
    </profile>
</profiles>

其他

Java 模块化问题

遇到类似如下问题:

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module

需要加上启动参数打开包权限(其他包的报错类似):

--add-opens java.base/java.lang=ALL-UNNAMED

maven-compiler-plugin 以及 maven-surefire-plugin 插件也可能会遇到此问题, 解决方法也是加参数:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
      <showWarnings>true</showWarnings>
      <fork>true</fork>
      <compilerArgs>
        <!-- 根据报错添加对应的包 -->
        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
      </compilerArgs>
  </configuration>
</plugin>
​
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
      <includes>
        <include>**/*Test.java</include>
      </includes>
      <argLine>
        <!-- 根据情况添加对应的包 -->
        --add-opens java.base/java.lang=ALL-UNNAMED
      </argLine>
  </configuration>
</plugin>

Spring Boot

Spring Boot 3.0 Migration Guide

Ref