☰
Java 17实战指南:从密封类到HMCL游戏配置全解析
2026/10/10 13:38:43 网站建设 项目流程

1. Java 17 的定位:为什么五年后它依然是现代 Java 的起点

Java 17 在 2021 年 9 月正式发布,是继 Java 11 之后第二个长期支持版本(LTS)。对绝大多数团队来说,Java 8 用了快十年,Java 11 又因为生态适配太慢被跳过,Java 17 才是"从 8 直接跨到新世界"的真正跳板。Spring Framework 6 和 Spring Boot 3 把基线定在了 17,主流框架、中间件和编译工具在 17 上的兼容性已经完全成熟。用一句糙话总结:现在再谈 Java 升级,基本就是"8 到 17"或"11 到 17",没人会跟你讨论更激进的跨度。

这一版一共落地了 14 个 JEP(JDK Enhancement Proposal),覆盖语言特性、核心库、JVM 运行时和安全性四个层面。先把完整的 JEP 清单列出来,后文再逐个拆:

JEP内容类别
306恢复始终严格的浮点语义运行时
356增强伪随机数生成器核心库
382新的 macOS 渲染管线图形
398弃用 Applet API清理
403强封装 JDK 内部 API运行时
406switch 模式匹配(预览)语言
407移除 RMI Activation清理
409密封类语言
410移除实验性 AOT/JIT 编译器清理
411弃用 Security Manager清理
412Foreign Function & Memory API(孵化)新特性
414Vector API(第二次孵化)新特性
415上下文相关反序列化过滤安全

还有一个有意思的现象:2023 年以后,网上关于 Java 17 的搜索量一直没降,但大量搜"java17 下载安装教程""java17 安装包"的人并不是后端开发者,而是玩我的世界的玩家——HMCL 这类启动器明确要求 Minecraft 1.18+ 必须跑在 Java 17 上。所以这篇我不会只停在语法层面,下载安装、项目配置、迁移避坑,连带着 HMCL 的实操都会一并讲清楚。

1.1 从 8 到 17,中间几年攒下的家底全在这里

很多人以为 Java 17 的新特性就是"密封类加一点小修补",这是低估它了。17 之前的几个版本把语言层面的硬骨头啃完,17 就像一个集大成者:你可以直接用 record 替代一堆样板代码,用 text block 写多行字符串,用 switch 表达式替代 if-else 链,用 instanceof 模式变量省掉一次强制类型转换。如果项目还停在 Java 8,升到 17 等于一次性拿到 9 到 16 的全部语法红利。

同时,17 对 JVM 内部做了一轮"强封装 + 清理老 API"。短期来看,某些老库会报错;长期来看,这是必须趟过去的门槛。理解了这一层,再看网上那些"升 17 踩坑"的帖子,你就能一眼看出根因。

1.2 这篇适合谁

适合三类人:第一类,维护 Java 8/11 老项目、正在评估升级路线的后端开发;第二类,想学新语法但一直没动手的同学;第三类,只为把游戏跑起来、第一次接触 JDK 的普通用户。读完你至少能判断三件事:Java 17 值不值得升、升级时会踩哪些坑、到底该装哪个发行版。

2. 语言和核心库的三张新面孔:密封类、switch 模式匹配与随机数 API 重构

2.1 密封类(JEP 409):把继承的边界写进源码

以前写接口,任何类都能实现它,继承边界完全敞开。设计一个领域模型时,你明明只想允许 Circle、Rectangle、Triangle 三种形状,但外部代码完全可以自己实现一个 Shape 塞进来,编译器不会拦。密封类就是来管这件事的:

public sealed interface Shape permits Circle, Rectangle, Triangle { double area(); } final class Circle implements Shape { private final double r; Circle(double r) { this.r = r; } public double area() { return Math.PI * r * r; } } non-sealed class Rectangle implements Shape { private final double w, h; Rectangle(double w, double h) { this.w = w; this.h = h; } public double area() { return w * h; } }

这里有几个约束要注意:被允许的子类必须和密封类在同一个模块(命名模块)或同一个包(未命名模块)里;每个被 permits 的子类本身必须是 final、sealed 或 non-sealed。non-sealed的意思是"我打开继承,你们随便扩",它给密封体系留了一个出口。

