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编号罗列更贴近实际工作。
| 分组 | 核心内容 | 日常影响 |
|---|---|---|
| 语法/API | String新增isBlank、strip、repeat、lines;Collection新增toArray(IntFunction);Files新增readString和writeString | 代码量直接变少,可读性提升 |
| 集合/Stream | Predicate.not;Stream.takeWhile、dropWhile;List.of、Set.of、Map.of | 写过滤和构造集合时最常用 |
| 类型推断 | var关键字继续完善,lambda参数也支持var | 声明局部变量更简洁,但过度使用会伤可读性 |
| 网络编程 | HttpClient正式标准化,支持HTTP/2、WebSocket | 告别原生HttpURLConnection,小工具不再强依赖第三方库 |
| 运行时/GC | G1成为默认垃圾回收器;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只处理码点小于等于U+0020的字符。换句话说,如果用户输入里混了全角空格,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版本里才被更多人放心使用。它们返回的是不可变集合,最大的价值是把“创建带初始值的集合”从多行代码压缩成一行:
List<String> names = List.of("张三", "李四", "王五"); Set<Integer> ids = Set.of(1, 2, 3); Map<String, 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 ArrayList<String>(); 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> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.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:MaxGCPauseMillis=100。
我个人的建议是:小内存应用(堆在2GB以内)可以继续使用默认G1,没必要折腾;大内存、低延迟场景可以研究ZGC,但要先做好压测。这里也提醒一句,GC参数的调整一定要有监控数据支撑,不要凭感觉加参数。很多人升级到Java 11后第一件事就是把CMS参数复制过来,结果启动直接报错,因为CMS相关参数在Java 11已经不再生效。升级时一定要清掉-XX:+UseConcMarkSweepGC这类老参数。
3.3 ZGC和Epsilon:面向未来的试验性GC
ZGC是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. HttpClient:Java 11最值得兴奋的新API
4.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(); HttpResponse<String> 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(); List<HttpRequest> requests = buildRequests(); // 构造多个请求 List<CompletableFuture<HttpResponse<String>>> futures = requests.stream() .map(req -> client.sendAsync(req, HttpResponse.BodyHandlers.ofString())) .collect(Collectors.toList()); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .join(); for (CompletableFuture<HttpResponse<String>> 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和JRE,Java 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里选JDK,Eclipse在Installed JREs里添加,Maven的settings.xml里如果硬编码了JAVA_HOME也要同步修改。配置环境变量只是第一步,工具的JDK路径不更新,系统命令是11,IDE编译还是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_HOME=C:\Program Files\Java\jdk-11.0.21我遇到过几次典型问题,先列出来供你快速定位:
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 提示“java不是内部或外部命令” | JAVA_HOME或Path没有配置正确 | 检查JAVA_HOME路径是否存在、Path是否包含%JAVA_HOME%\bin |
| java -version还是8 | 旧版本的bin目录排在Path更前面 | 把新JDK的bin目录移到Path最上方,或卸载旧JDK |
| javac有输出但java显示其他版本 | 系统中存在多个JDK,java.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.JAXBException | JAXB从JDK移除 | 引入jaxb-api和jaxb-runtime依赖 |
| ClassNotFoundException: javax.annotation.PostConstruct | JSR-250注解不在默认模块 | 引入jakarta.annotation-api依赖 |
| java.lang.NoClassDefFoundError: javax/activation/DataHandler | JAF模块被移除 | 引入javax.activation:activation依赖 |
| Unsupported major version 55 | class文件由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,是我实际用下来最平滑的方式。