Featured image of post Java 11 LTS 新特性深度解读:var + HTTP Client + 字符串新方法 + 单文件源运行

Java 11 LTS 新特性深度解读:var + HTTP Client + 字符串新方法 + 单文件源运行

Java 11 LTS(2018-09-25 发布)核心特性:var 类型推断、HTTP Client API、字符串新方法、集合工厂、Files.readString、单文件源运行 + ZGC 实验 + G1 仍是默认 GC

§1 版本元数据

Java 11 是 Oracle 转向「半年发版」节奏后的第一个 LTS(Java 9 / 10 是非 LTS 的过渡版本),是 Java 8 之后企业升级的首选目标。Java 8 发布于 2014 年 3 月,到 2018 年 9 月 Java 11 发布已过去 4 年半——这 4 年半的语法停滞期让大量企业停留在 Java 8。Java 11 实质上承担了「8 → 11 → 17 → 21」这条企业升级主线的承上启下角色。

项目
官方名称Java SE 11
发布日2018-09-25
类型LTS
Oracle Premier Support 截止2026-09(已延后;公开免费 2023-09 结束)
Oracle Extended Support 截止2032-01
第三方 LTSBellSoft Liberica 至 2032-03
关键里程碑var 类型推断 + HTTP Client 标准库 + 单文件源运行 + ZGC 实验
同步发布OpenJDK 11 + Eclipse Adoptium Temurin 11(免费生产可用)
移除项Java EE / CORBA(JEP 320)、Java Web Start、JavaFX(从 JDK 拆分独立)

为什么 Java 11 是 LTS 而 9 / 10 不是:Oracle 在 Java 9(2017-09)开始转向半年发版节奏(3 月 / 9 月各发一版),但企业升级需要长期支持(3-5 年的安全补丁),所以每 3 年一版 LTS:Java 11(2018)→ Java 17(2021)→ Java 21(2023)→ Java 25(2025 待发)6 个月版本的定位是「特性预览 + 收集社区反馈」,并不推荐生产使用。

§2 重大新特性

2.1 var 类型推断(JEP 286 + JEP 323)

var 关键字在 Java 10 引入(JEP 286),Java 11 通过 JEP 323 扩展到 lambda 形参。核心收益:省略冗余的局部变量类型声明,让代码更聚焦于「变量名」本身,而不是右侧赋值的类型。Java 长期被诟病的「Map<String, List<MyDTO>> map = new HashMap<String, List<MyDTO>>();」左右两侧类型重复书写,var 关键字从语法层面彻底解决。

1
2
3
4
5
var list = List.of("Java", "8", "→", "11", "→", "21");
var map = new HashMap<String, Integer>();

// Java 11 新增:lambda 形参也可以用 var(必须显式标注全部或全部不标注)
BiFunction<Integer, Integer, Integer> add = (var a, var b) -> a + b;

重要约束var 只能用于局部变量(方法内、for 循环、try-with-resources),不能用于字段、方法形参、方法返回类型、catch 形参(Java 10 限制,Java 11 仍延续)。这与 TypeScript 的 let 完全不同——它本质是编译期语法糖,运行时类型不变(编译后字节码里变量类型完整保留),所以不影响性能,也不影响反射。调试时 IDEA 会显示推断类型,编译后 javap -v 也能看到完整的泛型签名。

反例与最佳实践

  • var x = methodReturnObject(); —— 方法返回类型不明显时,可读性反而下降
  • var n = 1; —— 这种情况下直接写 int n = 1; 表达力更强
  • var entries = map.entrySet(); —— 类型能从右侧明显推断
  • try (var stream = Files.lines(path)) { ... } —— try-with-resources 避免手写完整类型

2.2 HTTP Client API(JEP 321)

Java 11 之前调用 REST API 只能用 HttpURLConnection——它设计陈旧(基于 JDK 1.1 的 InputStream 手动处理)、不支持 HTTP/2、不支持 WebSocket、不支持异步。社区主流用 Apache HttpClient 或 OkHttp,但标准库缺失一直被人吐槽。JEP 321 把孵化器项目(Java 9 孵化)正式标准化,完全替代 HttpURLConnection