密封类的价值主要体现在三处。第一,领域建模更干净,比如支付方式、订单状态这种"有限子类型"的场景,用 sealed 写出来,别人一看就知道有哪些可能。第二,它是 switch 穷尽性检查的基础——配合模式匹配,编译器能在编译期告诉你"还有分支没覆盖"。第三,反射层面也补了配套 API,Class.isSealed()和Class.getPermittedSubclasses()可以动态拿到所有允许的子类。对我们写业务的人来说,最直观的收益就是 API 设计意图清晰了,文档都不用写那么多。

2.2 switch 模式匹配(JEP 406):先学会,等转正

老写法处理"运行时判断类型再做事",就是连环 if + instanceof + 强转:

Object obj = getSomething(); if (obj instanceof Integer) { Integer i = (Integer) obj; System.out.println("整数:" + i); } else if (obj instanceof String) { String s = (String) obj; System.out.println("字符串长度:" + s.length()); }

Java 17 的预览版给了新写法,直接拿对象做 switch:

Object obj = getSomething(); String result = switch (obj) { case Integer i -> "整数:" + i; case String s -> "字符串(长度 " + s.length() + "):" + s; case null -> "是 null"; default -> "其他类型:" + obj.getClass().getName(); };

注意,case 分支里可以直接用变量 i、s,不用再强转,还能自然处理 null。想加条件判断就用 when 子句:

case String s when s.length() > 5 -> "长字符串";

不过要提醒一句:JDK 17 里这是预览特性,编译和运行都得加--enable-preview,千万别在正式环境直接用。它真正转正是 JDK 21(JEP 441),那时候写法基本没变。我的建议是现在就用 17 开个 scratch 项目练熟,等团队升 21 时无缝衔接。

2.3 随机数生成器大重写(JEP 356):Random 不再是唯一选择

Java 的java.util.Random二十多年没大动过,SecureRandom又偏重,做模拟、并行计算时经常觉得别扭。JEP 356 引入了统一的RandomGenerator接口,并提供了多个新算法,其中最有代表性的是 LXM 家族(L32X64、L64X128、L128X256 等),名字里的 L 是线性同余,X 是 xoroshiro/xoshiro,M 是 mix 最后混合一步。它们兼顾速度、周期和统计质量,很适合并行和模拟场景。

用法很简单:

RandomGenerator g = RandomGeneratorFactory.of("L64X128MixRandom").create(); int n = g.nextInt(100); // 0-99 的随机数

想看看 JDK 17 里到底有哪些生成器,可以跑这么一段:

RandomGeneratorFactory.all() .map(f -> f.group() + " : " + f.name()) .sorted() .forEach(System.out::println);

还有一个很实用的子接口SplittableGenerator,一个生成器可以"分裂"出多个子生成器,每个子生成器相互独立,非常适合并行流和 Fork/Join 任务:

SplittableGenerator sg = (SplittableGenerator) RandomGeneratorFactory.of("L64X128MixRandom").create(); SplittableGenerator worker = sg.split();

对普通业务代码来说,这个改动基本无感;但如果你写过蒙特卡洛模拟、洗牌、压测数据生成,新 API 能省不少事。关键是,Random类本身没被废弃,老代码一行不用改。

3. JVM 层面的大扫除:强封装、Security Manager 警告与浮点语义回归

3.1 强封装 JDK 内部 API(JEP 403):反射大法开始失灵

这是升级到 17 后最容易遇到"惊喜"的地方。从 JDK 9 开始,JDK 内部 API(sun.misc、com.sun.*这些)被分到了模块里,非模块代码访问它们默认会被拒绝。JDK 16 把默认行为改成了拒绝,JDK 17 更进一步,直接把--illegal-access这个放行开关移除了。

后果就是:你代码里setAccessible(true)去反射 JDK 内部类,会直接抛InaccessibleObjectException,典型报错长这样:

java.lang.reflect.InaccessibleObjectException: Unable to make field ... accessible: module java.base does not "opens java.lang" to unnamed module

很多老库都栽在这上面:老版 Lombok、CGlib 代理、ASM、某些早期 JSON 反射库。处理思路分两步走。

第一步,优先升级依赖。Lombok 用 1.18.22 以上,CGlib 用 3.3.4 以上,ASM 用 9.2 以上,基本都能解决问题。

第二步,实在有第三方库改不了,再用--add-opens给指定模块开门:

java --add-opens java.base/java.lang=ALL-UNNAMED -jar your-app.jar

这句话的意思是:把java.base模块里的java.lang包开放给所有未命名模块。注意,这只是临时方案,能跑不代表干净。

还有一个提前检查的好工具:jdeps。用它扫一下你的 jar 就知道哪些代码依赖了 JDK 内部 API:

