
Java 11 这个版本放在整个Java生态里都是一个绕不开的转折点。它既是Java 8之后真正意义上的长期支持版本LTS又第一次把Oracle JDK的免费许可开放到了个人开发和多数生产场景。很多人问我Java 11有哪些新特性我自己的回答是别只盯着语法糖先把字符串API、HttpClient、模块化边界和垃圾回收器这四块吃透再考虑从Java 8平滑迁移这才是这个版本真正的价值所在。这篇文章不打算逐条念官方Release Notes我想以实际使用的视角把Java 11里我用过、踩过坑、后来沉淀成团队规范的东西一次讲透。适合正准备升级的老项目、正在选型新项目基础版本的同学以及想在Windows 10上第一次装好Java 11、把环境变量配置顺手搞定的小白。1. Java 11到底新在哪先建立整体认知1.1 为什么Java 11是最不能错过的版本Java 11发布于2018年9月但它的地位放到今天依然很特殊。Java 8虽然普及率极高但已经是很老的版本而Java 9和Java 10都是“短命”版本官方不提供长期维护企业根本不敢把生产环境压上去。Java 11是Oracle在模块化大改动之后给出的第一个真正稳定的长期支持版本维护周期覆盖很多年这才让大批团队第一次有了“从Java 8往前迈一步”的理由。另一个关键变化是许可模式。Oracle JDK从11开始在OTN许可协议下可以免费用于开发、测试和一些生产场景这就打破了过去“商用要付费”的普遍顾虑。当然这里要提醒一句如果你的场景在许可边界上拿不准最稳妥的做法是直接使用OpenJDK发行版比如Adoptium Temurin、Amazon Corretto、Microsoft OpenJDK功能一致且没有商业协议负担。我团队里的新项目现在基本都是Temurin 11起步省心。1.2 新特性全景一览把Java 11涉及的JEP快速过一遍能帮你在后续阅读官方文档时建立索引。我按使用角度重新分组比按JEP编号罗列更贴近实际工作。分组核心内容日常影响语法/APIString新增isBlank、strip、repeat、linesCollection新增toArray(IntFunction)Files新增readString和writeString代码量直接变少可读性提升集合/StreamPredicate.notStream.takeWhile、dropWhileList.of、Set.of、Map.of写过滤和构造集合时最常用类型推断var关键字继续完善lambda参数也支持var声明局部变量更简洁但过度使用会伤可读性网络编程HttpClient正式标准化支持HTTP/2、WebSocket告别原生HttpURLConnection小工具不再强依赖第三方库运行时/GCG1成为默认垃圾回收器ZGC、Epsilon进入实验阶段大内存低延迟场景多了选择兼容性Java EE模块被移除、CORBA移除、JavaFX移除、Nashorn废弃老项目升级最容易在这里爆雷启动/部署支持直接运行单文件Java源码AppCDS应用类数据共享增强写脚本和微服务启动调优更方便安全/协议TLS 1.3默认支持网络安全合规方面的硬需求看完这张表你会发现Java 11不像Java 8当年那样是“翻天覆地”的语法革命它更像一次全面地“清扫和补强”。上承Java 9的模块化和Java 10的var下接后续版本的持续演进Java 11是把前面版本欠的债还完、把积攒的功能打磨到可用的关键节点。2. 日常开发最用得上的新特性2.1 字符串API增强isBlank、strip、repeat、lines字符串API是Java 11里我使用频率最高的新特性没有之一。先看一段最直接的演示String name hello ; System.out.println(name.strip()); // hello System.out.println(name.stripLeading()); // hello System.out.println(name.stripTrailing()); // hello System.out.println(abc.repeat(3)); // abcabcabc System.out.println( .isBlank()); // true System.out.println(a\nb\nc.lines().count()); // 3这里有个容易踩的坑很多人觉得strip就是trim的别名其实strip是基于Character.isWhitespace判断的它能处理中文全角空格这种Unicode空白而trim只处理码点小于等于U0020的字符。换句话说如果用户输入里混了全角空格trim根本去不掉strip才能干净处理。我在做表单校验时已经全面切到strip。lines()方法也很实用。以前要把多行文本拆成List得自己按换行符split还要处理换行格式差异。现在直接text.lines().toList()即可内部已经兼容\n、\r\n、\r三种换行。注意这里的toList是Java 16才加到Stream上的在Java 11里应该写collect(Collectors.toList())别在JDK 11环境下拿Stream.toList直接编译编译期就会报错。2.2 集合与Stream的便利更新List.of、Set.of、Map.of是Java 9引入的但在Java 11这个LTS版本里才被更多人放心使用。它们返回的是不可变集合最大的价值是把“创建带初始值的集合”从多行代码压缩成一行ListString names List.of(张三, 李四, 王五); SetInteger ids Set.of(1, 2, 3); MapString, Integer score Map.of(语文, 90, 数学, 95);用这几个方法要注意三点不能向返回的集合里添加元素否则抛UnsupportedOperationException。集合中不允许null元素Map也不允许null key和null value。Map.of最多支持10对键值超过10个要用Map.ofEntries。Stream在Java 11里也有两个高频操作takeWhile和dropWhile。它们和filter最大的区别是filter会遍历整个流而takeWhile遇到第一个不满足条件的元素就立刻截断dropWhile则跳过开头满足条件的元素。最适合处理有序数据比如日志中时间是递增的想取最近10分钟的数据就可以用takeWhile来快速短路避免无谓的全量遍历。另外Predicate.not是Java 11新增的静态方法。以前写“取反过滤”要么用!包一层要么单独写个方法引用现在可以直接Stream.of(, hello, , world) .filter(Predicate.not(String::isBlank)) .forEach(System.out::println);配合刚说的isBlank这行代码在清洗输入数据时极其好用。2.3 var说起来简单但有几个细节要记住var是Java 10引入的局部变量类型推断Java 11里补上了“lambda参数也支持var”这块拼图。最常见的写法var list new ArrayListString(); list.add(test); list.forEach((var s) - System.out.println(s));lambda参数使用var有一个关键限制要么全部参数都写var要么全部不写不能混合。它真正的应用场景是需要给lambda参数加注解时以前必须显式声明类型现在可以写成list.forEach((NotNull var s) - System.out.println(s));在使用var时我给自己定了三条纪律能用清晰类型声明的地方不强行用var。例如var user getUser()这种就没有任何信息量。变量名必须起得足够明确因为类型被隐藏后名字就是读者唯一的线索。绝不用var声明局部变量后再改变它的实际类型这会让代码变成“跟踪谜题”。var并不是关键字它更像一个“保留类型名”所以var var 1这种代码其实是合法的只是可读性极差。还有几个常见误区var不能用于字段、方法参数和返回类型不能声明数组var[] arr不能在声明时不初始化var x;也不能初始化为null。这些在编译期就会报错熟悉一下即可。3. 升级Java 11前必须知道的兼容性和移除项3.1 Java EE模块和CORBA被移除这是老项目从Java 8升级到Java 11时最大的拦路虎。JDK 11直接移除了java.se.ee根模块下的六个模块JAXB、JAX-WS、JAF、JTA、CORBA还有JavaFX也单独拆出去独立发布了。换句话说如果你的老项目直接在代码里用了javax.xml.bind.JAXBContext这类类升级后编译期就会报ClassNotFoundException。我记得当时迁移一个老服务启动后立刻爆ClassNotFoundException: javax.xml.bind.JAXBException排查了半天才发现是XML配置解析用到了JAXB。解决办法是在pom里显式引入依赖dependency groupIdjakarta.xml.bind/groupId artifactIdjakarta.xml.bind-api/artifactId version2.3.3/version /dependency dependency groupIdorg.glassfish.jaxb/groupId artifactIdjaxb-runtime/artifactId version2.3.3/version /dependency这里特别注意groupId和包名新版本的Jakarta EE会把javax.xml.bind迁移到jakarta.xml.bind但JAXB 2.3.3及之前的版本包名仍然是javax.xml.bind如果你的代码改动越少越好就用2.3.x版本。如果后续要用Jakarta EE 9包名会变成jakarta代码里的import也要跟着改。这个选择直接影响工作量。3.2 G1成为默认CMS正式告别从Java 9开始G1就是默认垃圾回收器Java 11继续沿用。CMS在Java 9被标记废弃Java 14才会被真正移除但在Java 11里如果还显式指定CMS启动会看到大量警告信息。对于大多数Web应用G1的停顿时间模型比ParallelGC更适合业务场景它能设置期望的GC停顿目标默认是200毫秒常用参数是-XX:MaxGCPauseMillis100。我个人的建议是小内存应用堆在2GB以内可以继续使用默认G1没必要折腾大内存、低延迟场景可以研究ZGC但要先做好压测。这里也提醒一句GC参数的调整一定要有监控数据支撑不要凭感觉加参数。很多人升级到Java 11后第一件事就是把CMS参数复制过来结果启动直接报错因为CMS相关参数在Java 11已经不再生效。升级时一定要清掉-XX:UseConcMarkSweepGC这类老参数。3.3 ZGC和Epsilon面向未来的试验性GCZGC是Java 11引入的实验性垃圾回收器目标是让GC停顿时间不随堆大小增长号称STW时间控制在10毫秒以内。它通过染色指针和读屏障实现并发标记、并发转移适合堆内存动辄几十GB、又要低延迟的服务。想试ZGC很简单加两个参数-XX:UnlockExperimentalVMOptions -XX:UseZGC但请务必加上“实验性”的心态看待它。我实测过ZGC在超大堆场景下的确亮眼但它在Java 11里还不支持某些功能比如类卸载在某些配置下不完整而且CPU占用会比G1高。生产环境如果要上ZGC最好等到Java 15之后的版本功能更成熟。Epsilon则是一个“不回收垃圾”的GC只做内存分配主要用于性能测试和短暂任务生产环境基本不会用它。3.4 直接运行Java源文件单文件源码启动JEP 330让Java 11可以直接通过java Xxx.java运行单个源文件不用先javac编译。这对写小工具、脚本、教学demo非常友好再也不用为一个小文件折腾编译参数。java Hello.java这里有一个实际限制该模式只适合“单文件源码级”运行如果你的程序依赖classpath上的第三方jar包或者跨文件引用多个java文件还是需要正常编译。而且它没有把源码编译输出到磁盘每次运行都会临时编译性能上不划算。我的经验是写算法题、跑测试片段、临时脚本用这个功能很爽但正式项目千万别用这种启动方式别人接手会懵。4. HttpClientJava 11最值得兴奋的新API4.1 同步请求三行代码搞定HTTP调用Java 11把HttpClient从孵化器转为标准API这真的是很多Java开发等了好多年的功能。以前用原生HttpURLConnection写请求又啰嗦又难用只能依赖Apache HttpClient或OkHttp。现在JDK自带的就能完成大部分工作。先看一个最基础的GET请求import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://api.example.com/user/1)) .timeout(Duration.ofSeconds(30)) .header(Accept, application/json) .GET() .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body());这基本上是“开箱即用”的体验。请求体的设置也很直观POST JSON可以这样HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://api.example.com/user)) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString({\name\:\张三\})) .build();这里我想强调一下BodyHandlers和BodyPublishers的设计。它们是响应和请求的“Body处理器”除了ofString还有ofByteArray、ofFile、ofInputStream、ofLines等选择。比如要下载文件用BodyHandlers.ofFile(Paths.get(xxx.zip))即可不需要手动处理流的生命周期。4.2 异步请求用CompletableFuture做并发HttpClient真正的优势在于异步。它基于CompletableFuture实现不阻塞当前线程在高并发调用场景下比同步发送要高效得多HttpClient client HttpClient.newBuilder() .executor(Executors.newFixedThreadPool(10)) .build(); ListHttpRequest requests buildRequests(); // 构造多个请求 ListCompletableFutureHttpResponseString futures requests.stream() .map(req - client.sendAsync(req, HttpResponse.BodyHandlers.ofString())) .collect(Collectors.toList()); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .join(); for (CompletableFutureHttpResponseString future : futures) { System.out.println(future.join().body()); }这里有个隐性问题CompletableFuture默认使用ForkJoinPool.commonPool线程数等于CPU核心数。如果你的业务是IO密集型比如要并发调用十几个下游接口默认线程池很容易成为瓶颈。建议通过HttpClient.newBuilder().executor(...)显式指定一个线程池或者在最终join()时配置好超时时间避免某个下游接口迟迟不响应导致整个调用链卡死。我写异步调用时还有一个习惯对每个future都做exceptionally处理把异常转换为一个默认结果或错误包装。否则一个接口异常虽然不会中断其他请求但最后的join会直接抛出CompletionException排错时很难定位是哪个请求出了问题。4.3 WebSocket客户端和连接管理Java 11的HttpClient还内置了WebSocket客户端不需要再引第三方库。最基础的用法HttpClient client HttpClient.newHttpClient(); WebSocket webSocket client.newWebSocketBuilder() .buildAsync(URI.create(ws://localhost:8080/ws), new WebSocket.Listener() { Override public CompletionStage? onText(WebSocket webSocket, CharSequence data, boolean last) { System.out.println(收到消息: data); return null; } }) .join(); webSocket.sendText(hello, true);这里有一个细节WebSocket.Listener的每个回调都返回CompletionStage目的是实现背压控制——只有前一个CompletionStage完成才会继续触发下一个回调。这是Java 11 WebSocket和很多简单封装的第三方库不一样的地方。如果你返回null相当于立刻完成不控制速度。在生产环境处理大量实时消息时这个机制很有价值。5. Java 11下载与Windows 10环境变量配置指南5.1 下载前先选对JDK发行版很多人一搜“Java 11下载”就直接进了Oracle官网其实现在选择已经很多了。我的建议是开发环境用Oracle JDK或Temurin都行生产环境优先OpenJDK系发行版比如Temurin、Corretto理由就是许可清晰、更新及时、社区验证充分。如果你在Windows 10上安装下载时注意两个东西一是要选对架构现在基本是x64但有人拿32位安装包装64位系统就会莫名报错二是要区分JDK和JREJava 11开始Oracle不再单独发布JRE安装包如果你想运行Java程序直接安装完整JDK即可。下载之后得到的是exe安装包或者zip压缩包zip版解压后配置环境变量就能用也方便做多版本切换。5.2 Windows 10安装与配置JAVA_HOME如果使用exe安装一路Next即可但要注意安装在哪个目录后面配置环境变量会用到。如果是zip包先解压到一个干净目录比如C:\Program Files\Java\jdk-11.0.21然后按下面步骤操作右键“此电脑”选择“属性”点击“高级系统设置”。点击“环境变量”在“系统变量”区域点击“新建”。变量名填写JAVA_HOME变量值填写JDK安装路径例如C:\Program Files\Java\jdk-11.0.21注意不要带bin目录。在系统变量里找到Path选中后点击“编辑”点击“新建”添加%JAVA_HOME%\bin。把%JAVA_HOME%\bin通过“上移”按钮移到最上面避免和其他JDK版本冲突。一路确定保存然后重新打开一个命令行窗口。这里特别强调一个容易出问题的地方JAVA_HOME的值一定不要写成C:\Program Files\Java\jdk-11.0.21\bin配置的是JDK根目录Path里才拼bin。很多教程都栽在这个细节上。还有一些开发工具需要单独设置。IDEA里可以在Project Structure里选JDKEclipse在Installed JREs里添加Maven的settings.xml里如果硬编码了JAVA_HOME也要同步修改。配置环境变量只是第一步工具的JDK路径不更新系统命令是11IDE编译还是8这种情况很常见。5.3 验证配置和常见配置错误配好后重新打开命令行窗口依次输入java -version javac -version echo %JAVA_HOME% where java期望看到类似java version 11.0.21 2023-10-17 LTS javac 11.0.21 JAVA_HOMEC:\Program Files\Java\jdk-11.0.21我遇到过几次典型问题先列出来供你快速定位现象原因解决方法提示“java不是内部或外部命令”JAVA_HOME或Path没有配置正确检查JAVA_HOME路径是否存在、Path是否包含%JAVA_HOME%\binjava -version还是8旧版本的bin目录排在Path更前面把新JDK的bin目录移到Path最上方或卸载旧JDKjavac有输出但java显示其他版本系统中存在多个JDKjava.exe路径不对用where java查看实际匹配路径清理PATH中的旧项重启后命令窗口仍不生效环境变量修改后需要重开终端重新打开cmd不要使用修改前已打开的窗口如果验证通过可以顺手写个Hello World测试一下编译和运行是否正常echo public class Main { public static void main(String[] args) { System.out.println(Java11 OK); } } Main.java javac Main.java java Main6. 迁移到Java 11的实战避坑与心得6.1 常见问题速查表这节是把Java 8项目迁移到Java 11后我实际遇到并帮助同事解决过的问题汇总异常/现象根因解决方案ClassNotFoundException: javax.xml.bind.JAXBExceptionJAXB从JDK移除引入jaxb-api和jaxb-runtime依赖ClassNotFoundException: javax.annotation.PostConstructJSR-250注解不在默认模块引入jakarta.annotation-api依赖java.lang.NoClassDefFoundError: javax/activation/DataHandlerJAF模块被移除引入javax.activation:activation依赖Unsupported major version 55class文件由Java 11编译运行环境是旧JDK运行时升级到JDK 11或降低编译release版本--add-opens相关警告模块化后反射访问限制明确使用add-opens开放指定包编译时报“不兼容的 types”代码中误用已移除API全局搜索javax.xml、javax.ws等包名逐一替换JavaFX应用启动失败JavaFX已从JDK移除单独引入OpenJFX依赖或改用独立版本这里最关键的经验是迁移前先做一次“依赖审计”。把pom.xml和所有lib目录里的jar包扫一遍凡是涉及javax.annotation、javax.xml.bind、javax.jws、javax.transaction的大概率都要补依赖。这一步提前做了后面能少加一晚上的班。6.2 哪些新特性优先用哪些先观望我给团队定的Java 11特性采用策略是分层的第一梯队立刻用String.strip和isBlank、Files.readString和writeString、List.of/Set.of/Map.of、Predicate.not、HttpClient的新项目优先使用。这些API无副作用、学习成本低、收益明确。第二梯队看场景用var在局部变量和lambda参数中使用但要求代码评审严格把关单文件源码启动只用于脚本和工具HTTP/2在内部服务间如果基础设施支持值得开启。第三梯队先观望ZGC和Epsilon这类实验性GC必须经过充分压测和监控验证后才考虑直接源码运行不适合生产模块化相关的高级用法除非你是做框架或基础组件否则不必为了模块化而模块化。6.3 由一次迁移引发的个人体会从Java 8迁到Java 11我最大的感受是“惊喜比预想的多坑也比预想的集中”。惊喜在于代码确实变短了HttpClient写起来顺手G1默认配置的GC表现比预期稳。坑则集中在移除模块上基本都是ClassNotFoundException而且往往要到运行期才炸出来。后来摸索出一套稳定的迁移流程先补依赖再改代码最后跑回归。补依赖阶段把jaxb、annotation、activation这些老库全部显式声明改代码阶段用全局搜索把javax.xml等相关import挨个替换跑回归时重点测XML解析、WebService调用、反射工具和启动脚本。整个流程走通后老项目升级Java 11的成本其实比想象中低收益却非常实在。最后再分享一个小技巧如果短期内没法完整迁移可以用--release 8参数让Java 11编译器输出Java 8版本的字节码这样开发环境先切换到JDK 11产出的class文件仍然能被Java 8运行时加载。这个参数可以让团队在“还没彻底升级生产”的过渡期提前享受新版编译器和工具体验为真正的迁移争取时间。先跑起来再逐步替换老API是我实际用下来最平滑的方式。