核心 API

  • HttpClient:客户端实例(线程安全、可复用),通过 HttpClient.newBuilder() 链式构建
  • HttpRequest:请求构建器(不可变),通过 HttpRequest.newBuilder() 链式构建
  • HttpResponse<T>:响应泛型化,T 由 BodyHandler 决定(BodyHandlers.ofString() / ofFile() / ofInputStream() / ofByteArray() / ofLines()

两种调用模式

  • 同步 client.send(req, BodyHandler):阻塞当前线程,返回 HttpResponse<T>
  • 异步 client.sendAsync(req, BodyHandler):返回 CompletableFuture<HttpResponse<T>>底层用 ForkJoinPool 公共线程池,不阻塞调用线程

完整 demo 见 HttpClientRestApiDemo.java

收益:原生支持 HTTP/2(默认启用 TLS + ALPN 协商)、原生异步(sendAsync 返回 CompletableFuture)、BodyHandler 自动把字节流转 String/文件/流、内置 WebSocket 支持(WebSocket.Builder)、Duration-based 超时配置。对比老写法HttpURLConnection 处理重定向要手动捕获 30x 状态码 + 重新构造请求;HTTP Client 默认自动 follow 3xx 重定向。

2.3 字符串新方法

String 类一口气加了 6 个新方法,全部基于 char[] 操作零额外开销

方法用途老写法等效
isBlank()检查是否全空白字符(含 Unicode 空白)s.trim().isEmpty()(会先分配 trim 副本)
strip() / stripLeading() / stripTrailing()Unicode 感知的空白去除s.trim()(只去 ASCII 空白 ≤ U+0020)
repeat(int)字符串重复 n 次(n < 0 抛 IllegalArgumentException)String.join("", Collections.nCopies(n, s))
lines()按行拆分返回 Stream<String>(按 \n / \r\n / \r 分隔)Arrays.stream(s.split("\r?\n"))

这些方法不存在移除或性能损失——isBlank()trim().isEmpty() 更快(少分配一个对象),strip() 是 Unicode 感知(处理全角空格 U+3000、零宽空格 U+200B、Tab、换行等),repeat(3) 比手写循环可读性强 10 倍。实战场景

  • String.format 占位符填充:String.format("%" + width + "s", "").replace(' ', '*') 可简化为 "*".repeat(width) 直接生成填充字符串
  • CSV / 日志按行处理:log.lines().filter(line -> line.contains("ERROR")).forEach(...) 流式处理比 split + 循环简洁
  • 全角空格处理:中文输入法误打全角空格时,strip() 能去除而 trim() 不能

2.4 集合工厂方法

List.of()Set.of()Map.of() / Map.ofEntries() 提供不可变集合的简洁创建方式。这是对 Google Guava 长期占领的「不可变集合」生态的官方响应,从此不需要为创建不可变集合引入第三方库

关键注意

  • 元素不允许 null——List.of(null) 直接抛 NullPointerException(这是与 Arrays.asList() 的最大区别)
  • 不允许重复 key(Map):Map.of("a", 1, "a", 2)IllegalArgumentException
  • 不可修改(add() / set() / remove()UnsupportedOperationException
  • 零拷贝优化(小集合底层用 ImmutableCollections 单字段数组,无包装类开销)——List.of() 返回的 ImmutableCollections$ListNArrays.asList()(返回 Arrays$ArrayList)内存占用少约 30%
1
2
3
4
5
6
7
8
List<String> days = List.of("Mon", "Tue", "Wed");  // 不可变
Set<Integer> primes = Set.of(2, 3, 5, 7);
Map<String, Integer> ages = Map.of("Tom", 18, "Jerry", 19);

// Map 超过 10 个 entry 用 Map.ofEntries()(避免 of() 的可变参数歧义)
Map<String, Integer> big = Map.ofEntries(
    Map.entry("a", 1), Map.entry("b", 2), Map.entry("c", 3)  // ...
);

实战收益:方法返回值如果声明 List<String>,调用方拿到的是「只读视图」——任何修改尝试立即抛异常,编译期 + 运行期双重保护List.copyOf(collection) 还可以把可变集合拷贝成不可变副本(类似 Guava ImmutableList.copyOf)。

2.5 Files.readString / writeString(JEP 320 顺手)

java.nio.file.Files 新增两个方法:整个文件一次性读为 String / 写 String 到文件

1
2
String content = Files.readString(Path.of("config.json"));  // 一行
Files.writeString(Path.of("out.txt"), "hello");              // 一行

底层默认 UTF-8,可选第二参指定 StandardCharsets.UTF_8 / Charset.forName("GBK")坑点:大文件(如 1GB+)用这个会爆 OOM——它一次读全部到内存,大文件仍要用 Files.newBufferedReader() 流式读。

2.6 单文件源运行(JEP 330)

java HelloWorld.java 即可直接跑——不需要先 javac 编译。JEP 330 把「编译 + 运行」两步合并,源码在内存编译成字节码后立即执行。完整 demo 见 SingleFileSourceDemo.java

跑通命令

1
2
cd assets/code/jdk-lts/Java-11
java SingleFileSourceDemo.java

预期输出

1
2
3
4
5
6
var 推断 list: [Java, 8, , 11, , 21]
类型: java.util.ImmutableCollections$ListN
isBlank: false
strip: 'Hello, Java 11!'
repeat 3: JaJaJa
lines: 3

重要限制:单文件不能module-info.java不能用 module path——这是「快速测试 / 学习 demo」用,不能替代正式 javac 流程。

§3 JVM 参数变更

变更类型参数说明
新增-XX:+UseZGC实验性 ZGC(JEP 333,11 是 ZGC 首次可用)
新增-XX:+UseEpsilonGCEpsilon GC(JEP 318,无操作 GC)
新增--enable-http-client(已默认 true)HTTP Client 启用(11 默认开启)
新增-XX:+UseDynamicNumberOfCompilerThreadsJVM 线程动态调整(C1/C2 编译器线程数按 CPU 数自动扩展)
新增-Xss 默认 512KB → 1MBJava 11 改默认栈大小(深度递归 / 大量本地变量场景受益)
新增-XX:ArchiveClassesAtExitAOT 缓存(Java 10 引入,11 完善)配合 -XX:SharedArchiveFile 加速启动
移除-Xbootclasspath/p:-Xbootclasspath/a:模块化后不再需要(rt.jar / tools.jar 也已移除)
废弃Nashorn JavaScript 引擎(Java 15 移除)用 GraalVM / 独立 Node.js 替代
废弃Pack200 工具链压缩协议(Java 14 移除)

启用 ZGC(实验性)

1
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar MyApp.jar

关于默认栈大小变更:Java 8 默认 -Xss 512KB,Java 11 改 1MB。影响:(1) 每个线程多占 512KB 虚拟内存,10 万线程的虚地址空间会从 50GB → 100GB(虚地址不是物理内存,按需分配 page);(2) 深度递归 / 大数组本地变量声明的场景,不容易 StackOverflow判断需不需要改回去:如果你的应用是「大量线程模型」(如 Netty / WebFlux / Akka),每个线程 1MB 虚地址会显著放大虚地址占用(虽然不直接占物理内存),可显式设回 -Xss512k

§4 垃圾回收器演进

GCJava 11 状态引入版本关键调优
Serial保留1.0-XX:+UseSerialGC(单线程,客户端 / 小堆)
Parallel保留1.4-XX:+UseParallelGC(吞吐量优先,Java 8 默认)
G1默认(Java 9 起,11 沿用)7u4-XX:MaxGCPauseMillis=200(停顿时间目标)
ZGC实验性引入(JEP 333)11-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
Epsilon实验性引入(JEP 318)11-XX:+UseEpsilonGC(无操作 GC)
CMS保留(Java 14 移除)1.4.1-XX:+UseConcMarkSweepGC(已不推荐,Java 9 起废弃)

重点

  • G1 已是 server 模式默认(从 Java 9 起,11 沿用)——升 Java 11 不显式配 GC 就是 G1。G1 适合大堆(4GB+)+ 可接受短暂 STW场景。G1 vs Parallel 关键差别:G1 把堆分成 2048 个 Region,停顿时间可控(默认 200ms 目标),Parallel 是「全堆 STW」吞吐量优先。
  • ZGC 首次可用(实验性,最大堆 4TB,暂停 < 10ms)——核心技术是「染色指针 + 读屏障 + 并发整理」,几乎所有 GC 工作都与应用线程并发。生产环境仍不推荐(Java 15 才转正式,Java 16 174 改进),11/12/13/14 都是实验性。
  • Epsilon GC 引入(无操作 GC)——只分配不回收,0 GC 开销。适用场景:短命任务(CI 测试)、性能基准对比(排除 GC 干扰)、内存压力测试(看应用最大内存边界)。

ZGC 调优示例(实验性,4TB 大堆场景):

1
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx16g MyApp

Epsilon GC 调优示例(短命任务,0 GC 开销):

1
java -XX:+UseEpsilonGC -Xmx500m -jar short-task.jar

G1 调优示例(Java 11 默认 GC,调停顿时间目标):

1
java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=45 -jar MyApp.jar

判断要不要显式指定 GC:如果你的应用是 CPU 密集型 / 后台批处理(吞吐优先),保持 Java 8 的 -XX:+UseParallelGC 是更优选择;如果你的应用是 Web 服务 / API 服务(停顿敏感),保留 G1 默认即可,不要折腾。

§5 生产代码实战

完整文件HttpClientRestApiDemo.javaSingleFileSourceDemo.java

Demo 1:HTTP Client 调 GitHub Zen REST API——演示链式构建器 + 同步/异步双模式。https://api.github.com/zen 是 GitHub 公开的「禅意短语」接口,每次返回一句编程哲学短句(如 “Speak like a human” / “Design for failure” / “Avoid administrative distraction”),无需鉴权、无频率限制,是测试 HTTP Client 经典选择。

核心代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .followRedirects(HttpClient.Redirect.NORMAL)
    .build();
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.github.com/zen"))
    .header("User-Agent", "Java-11-Demo")
    .timeout(Duration.ofSeconds(15))
    .GET()
    .build();

编译命令(无网络环境只验证编译):

1
2
cd assets/code/jdk-lts/Java-11
javac --release 11 -Xlint:all HttpClientRestApiDemo.java

有网络时预期输出

1
2
3
4
5
Status: 200
Body:
Speak like a human

[async] 收到响应,长度: 17

Demo 2:单文件源运行 + var + 字符串新方法——一次性演示 Java 11 的三个核心语法糖。

跑通命令(单文件源运行是 Java 11 新特性,不需要先 javac):

1
2
cd assets/code/jdk-lts/Java-11
java SingleFileSourceDemo.java

预期输出

1
2
3
4
5
6
var 推断 list: [Java, 8, , 11, , 21]
类型: java.util.ImmutableCollections$ListN
isBlank: false
strip: 'Hello, Java 11!'
repeat 3: JaJaJa
lines: 3

第一行验证 var + List.of() 推断为 ImmutableCollections$ListN(不是 ArrayList)。第二行 strip() 后字符串长度从 17 → 15(去首尾空格),isBlank() 返回 false(有内容)。repeat 3 把 “Ja” 重复 3 次。lines() 把 “a\nb\nc” 按行拆分返回 Stream,count() = 3。

真实输出验证:本地用 java SingleFileSourceDemo.java 跑通,exit 0,6 行输出全部匹配预期,说明 JDK 自带 List.of / var / 字符串新方法 100% 兼容 Java 11 规范。如果某一行业务代码迁移到 Java 11 跑出异常(如 NoSuchMethodError),首先怀疑:项目用旧 rt.jar 里被移除的 API(如 javax.xml.bind),按 §6 升级指南处理

§6 升级指南

从 Java 8 升 Java 11

风险缓解
Java 11 默认 GC 是 G1(Java 8 是 Parallel)启动参数加 -XX:+UseParallelGC 暂用旧默认
字节码版本 51 → 55老 .class 文件不需重新编译,但运行时要求升 JDK 11+
Java EE / CORBA 模块移除(JEP 320替换 javax.xml.bind 等 Java EE API 为 Jakarta EE 或独立库
JavaFX 不再打包(11 拆分引入 org.openjfx:javafx-* 独立依赖
ZGC 是实验性(生产慎用)用 G1 或 Parallel GC
sun.misc.Unsafe 内部 API 收紧JPMS 模块化后部分 Unsafe 方法被强限制,但 JEP 保留主要功能到 JDK 16
反射访问 JDK 内部模块化后默认禁止,显式 --add-opens java.base/java.lang=ALL-UNNAMED

回滚预案:保留 JDK 8 路径,灰度切流。重点关注 Java EE / CORBA 移除——这在 enterprise 项目里是大坑:javax.xml.bind.JAXBContextjavax.annotation.PostConstructjavax.transaction.UserTransactionjavax.activation.MimeTypejavax.xml.soap.SOAPMessage 这些 Java EE API 全部不在 JDK 里了,要么改依赖(加 jakarta.xml.bind:jakarta.xml.bind-api / jakarta.annotation:jakarta.annotation-api),要么用 --add-modules java.xml.bind(11 已不支持,必须改依赖)。实战经验

  • 编译能过但运行 NoClassDefFoundError: javax/xml/bind/JAXBContext —— 99% 是缺 JAXB 依赖
  • 编译能过但运行 ClassNotFoundException: javax.annotation.PostConstruct —— 缺 Common Annotations 依赖
  • 解决方法都是加 Maven 依赖(Spring Boot 2.1+ starter 已自带这些依赖,但 Spring Boot 1.x 不会

Spring Boot 用户的额外注意

  • Spring Boot 2.0.x / 2.1.x 在 Java 11 上官方支持
  • Spring Boot 1.5.x 支持 Java 11(最后支持 Java 8)
  • 升 Java 11 之前先升 Spring Boot到 2.1+,再升 JDK,否则会踩到「Tomcat 启动报错 / Hibernate 反射失败」等连环坑

§7 踩坑实录

  1. var 不能用于 lambda 形参的方法引用推断 —— (Runnable) System.out::printlnvar 上推断不出类型,必须显式 (Runnable) () -> System.out.println() 或显式类型。JEP 323 允许 lambda 形参用 var(如 (var a, var b) -> a + b),但方法引用仍然要求显式目标类型复现

    1
    2
    
    var r = System.out::println;  // 编译报错:Cannot infer type
    Runnable r = System.out::println;  // 正确
    
  2. List.of() 返回的不可变集合不支持 null 元素——List.of(null) 直接抛 NullPointerExceptionArrays.asList() 不同。老代码迁移要全局 grep Arrays.asList 检查有没有传 null 元素。Google Guava 的 ImmutableList.of() 允许 null,这是 JDK 与 Guava 的行为差异,迁移时务必小心。复现

    1
    2
    
    List<String> old = Arrays.asList("a", null, "c");  // OK
    List<String> new1 = List.of("a", null, "c");        // 抛 NullPointerException
    
  3. 单文件源运行(JEP 330)跟 jar 冲突 —— java MyApp.java 跑的是单文件源模式,不能module-info.java不能用 module path;想用 module 必须先 javac --module-source-path 编译。这是「快速测试」用,不能替代正式编译流程。另外,#! shebang 只能在 Linux/macOS 用,Windows 上需要 java MyApp.java 显式调用。复现

    1
    2
    3
    
    # 错:项目里同时有 module-info.java
    $ java MyApp.java
    error: 源文件中发现了 module-info.java,使用 'java' 直接运行该源文件无效
    
  4. HTTP Client 在 JDK 11 的 TLS 协商坑 —— TLS 1.3 在 Java 11 是默认(JEP 332),但部分老服务端只支持 TLS 1.0/1.1,HTTP Client 会直接 handshake 失败。缓解

    1
    2
    3
    
    HttpClient client = HttpClient.newBuilder()
        .sslContext(SSLContext.getInstance("TLSv1.2"))  // 强制降到 TLS 1.2
        .build();
    

    真实案例:升级 Java 11 后,公司老内部系统(5 年前的 WebLogic 10.3.6)调不通——TLS 握手失败,原因就是 Java 11 客户端默认 TLS 1.3,服务端最高 TLS 1.0。

  5. Files.readString 大文件 OOM —— Files.readString(Path.of("big.csv")) 默认一次把整个文件读进 String 堆内存。1GB 文件 = 2GB 堆占用(Java 内部 char[] 占 2 字节 / 字符)。大文件仍要用 Files.newBufferedReader() 流式读

    1
    2
    3
    4
    5
    6
    7
    
    // 错:1GB 文件会 OOM
    String content = Files.readString(Path.of("big.csv"));
    
    // 对:流式读,按行处理
    try (Stream<String> lines = Files.lines(Path.of("big.csv"))) {
        lines.filter(l -> l.contains("ERROR")).forEach(System.out::println);
    }
    

§8 下一篇预告

下一站 Java 17(2021-09-14 发布)—— Records / Sealed Class / Pattern Matching / Text Blocks 全面正式,G1 正式取代 Parallel 成默认 GC。详见 Java 17 LTS 深度解读。从 Java 11 到 Java 17 又过了 3 年,这 3 年是 Java 历史上语法变化最密集的时期——密封类、记录类、模式匹配、文本块、switch 表达式等现代 Java 语法基本都在这个窗口期定型。如果你的项目从 Java 8 升到 Java 11 后稳定运行,下一步最自然的选择就是 Java 17,而不是 21。

本系列共 5 篇,本文为第 2 篇 · 查看全部
使用 Hugo 构建
主题 StackJimmy 设计