jdeps --jdk-internals your-app.jar

输出会列出具体使用了哪个内部 API、以及官方建议的替代方案。升级老项目之前,强烈建议先跑一遍这个命令,比上线后半夜处理告警舒服多了。

3.2 Security Manager 进入退役倒计时(JEP 411)

Security Manager 是 Java 诞生时就有的沙箱机制,靠配置权限来限制代码能做什么,早期 Applet 时代还活跃过。后来浏览器插件死了,服务端应用很少再用它,但维护这个机制的成本一直很高,Oracle 终于在 17 里把它标记为@Deprecated(forRemoval = true)。

注意,标记弃用不等于移除,Java 17 里System.setSecurityManager(...)还能用,只是编译会报警告。我的建议是:现在就去项目里搜一下SecurityManager和policy相关配置,如果只是历史遗留,就直接删;如果真有安全需求,迁移到操作系统容器隔离、Java 模块系统或者专门的权限校验框架,别把希望寄托在一个快要移除的 API 上。

3.3 浮点运算终于统一严格(JEP 306)

这个 JEP 背后的故事有点年代感。早年 x86 的 x87 浮点协处理器用 80 位扩展精度做中间计算,导致同一段浮点代码在不同 CPU 平台上的结果可能差最后几位,于是 Java 提供了strictfp关键字强制所有中间结果截断到 64 位。现代 CPU 的 SSE2 指令本身就能保证计算结果一致,strictfp已经名存实亡。JEP 306 让所有浮点运算默认就按 IEEE 754 严格语义执行,strictfp从此成了历史。

对绝大多数业务系统来说,这个改动没有任何感知;但对数值计算、科学计算类库是重大利好:跨平台结果完全一致,代码里再也不用到处贴strictfp了。如果你看到老代码里带strictfp,可以直接删掉,行为不会变。

3.4 被移除的老技术:AOT/JIT 与 RMI Activation

这一版还顺手清理了两块历史包袱。JEP 410 移除了 JDK 9 引入的实验性 AOT 编译器(jaotc)和基于 Graal 的实验性 JIT 编译器,原因很简单:没多少人用,维护成本却高。真需要 AOT 启动速度的话,现在业界走的是 GraalVM Native Image 路线,跟 OpenJDK 自带的实验功能不是一回事。

JEP 407 移除了 RMI Activation(java.rmi.activation包)。注意,RMI 本身还在,只是"按需激活远程对象"这套机制没了。如果老系统用了 Activation 相关 API,需要改成普通 RMI 服务或者换消息队列方案。这条基本只影响一些很老的分布式系统,普通项目不用管。

4. 给未来的三张门票:FFM、Vector API 和反序列化过滤器

4.1 Foreign Function & Memory API(JEP 412):JNI 的掘墓人

以前 Java 想调 C/C++ 库,只能走 JNI,写一堆 C 头文件、native 声明、编译动态库的胶水代码,不光繁琐,还容易把 JVM 搞崩。JEP 412 在 17 里以孵化模块jdk.incubator.foreign的身份带来了 Foreign Function & Memory API(简称 FFM),核心目标就两个:安全地操作堆外内存、直接调用本地函数。

看一段最基础的内存操作:

