Java新特性(二):Java 9 ~ 11
Java新特性(二):Java 9 ~ 11
导语:Java 9 开启了「每半年一版」的快节奏,也带来了争议最大的模块系统(JPMS);Java 10 只做了一件事但影响深远——
var类型推断;Java 11 是 8 之后的第一个 LTS,标准 HTTP Client、String 增强和单文件运行是面试最常问的三点。本篇按版本梳理 Java 9 ~ 11 的代表性特性、必问点和代码示例。
一、Java 9:模块化与 API 补课
Java 9 共有 90+ 个 JEP,是 Java 历史上改动最大的版本之一(模块化改造了整个 JDK 结构)。面试真正会问的只有以下几个。
1. 什么是模块系统(JPMS)?它解决什么问题?(高频)
答: JPMS(Java Platform Module System,JEP 261)把「模块」作为 Java 的一等公民,核心目标是:
- 可靠的配置:模块必须显式声明依赖,启动时即可发现「缺包 / 循环依赖」,而不是等到运行时抛
NoClassDefFoundError; - 强封装:模块可以只导出想曝露的包(
exports),未导出的包即使 public 也无法被外部访问,解决了「类路径(classpath)上一切都是公开的」这一历史问题; - 可裁剪的运行时:配合
jlink可生成只含所需模块的精简 JRE,镜像体积从 200MB+ 降到几十 MB。
模块声明文件 module-info.java:
// src/main/java/module-info.java
module com.example.order {
requires java.base; // java.base 隐式依赖,无需书写
requires transitive java.sql; // transitive:依赖我的模块也能看到 java.sql
requires java.logging;
exports com.example.order.api; // 只导出 api 包,internal 包外部不可见
// opens:允许反射访问(Spring/MyBatis 等框架依赖反射)
opens com.example.order.entity to spring.core;
uses com.example.order.spi.PayService; // 服务消费者
provides com.example.order.spi.PayService // 服务提供者(SPI)
with com.example.order.impl.AliPayService;
}编译与运行(模块路径替代类路径):
javac -d out --module-source-path src $(find src -name "*.java")
java --module-path out --module com.example.order/com.example.order.Main
# 生成精简运行时镜像(只含所需模块)
jlink --module-path out --add-modules com.example.order --output myjre面试追问:模块化在真实项目落地了吗?
- 落地面不大:主流业务依旧跑在类路径上。原因是 Spring Boot、老依赖(自动模块名不稳定、拆包问题 split package)与 JPMS 冲突较多,迁移成本高。
- 但它的副产品被广泛使用:JDK 自身模块化后,出现了「强封装 JDK 内部 API」(JDK 16 默认 deny),
sun.misc.*、com.sun.*的使用越来越受限制;jlink也让容器镜像可以瘦身。
易错点:JPMS 是编译期 + 启动期的模块化,不是运行时动态模块(那是 OSGi 的领域);JPMS 也不负责版本管理,模块没有版本号。
2. List.of() 和 Arrays.asList() 有什么区别?(高频)
答:
// Arrays.asList:固定长度,但允许 set 修改元素,允许 null
List<String> a = Arrays.asList("a", "b");
a.set(0, "x"); // ✅ 可以
a.add("c"); // ❌ UnsupportedOperationException(长度固定)
a.contains(null); // ✅ 允许 null
// List.of:真正的不可变集合,禁止修改,禁止 null
List<String> b = List.of("a", "b");
b.set(0, "x"); // ❌ UnsupportedOperationException
List.of((String) null); // ❌ NullPointerException| 维度 | Arrays.asList | List.of / Set.of / Map.of |
|---|---|---|
| 返回类型 | java.util.Arrays$ArrayList(内部类) | ImmutableCollections 的私有实现 |
| 是否可变 | 长度不可变,元素可 set | 完全不可变 |
| 是否允许 null | 允许 | 不允许(NPE) |
| 重复元素 | 允许 | Set.of/Map.of 遇重复抛 IllegalArgumentException |
| 与源数组关系 | 与之共享(视图) | 独立拷贝 |
// Set.of 去重校验:重复元素直接抛异常,而不是默默去重
Set<String> s = Set.of("a", "a"); // ❌ IllegalArgumentException: duplicate element: a
// Map 写法
Map<String, Integer> m1 = Map.of("a", 1, "b", 2); // 最多 10 对
Map<String, Integer> m2 = Map.ofEntries( // 超过 10 对用 entries
Map.entry("a", 1),
Map.entry("b", 2));补充:不可变集合天然线程安全且可被安全共享(常用于常量表),但要修改时得
new ArrayList<>(List.of(...))。
3. 接口可以写私有方法了(JEP 213)
答: Java 9 允许接口中定义 private(含 private static)方法,用于抽取多个 default 方法的公共逻辑,又不把这个方法暴露给实现类。
public interface OrderValidator {
default boolean valid(String order) {
return notBlank(order) && order.length() < 64;
}
default boolean validStrict(String order) {
return notBlank(order) && order.matches("[A-Z0-9]+");
}
// 只服务于接口内部,实现类看不到也调不到
private boolean notBlank(String s) {
return s != null && !s.trim().isEmpty();
}
}4. Stream / Optional 的增强
答:
// ---------- Stream(JDK 9 新增) ----------
// takeWhile:从开头取,直到条件不满足(有序流,短路)
Stream.of(1, 2, 3, 5, 1).takeWhile(i -> i < 4).forEach(System.out::print); // 123
// dropWhile:从开头丢,直到条件不满足,剩余保留
Stream.of(1, 2, 3, 5, 1).dropWhile(i -> i < 4).forEach(System.out::print); // 51
// ofNullable:允许单个 null 元素,空则返回空流(常用于「可能为 null 的单个对象」)
Stream.ofNullable(null).count(); // 0
// iterate 增加「终止条件」重载(JDK 8 的 iterate 是无限流,必须配 limit)
Stream.iterate(1, i -> i < 100, i -> i * 2).forEach(System.out::print); // 1248163264
// 注意区别:filter 会遍历全部元素,takeWhile 遇到第一个不满足即停止// ---------- Optional(JDK 9 新增) ----------
Optional<String> opt = Optional.of("java");
// ifPresentOrElse:有值走第一个分支,无值走第二个(替代 isPresent/if-else)
opt.ifPresentOrElse(v -> System.out.println("值:" + v),
() -> System.out.println("空"));
// or:为空时返回另一个 Optional(比 orElseGet 更适合链式)
Optional<String> result = opt.or(() -> Optional.of("default"));
// stream:把 Optional 转成 0 或 1 个元素的流,便于扁平化
List<String> list = Stream.of(Optional.of("a"), Optional.<String>empty())
.flatMap(Optional::stream)
.collect(Collectors.toList()); // [a]5. 其他常用小改进
答:
- try-with-resources 支持 effectively final 变量(JEP 213):
// JDK 7:必须在 try 的括号里声明资源
try (InputStream in = new FileInputStream("a.txt")) { }
// JDK 9:可以直接引用已声明且 effectively final 的资源
InputStream in = new FileInputStream("a.txt");
try (in) { } // 少一层嵌套@Deprecated增强(JEP 277):新增since(从哪版开始弃用)与forRemoval(是否计划删除),让 IDE 能区分「不推荐」和「即将移除」。CompletableFuture增强(JEP 266):新增orTimeout、completeOnTimeout、delayedExecutor、copy等,同时引入Flow响应式编程接口。VarHandle(JEP 193):标准化的变量句柄,提供类似Unsafe的原子/有序/volatile 字段访问能力,是「摆脱sun.misc.Unsafe」的第一步。
class Counter {
private volatile int value;
private static final VarHandle VALUE;
static {
try {
VALUE = MethodHandles.lookup().findVarHandle(Counter.class, "value", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
void increment() { VALUE.getAndAdd(this, 1); } // 原子自增,替代 Unsafe
int get() { return (int) VALUE.getVolatile(this); }
}- G1 成为默认垃圾收集器(JEP 248):JDK 8 默认 Parallel Scavenge + Parallel Old,JDK 9 起默认 G1,标志着「低延迟优先」的取向。
- Compact Strings(JEP 254):
String内部由char[]改为byte[] + coder,纯 ASCII(Latin-1)字符串内存占用直接减半。 - 多版本 JAR(MR-JAR,JEP 238):一个 jar 里可以为不同 JDK 版本放不同实现,
META-INF/versions/9/...;JDK 自带的多版本 jar 就是靠它兼容旧版本。 - 统一 JVM 日志(JEP 158 / 271):
-Xlog一套参数统一所有 GC 与运行时日志,取代过去五花八门的-XX:+PrintGCDetails。 - JShell(JEP 222):交互式 REPL,
jshell命令直接试代码,学习与验证片段非常方便。 - Process API 增强(JEP 102):
ProcessHandle可以查询进程 PID、父进程、启动时间、命令行参数,并支持监听进程退出。
二、Java 10:一个 var 撑起一个版本
6. var 是动态类型吗?有哪些限制?(高频)
答: 不是。 var 是局部变量类型推断(JEP 286),不是动态类型:变量在编译期就确定了具体类型(字节码中仍是 String、ArrayList),运行时没有任何变化,也不影响性能。
var name = "java"; // 推断为 String
var list = new ArrayList<String>();// 推断为 ArrayList<String>,注意泛型由右侧决定
var map = new HashMap<String, List<Integer>>();
// 它只是「省略了显而易见的类型」,编译后完全等价于显式声明限制(考点密集):
var x; // ❌ 没有初始值无法推断
var y = null; // ❌ 不能初始化为 null
var z = 1; z = "str"; // ❌ 类型已确定为 int,不能再赋 String
class A {
var field = 1; // ❌ 不能用于成员变量
void m(var p) { } // ❌ 不能用于方法参数
var m2() { return 1; } // ❌ 不能用于返回值
}
var lambda = (String s) -> s.length(); // ❌ 不能直接推断 Lambda(需函数式接口上下文)
var arr = {1, 2, 3}; // ❌ 数组初始化简写不行,需 var arr = new int[]{1,2,3};
var ok = (Runnable) () -> System.out.println("hi"); // ✅ 有目标类型时可推断
for (var i = 0; i < 10; i++) { } // ✅ for 循环变量可以
try (var in = new FileInputStream("a.txt")) { } // ✅ try-with-resources 可以使用建议:只在「右侧已经写明类型、左侧再写一遍是冗余」时用(如 var list = new ArrayList<String>());方法返回值、字段一律显式声明类型,可读性优先。
7. Java 10 还有哪些值得提的?
答:
- 集合新增
copyOf:List.copyOf/Set.copyOf/Map.copyOf返回不可变拷贝(源集合改动不影响新集合)。 - G1 并行 Full GC(JEP 307):Full GC 由单线程改为并行,
-XX:ParallelGCThreads可调,降低极端情况下的停顿。 - 应用类数据共享 AppCDS(JEP 310):把应用类也纳入 CDS 归档,加速启动、降低内存;它是后面 Java 24/26 AOT 缓存的前身。
- 线程局部握手(JEP 312):可以在单个线程的安全点执行回调,减少全局安全点带来的停顿,为后续低延迟 GC 打基础。
三、Java 11:第二个 LTS,实用主义
8. 标准 HTTP Client 有什么特点?(高频)
答: java.net.http.HttpClient(JEP 321)取代了又老又难用的 HttpURLConnection,特点:
- 支持 HTTP/2(默认协商)、HTTP/1.1,以及 WebSocket;
- 同步 + 异步双 API:
send阻塞、sendAsync返回CompletableFuture; - 内置连接池、重定向策略、超时、代理、认证、Cookie 管理;
- 请求/响应体用
BodyPublishers/BodySubscribers描述,扩展性好。
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
public class HttpClientDemo {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// ---------- 同步 GET ----------
HttpRequest get = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.timeout(Duration.ofSeconds(10))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> resp = client.send(get, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.statusCode() + " -> " + resp.body());
// ---------- 异步 POST(JSON) ----------
HttpRequest post = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/post"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"java\"}"))
.build();
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(post, HttpResponse.BodyHandlers.ofString());
future.thenApply(HttpResponse::body).thenAccept(System.out::println).join();
}
}面试可延伸:Java 26(JEP 517)为它加上了 HTTP/3(QUIC) 支持,只需
HttpClient.Version.HTTP_3,且能自动向下协商降级。
9. Java 11 的 String / Files 增强(写代码很常用)
答:
String s = " Java 11 ";
s.isBlank(); // false:是否只由空白组成(注意 "" 也算 blank)
"".isBlank(); // true
s.strip(); // "Java 11":去除首尾空白(支持 Unicode 空白,trim 只认 <= U+0020)
s.stripLeading(); // 去头部
s.stripTrailing(); // 去尾部
"ab\ncd".lines().count(); // 2:按行切分成 Stream<String>
"ab".repeat(3); // "ababab"
// Files 读写(告别 BufferedReader/FileWriter 模板代码)
String content = Files.readString(Path.of("a.txt"));
Files.writeString(Path.of("b.txt"), "hello");
trim()与strip()的区别是经典追问:trim()只去除码点 ≤ 32 的字符(老式空白),strip()基于Character.isWhitespace,能处理全角空格等 Unicode 空白。
10. 单文件源码运行:java Hello.java
答: JEP 330 允许直接运行单个 .java 源文件,无需先 javac:
# 以前:javac Hello.java && java Hello
java Hello.java # 现在一行搞定// Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, " + (args.length > 0 ? args[0] : "world"));
}
}它让 Java 在「写小脚本」场景第一次具备了可用性。Java 22 进一步支持多文件源码启动(JEP 458):java Main.java 会自动编译同目录下被引用的其它源文件。
11. var 也能用在 Lambda 参数上(JEP 323)
答: Java 11 允许 Lambda 参数使用 var,主要目的是配合注解使用(因为注解需要类型):
// 语法上等价于 (a, b) -> a + b,但可以加注解了
list.stream()
.map((@NotNull var s) -> s.toUpperCase())
.collect(Collectors.toList());注意:要么全部用 var 且带类型推断,要么全部省略类型,不能混用((var a, String b) 非法)。
12. Java 11 的 GC 与运行时变化
答:
- ZGC 实验性引入(JEP 333):目标是把停顿控制在 10ms 以内且与堆大小无关,支持 TB 级堆;采用染色指针 + 读屏障,并发标记整理。JDK 15 转正,JDK 21 引入分代 ZGC,JDK 23 起默认即分代模式。
- Epsilon GC(JEP 318):一个「只分配、不回收」的 No-Op GC,用于性能测试、短命进程(用完即退)等场景,启用
-XX:+UseEpsilonGC。 - Flight Recorder 开源(JEP 328):JFR 从商业特性变为开源,成为生产环境低开销诊断的标准工具(覆盖 CPU、内存、GC、锁、I/O 等事件)。
- Nest-Based Access Control(JEP 181):同 nest(同一顶层类及其嵌套类)内可直接访问彼此的私有成员,编译器不必再生成
access$000桥接方法(这就是为什么 JDK 11 编译的嵌套类反射getNestMembers()能直接工作)。 - 移除 Java EE 与 CORBA 模块(JEP 320):
java.xml.ws、java.corba、java.activation等被删除,需要自行引入 Jakarta 对应实现——这也解释了后面javax.*→jakarta.*的迁移故事(注意这是 Jakarta EE 生态的改名,不是 JDK 17 改的)。
顺带一提,Java 11 还弃用了 Nashorn JS 引擎(JEP 335,JDK 15 正式移除)。
四、小结
| 版本 | 必须记住的三个点 |
|---|---|
| Java 9 | 模块系统 JPMS、List.of 不可变集合、Stream takeWhile/dropWhile + 接口私有方法 |
| Java 10 | var(编译期类型推断,不是动态类型)、G1 并行 Full GC、集合 copyOf |
| Java 11 | HTTP Client、String 增强(isBlank/strip/lines/repeat)、单文件源码运行;ZGC / Epsilon / JFR 开源 |
