
1. 上手第一印象Java 18不是LTS但值得你用一用先说个反直觉的结论Java 18虽然只是个非长期支持版本但里面有几个特性是我在日常开发中真正会主动用起来的其中一个甚至让我把用了好几年的Python HTTP服务器脚本直接扔进了回收站。Java 18发布于2022年3月属于Java 17之后的过渡版本。按照Oracle的发布节奏Java 17是LTS长期支持版Java 18和Java 19都是中间版本到Java 21才会有下一个LTS。很多团队的生产环境还锁在Java 11甚至Java 8上对这种非LTS版本天然无感。但我的观点是非LTS版本不一定等于不值得看恰恰因为是中间的过渡版本很多在LTS里看起来突然出现的特性其实早就在这些版本里逐步孵化成型了。Java 18里就有好几个JEP是从孵化到成熟的关键一步提前了解它们等Java 21普及的时候你不会有任何陌生感。这一版的主要内容可以用一张表快速过一遍JEP编号特性类型对普通开发者的影响程度JEP 400UTF-8作为默认字符集语言/运行时改动高很多历史遗留问题被翻转JEP 408Simple Web Server新工具高轻量HTTP服务随手可用JEP 413Java API文档中的代码片段注解文档工具中写技术文档和SDK的人会喜欢JEP 416使用方法句柄重新实现核心反射内部实现低但对框架作者是大事JEP 417Vector API第三轮孵化孵化API低偏向高性能计算场景JEP 418Internet-Address解析SPI网络API中云环境DNS异常场景有用JEP 419Foreign Function Memory API第二轮孵化孵化API低但方向很重要JEP 420Switch模式匹配二次预览预览特性中语法演进的前奏JEP 421弃用finalization机制运行时清理中涉及JVM参数调整下面我从最实用的几个特性展开讲每个都会给出能直接跑起来的例子和我在实际使用中踩到的坑。2. Simple Web Server不用再为临时HTTP服务装一堆依赖了这是Java 18里我最喜欢的功能没有之一。它解决的痛点非常朴素有时候我就是想在某个目录下快速起一个HTTP服务把文件共享给同事浏览器下载或者本地调试一个前端页面不想为了这个需求引入Spring Boot、Tomcat或者Node.js的http-server。以前我一般写个Python的python -m http.server 8080但有些机器上Python环境不是默认配置好的尤其是只装了JDK的服务器上。Java 18直接用命令行就能启动jwebserver默认绑定的地址是127.0.0.1:8000当前目录就是服务的根目录。如果想把服务暴露给局域网其他机器访问可以这样jwebserver -b 0.0.0.0 -p 8080 -d /home/share对应的参数含义-b绑定地址默认是回环地址外部访问不到这其实是安全考虑。生产环境的临时服务如果要用记得显式指定网卡IP或0.0.0.0。-p端口默认8000。-d指定暴露的目录默认是当前目录。-o输出格式有none、info、verbose三档verbose可以看到每个请求的详情调试时候很有用。如果你是在项目里以编程方式启动jdk.httpserver包下的SimpleFileServer类提供了完整的APIimport com.sun.net.httpserver.SimpleFileServer; import com.sun.net.httpserver.HttpServer; import java.net.InetSocketAddress; import java.nio.file.Path; var server SimpleFileServer.createFileServer( new InetSocketAddress(8080), Path.of(/home/share), SimpleFileServer.OutputLevel.VERBOSE ); server.start();这个API的背后其实就是com.sun.net.httpserver包的老牌HttpServer只不过以前你要自己写handler来处理静态文件现在官方把这部分逻辑封装好了开箱即用。2.1 jwebserver的定位局限知道它不能干什么更重要这个工具官方定位就很明确用于原型验证、临时分享、教学演示不是用来替代生产级Web服务器。没有TLS、没有认证、没有Servlet支持、没有连接池调优参数很多并发连接的情况下性能也就那样。我在生产环境不会用但在以下场景里它非常顺滑临时把构建产物比如Jekyll生成的静态站点、Webpack打包后的dist目录起一个本地服务进行预览。在内网机器上快速给同事分享一个大文件——直接用浏览器下载就行。给前端联调的mock服务提供简单的静态JSON文件。值得一说的是jwebserver还配了几个JFRJDK Flight Recorder事件比如jdk.WebServerRequest可以用JFR采集HTTP请求信息。虽然这个工具的定位很简单但连可观测性都给了说明不是敷衍地做个demo了事。3. 默认UTF-8一个影响所有Java程序的隐形改变很多人看到默认字符集改成UTF-8会觉得这算什么特性不本来就是UTF-8吗如果你这么想说明你多半是在macOS或Linux上做开发或者项目从一开始就踩在规范的道路上。但在真实的存量系统里有不少应用跑在Windows上而Windows平台的默认字符集是GBK也就是file.encoding这个系统属性在Windows上默认返回GBK。Java程序在跨平台处理文件读写、网络传输时如果没有显式指定字符集就会跟着操作系统的默认值走同一套代码在不同平台上行为不一致。Java 18之前Charset.defaultCharset()的返回值依赖运行环境的本地化设置。Java 18以后无论运行在什么平台默认字符集一律是UTF-8。这就意味着所有依赖默认字符集的IO操作——Files.readString、FileReader、PrintStream输出到控制台——行为都统一了。3.1 历史代码的查缺补漏哪些地方会被默默影响这里要特别提醒这项改动对正确设置了字符集的项目没有影响但如果你写过依赖默认编码的老代码Java 18升级后会悄悄改变行为。最典型的例子是// 这段代码在Java 17的Windows上文件会是GBK编码 // 在Java 18的Windows上文件变成UTF-8编码 Files.writeString(Path.of(a.txt), 中文内容);如果下游系统还在用GBK读取这个文件就会出现乱码。反过来的情况也存在以前在Windows上写出来的文件是GBK下游用UTF-8读本来就是乱码升级到Java 18之后反而歪打正着变正确了。我建议在升级JDK之前做一次全局检索重点排查以下几类用法没有指定字符集的FileReader/FileWriter构造方法。String.getBytes()无参方法——它用的是平台默认字符集。new String(bytes)无参构造方法。HttpURLConnection或HttpClient处理响应体时没设置Charset。读取Properties文件时用了默认字符集的地方。不过不用过于担心大多数现代化的框架和中间件早就显式指定了UTF-8比如Spring的CharacterEncodingFilter、Tomcat的URIEncoding配置。这项改动真正的好处是让Java程序的行为在跨平台时变得可预期特别是容器化部署场景同一个镜像跑在不同基础环境的宿主机上不会再因为字符集差异出现诡异bug。3.2 file.encoding和System.out.println的细微差别有一个细节很多人没注意到System.out和System.err在某些情况下仍然可能使用stdout.encoding这个独立属性而不是file.encoding。Java 18里file.encoding固定为UTF-8但控制台输出在Windows上默认可能还是使用本地编码。如果你在Windows的命令行里直接运行Java 18程序打印中文控制台显示不乱码的保障实际上来自系统代码页与程序编码的匹配。真的需要精确控制控制台输出编码可以显式指定-Dstdout.encodingUTF-8。这算是个偏门知识但排查乱码问题的时候能帮你少走很多弯路。我遇到过同事在Windows上用Java 18跑批处理日志文件里的中文正常但控制台打印乱码就是因为System.out的编码没跟上。4. snippet注解JavaDoc从此不用再写残缺的示例代码写过开源项目或内部公共组件的人应该对JavaDoc的代码示例深有体会。最常见的痛点就是示例代码以纯文本形式写在注释里编译期完全没有校验等用户照着抄发现API变了、方法签名改了文档里的代码早就跑不起来了。维护文档比写文档更痛苦这就是snippet注解要解决的问题。简单来说snippet允许你在JavaDoc里引用外部文件中的代码片段或者直接内联一个代码区域并且这段代码在编译期会被真实校验。/** * 使用示例 * {snippet : * // 创建一个简单的HTTP服务器 * var server SimpleFileServer.createFileServer( * new InetSocketAddress(8080), * Path.of(/tmp), * SimpleFileServer.OutputLevel.INFO * ); * server.start(); * } */ public class SnippetDemo { }JavaDoc生成时会把这个注释块里的代码渲染成带语法高亮的代码片段编译器会去解析这段代码存在语法错误就会在编译期暴露出来。更进一步snippet可以从外部文件引用代码/** * 参考外部测试文件中的示例 * {snippet fileSnippetDemoTest.java regionexample} */ public class SnippetDemo { }然后在对应的测试文件里用// start regionexample和// end标记出片段范围public class SnippetDemoTest { Test void demo() { // start regionexample var sum IntStream.rangeClosed(1, 100).sum(); System.out.println(Sum sum); // end assertEquals(5050, sum); } }这样做的好处显而易见文档里的示例代码和真实测试代码可以同源维护测试通过意味着示例代码大概率也能跑通。我在自己维护的公共库中已经开始用这种方式组织README之外的API示例质量提升是立竿见影的。另外snippet还支持highlight、substitute这些高级属性可以对片段里的特定行做高亮或替换这个对讲清楚核心步骤很有帮助。5. 面向未来的技术储备反射重写、Vector API、外部函数接口Java 18里还有几个JEP普通业务开发一时半会碰不到但属于技术方向信号的改动值得按图索骥地了解。5.1 JEP 416核心反射用Method Handles重写这是一个典型的看不见的优化。JDK自己内部的反射实现从Java 18开始不再直接调用java.lang.reflect.Method.invoke背后的native实现而是基于java.lang.invoke.MethodHandle来重新构建。结果是大部分反射调用的性能得到了提升而且JDK内部的反射与MethodHandle的语义统一了比如对模块边界的访问检查。对普通业务代码来说你不需要改任何东西反射API的用法完全不变。但对那些重度依赖反射的框架——Spring、MyBatis、Jackson——这是个好消息因为它们底层的反射调用会白捡性能提升。如果你对自己的反射调用性能敏感可以配合-Dsun.reflect.inflationThreshold等调优参数观察变化但在绝大多数场景下默认行为就够了。5.2 JEP 417Vector API第三轮孵化Vector API的目标是让Java程序能显式利用CPU的SIMD指令单指令多数据这在图像处理、机器学习推理、科学计算这些对吞吐量敏感的场景里很关键。它提供了一套跨平台的向量计算抽象你不用为不同CPU架构写不同的intrinsic代码而是统一写FloatVector、IntVector这类APIJIT编译器会把它编译成对应平台的SIMD指令。Java 18里这个API进入第三轮孵化接口细节相比前两轮又有了一些调整。普通业务开发基本用不上但如果你的应用里有大量的数值计算并发循环关注这个方向是有价值的——等它转正的那天可能就是你优化性能的利器。// 伪代码示意孵化API包名在后续版本可能会变 var floatVector FloatVector.fromArray( FloatVector.SPECIES_128, new float[]{1.0f, 2.0f, 3.0f, 4.0f}, 0 ); var result floatVector.mul(2.0f);5.3 JEP 419Foreign Function Memory API第二轮孵化这是Java在告别JNI这条路上走出的又一步。JNIJava Native Interface虽然能调用C/C代码但编写起来繁琐、容易导致JVM崩溃、性能开销也不小。Foreign Function Memory API提供了一套更安全、更现代的方式来调用本地代码和访问本地内存纯Java代码里就能完成不需要手写C头文件和封装层。Java 18里这个API进入第二轮孵化仍然带--enable-native-accessALL-UNNAMED这样的启动参数才能用。对于大多数应用这确实是看看就好的范畴但了解这个方向能让你对Java生态的边界有更准确的认识——Java从来没有放弃和底层系统交互的能力只是在不断降低难度。5.4 一个反常规的JVM参数字符串去重需要显式开启Java 18还有一个容易被忽略的运行时改动-XX:UseStringDeduplication这个参数从默认开启变成了需要显式开启。这个参数的作用是让G1垃圾回收器自动清理堆中内容重复的String对象内部的char数组从而减少内存占用。以前它在某些场景下是默认开启的Java 18之后必须显式加上才行。java -XX:UseStringDeduplication -jar your-app.jar如果你运行的应用内存里存在大量重复字符串比如业务上大量使用相同的状态标签、枚举名字这个参数值得开起来测试一下效果。但要注意它只对G1有效而且开启后会增加CPU开销不要盲目照抄建议在压测环境里先对比堆内存和吞吐量再决定。6. finalization机制被正式标记为弃用该考虑迁移了JEP 421把finalize()方法以及相关的Runtime.runFinalization()和System.runFinalization()正式标记为弃用。这并不代表Java 18里这些功能马上就没了它只是官方再次明确表态别再用这个方法了未来某个大版本会彻底移除。finalize()在GC回收对象之前被调用曾经被用来做资源清理但它有根本性缺陷执行时机完全取决于垃圾回收器你无法预知它什么时候执行、甚至执行不执行。用它来关闭文件流、数据库连接这些宝贵资源可能导致资源长时间不被释放。替代方案很成熟用java.lang.ref.Cleaner或者AutoCloseable配合try-with-resources。比如// 不要这样 Override protected void finalize() throws Throwable { try { connection.close(); } finally { super.finalize(); } }// 推荐的方式 public class MyResource implements AutoCloseable { private Connection connection; Override public void close() { if (connection ! null) { connection.close(); } } } // 使用方 try (var resource new MyResource()) { // 业务逻辑 }另外注意开启finalize()时jvm一度提供了一个--finalizationdisabled选项来测试移除finalization机制的效果但在Java 18里这个选项还在实际上是有的不过荷包蛋的做法是别碰。如果你的项目还在用finalize()现在就该开启重构了。7. 结合实战碎碎念Java 18到底适合谁升级如果把Java 18放进选型决策框架里我的判断是已经在用Java 17 LTS的团队不必急于升级到Java 18但可以安排一两个人先在预发环境试跑、验证兼容性尤其是把jwebserver、UTF-8默认编码这两个特性吃透方便后续迁移到Java 21时无缝衔接。还在Java 8/11的团队别直接跳到Java 18因为中间的非LTS版本升级路径太短、维护时间只有半年升级到Java 21才是正路但可以把Java 18当作认知Java未来形态的预览版来研究。个人开发者和工具链维护者Java 18值得直接上手因为新特性会直接影响你写小工具和设计API的方式。框架和中间件开发者JEP 416、417、419这几个JEP对你才是真正的硬核更新值得早点花时间跟进。最后分享一个我在升级到Java 18后发现的小坑Maven项目的maven-compiler-plugin默认编译级别如果还是1.8那么即使JDK是18你也完全用不上这些新API。记得在pom.xml里显式设置release参数否则编译能过但运行时会报UnsupportedClassVersionError这属于必踩一次但知道了就再也不会忘的教训。properties maven.compiler.release18/maven.compiler.release /propertiesJava 18的意义不在于是不是LTS而在于它把一个更现代化、更统一、更高效的Java形态切切实实地推到了我们面前。当你习惯了jwebserver随手起服务、默认UTF-8带来的跨平台省心、snippet让文档和代码同源维护再回到Java 8时代写代码那种回不去了的感觉会非常强烈。新技术不是炫技它的价值最终体现在让你把注意力从环境差异和底层细节上挪开专心做业务逻辑本身。