import jdk.incubator.foreign.MemorySegment; try (var segment = MemorySegment.allocateNative(64)) { // 这里拿到 64 字节的堆外内存,读写完自动释放 }

MemorySegment用 try-with-resources 管理生命周期,不用再手动 malloc/free。调用 C 函数则是通过CLinker把函数签名描述成FunctionDescriptor,再用 MethodHandle 发起调用。这套 API 在 17 里还是第一个孵化版本,包名和类名在后续版本中反复调整,到 JDK 22 才以java.lang.foreign正式转正。所以现在想在生产环境大量使用还早,但把它理解成"下一代 JNI"是没问题的,它消除了跨语言调用最大的几个痛点。

4.2 Vector API(JEP 414):把 SIMD 写进 Java

现代 CPU 都有 SIMD 指令集,可以一次处理多个数据。以前 Java 靠 JIT 自动向量化,有时候能优化,有时候优化不到。Vector API 让你在 Java 层面显式表达"我要用 256 位向量并行计算",以浮点数组加法为例:

import jdk.incubator.vector.FloatVector; FloatVector va = FloatVector.fromArray(FloatVector.SPECIES_256, a, 0); FloatVector vb = FloatVector.fromArray(FloatVector.SPECIES_256, b, 0); va.add(vb).intoArray(c, 0);

SPECIES_256表示 256 位宽的向量,一次能装 8 个 float,上面三行代码就完成了 8 对浮点数的并行加法。图像处理、音视频编解码、加密算法、科学计算这类对性能敏感的场景,用它能实打实地吃满 CPU 算力。不过在 JDK 17 里它还是第二次孵化状态,API 不稳定,同样需要--add-modules jdk.incubator.vector才能跑。我的态度是:知道有这个东西,等它稳定后再进生产。

4.3 反序列化过滤器(JEP 415):给 ObjectInputStream 加安全带

Java 反序列化漏洞几乎是安全圈的老朋友了,攻击者构造恶意字节流,让目标系统反序列化出危险对象形成 gadget 链。JEP 415 提供了一套标准过滤器机制,能限制反序列化时允许的类、对象深度和字节数。

全局配置可以直接加 JVM 参数:

-Djdk.serialFilter="java.base/*;!com.example.untrusted.*;maxdepth=20;maxbytes=1000000"

这段的意思是:允许 java.base 模块下的类,排除 com.example.untrusted 包,对象嵌套深度最多 20 层,整个流最多 1 百万字节。也可以对单个ObjectInputStream设置过滤器:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter( "java.base/*;!com.example.untrusted.*;maxdepth=20;maxbytes=1000000"); ois.setObjectInputFilter(filter);

JEP 415 的关键点在于引入了 JVM 级别的过滤器工厂,可以把全局配置和每个流上的过滤规则组合起来,做到统一治理又保留灵活性。凡是项目里还直接用ObjectInputStream接收不可信数据的,这个特性属于必上的安全补丁。

5. 想把 Java 17 用起来:发行版选择、安装、项目配置与迁移避坑

5.1 先选一个发行版

Java 早就不是"只从甲骨文下载"的时代了,现在主流的 OpenJDK 发行版各有各的来头。如果你所在的公司对开源许可证敏感,或者想省心,我的建议直接看这张表:

发行版维护方特点
Oracle JDK 17Oracle功能最全,个人和开发免费,商业使用要注意 OTN 条款
Temurin 17Eclipse Adoptium社区热度最高,过 TCK 认证,商业友好
Microsoft OpenJDK 17Microsoft装机量增长快,适用于云和游戏场景
Amazon Corretto 17Amazon长期维护,云上友好,性能调优积极
Liberica JDK 17BellSoftFull 版自带 JavaFX,桌面应用方便

我可以给你的结论是:后端项目用 Temurin 或 Corretto,几乎没有坑;我的世界玩家用 Temurin 或 Microsoft OpenJDK 都行,HMCL 也能自动识别。没必要一定装 Oracle 原版,除非你有具体理由。

5.2 下载安装的具体操作

Windows 最简单,去 Adoptium 官网下载.msi安装包,安装时勾上"设置 JAVA_HOME"和"加入 PATH",一路下一步就完事。完成后打开命令行验证:

java -version javac -version

macOS 用户推荐 Homebrew:

brew install --cask temurin17

装完再确认一下:

/usr/libexec/java_home -V

会列出系统里所有 JDK 版本,其中temurin-17.jdk就是刚装的。

Linux 用户最省心的方案是用 Adoptium 的 apt 源:

sudo apt install -y wget apt-transport-https gnupg wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add - echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/VERSION_CODENAME/{print $2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list sudo apt update sudo apt install temurin-17-jdk

如果你习惯用 SDKMAN 管理多个 JDK,一条命令就搞定:

sdk install java 17.0.10-tem sdk use java 17.0.10-tem

SDKMAN 的好处是版本切换极快,适合同一台机器上既要用 Java 8 又要用 17 的开发者。

5.3 在 Maven 和 Gradle 里配置 Java 17

Maven 项目,最推荐在pom.xml的属性里直接指定 release:

<properties> <maven.compiler.release>17</maven.compiler.release> </properties>

有人习惯用<maven.compiler.source>和<maven.compiler.target>,这两个只控制语法级别,不控制编译时链接的 API,容易出现"用 Java 17 语法编译、但引到了旧 API"的怪问题。release会同时约束语法版本和 API 签名,是更稳的做法。

Gradle 项目推荐用 toolchain,让 Gradle 自动找 JDK 17:

java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }

IDE 这边,IDEA 2021.2 起完整支持 Java 17,Eclipse 2021-09 起也行。老版本的 IDE 装了对版本插件也未必顺畅,建议顺手升级。

5.4 迁移时的高频坑

从 8/11 升到 17,九成问题集中在下面这几类,我把症状和应对列成一张表:

症状根因处理
InaccessibleObjectExceptionJDK 内部模块被强封装升级依赖版本,或临时--add-opens
Lombok 编译报错Lombok 版本太老升级到 1.18.22+,最好最新版
CGLIB/ByteBuddy 代理失败老字节码工具访问内部 API升级 CGlib 3.3.4+、ByteBuddy 最新版
JAXB/JAX-WSClassNotFoundExceptionJava EE 模块早已被移除手动引入 jakarta.xml.bind 等依赖
启动报Unsupported class file major version 61字节码库不认识 Java 17 的 class升级 ASM 到 9.2+

还有一个细节:JDK 17 的javac依然支持--release 8编译老代码,给迁移过程留了缓冲。但千万别长期停留在"用 17 编译、源码级别 8"的状态,那样既享受不到新语法,又要继续背着老包袱。

我在升级老项目时的固定动作是:先jdeps --jdk-internals扫一遍依赖,把高危项全部升级到支持 17 的版本,再跑全套测试,最后再看--add-opens还剩下几个。绝大多数项目做到第二步就已经干净了。

6. 给 HMCL 玩家的特别说明:游戏要的 Java 17,到底怎么装

6.1 为什么 Minecraft 1.18+ 非要 Java 17

Mojang 在 1.18 版本把 Minecraft Java 版的运行基线从 Java 8 提到了 Java 17,因为这代游戏用到了不少新语法和性能改进,也顺势选了个长期支持版本。简单说:1.17 需要 Java 16 起步,1.18 到 1.20.4 需要 Java 17 起步,再新一点的版本官方启动器已经切到 Java 21 了。很多启动器,包括 HMCL,检测到本机没有 Java 17 时,会直接弹提示"缺少 Java 运行时"。

所以你现在看到的"hmcl java17"相关搜索,本质是启动器要求装了 17 才能玩新版整合包。我额外提醒一句:如果你还想玩老版本(比如 1.12.2 的整合包),那 Java 8 也得留着,HMCL 支持为不同版本指定不同的 Java,两者可以共存。

6.2 在 HMCL 里指定 Java 路径的两种方式

第一种,手动指定。打开 HMCL,进入"设置 → 全局游戏设置",找到"Java 路径"或者"Java 可执行文件"选项,点击浏览,选中 java 可执行文件。Windows 上路径大概长这样:

C:\Program Files\Microsoft\jdk-17.0.8.7-hotspot\bin\java.exe

macOS 上则要把路径指到 JDK 内部的 bin:

/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home/bin/java

确认路径后保存,再启动任意 1.18+ 版本,HMCL 就会用这个 Java 跑游戏,不再报缺运行时的错。

第二种,让 HMCL 自动下载。新版 HMCL 在下载页或者设置页提供了"安装 Java"的入口,点它会自动帮你下一个独立 JDK,不需要手动改任何环境变量。对不想折腾电脑的玩家,这种是最省心的。注意,HMCL 启动器本体对 Java 版本要求不高,老一点的本体用 Java 8 也能跑,但游戏本体是另一回事,两者别混淆。

6.3 装完 17 会不会把原来的 Java 8 搞坏

不会坏,但有一个常见的坑:Windows 安装 Java 时,安装包可能会顺手把JAVA_HOME和PATH环境变量指向新版本。如果你原来的命令行脚本依赖 Java 8,可能出现"本来好好的,现在mvn或者某个脚本突然失败"的情况。这时候去改环境变量,把JAVA_HOME指回原来的 Java 8 路径就行。游戏本身不受影响,因为 HMCL 用的是绝对路径,不读环境变量。

排查当前命令行用了哪个 Java,可以用:

where java

Windows 下会列出所有候选路径,从上到下就是生效顺序。别因为装了 17 就去卸载 Java 8,老版本整合包和某些老项目都还指着它。

我自己的习惯是:电脑上同时装着 Java 8、17、21 三套,日常开发用 SDKMAN 切换,游戏和独立工具则直接指定路径。这套玩法用了两年,稳得很。Java 17 这个版本,从语法到运行时都处在一个"新但不过激"的平衡点,如果你正在观望升级,我的建议是别再等了——先用 17 开个空项目把密封类和记录类型玩熟,再动手平迁老项目,你会发现这条路比想象中平坦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询