JDK 9~11 新特性:模块化时代来临
想象一下:你写了一个工具类,被其他项目引用。几年后,你删除了某个方法——结果导致引用方编译失败。这种「脆弱的依赖」问题,在 Java 9 之前几乎无解。
直到 Jigsaw Project 带来的模块化系统,彻底改变了 Java 应用程序的结构。
JDK 9:模块化元年
2017 年,Java 9 发布。这是自 Java 8 以来最大的一次变革。
模块化系统(Module System)
模块化是 Java 9 最重要的特性。它不仅仅是把代码组织成模块,更是重新定义了 Java 的可访问性边界。
// module-info.java - 模块定义文件
module com.example.myapp {
// 暴露给其他模块的包
exports com.example.myapp.api;
// 依赖其他模块
requires com.example.utils;
requires com.example.logging;
}核心概念:
| 指令 | 作用 |
|---|---|
exports | 暴露指定的包,允许其他模块访问 |
exports ... to | 暴露给指定模块(更细粒度控制) |
requires | 声明依赖的模块 |
opens | 允许运行时反射访问(opens vs exports) |
为什么重要?
传统 JAR 包没有访问控制边界。public 就意味着「全世界都能访问」。模块化让开发者可以精确控制哪些类对外可见:
// 未模块化:只要是 public,就能被任何人访问
public class InternalService {} // 你本不想暴露,但没办法
// 模块化后:明确声明导出哪些包
module com.example.myapp {
exports com.example.myapp.public.api; // 只暴露这个包
// com.example.myapp.internal 下的类,对外部模块不可见
}JAR 碎片化问题:
Java 9 之前,同一个 JAR 文件的不同版本可能同时存在导致冲突。模块系统通过强制的模块依赖声明,让这类问题在编译期就能发现,而不是等到运行时。
接口私有方法
JDK 8 引入了 default 方法,但两个 default 方法如果有重复代码,只能 copy-paste。JDK 9 支持接口私有方法:
public interface OrderService {
default void create(Order order) {
// 校验逻辑
validate(order);
// 保存逻辑
save(order);
}
default void update(Order order) {
// 校验逻辑 - 同样的代码
validate(order);
// 更新逻辑
update(order);
}
// JDK 9:私有方法,消除重复代码
private void validate(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order cannot be null");
}
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
void save(Order order);
void update(Order order);
}集合工厂方法
告别繁琐的集合创建方式:
// JDK 8 及之前
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
// JDK 9+
List<String> list = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);注意事项:
- 返回的集合是不可变的(Immutable)
- 不能添加、删除或修改元素
- 元素不能为
null - 如果元素重复,
Set.of()和Map.of()会抛IllegalArgumentException
Stream 增强
// takeWhile() - 遇到不满足条件的元素就停止
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 1, 2);
List<Integer> result = numbers.stream()
.takeWhile(n -> n < 4) // [1, 2, 3] - 遇到 4 就停止
.toList();
// dropWhile() - 跳过满足条件的元素
List<Integer> result2 = numbers.stream()
.dropWhile(n -> n < 4) // [4, 5, 1, 2] - 跳过前几个直到遇到不满足的
.toList();
// ofNullable() - 避免 NullPointerException
List<String> names = List.of("Alice", null, "Bob");
// 不要这样用!应该用 Optional 或 flatMap
Stream<String> stream = Stream.ofNullable(null);Optional 增强
// stream() - 把 Optional 转成 Stream
Optional<String> opt = Optional.ofNullable(getName());
List<String> list = opt.stream()
.map(String::toUpperCase)
.toList();
// or() - 如果为空,提供一个备选的 Optional
Optional<String> result = opt.or(() -> Optional.of("default"));
// ifPresentOrElse() - 有值执行一个操作,为空执行另一个
opt.ifPresentOrElse(
name -> System.out.println("Hello, " + name),
() -> System.out.println("Hello, Guest")
);Process API 改进
// JDK 9 之前,获取当前进程 PID 很麻烦
// JDK 9+
long pid = ProcessHandle.current().pid();
String name = ProcessHandle.current().info().commandName();
// 列出所有进程
ProcessHandle.allProcesses()
.filter(p -> p.info().commandName().orElse("").contains("java"))
.forEach(p -> System.out.println(p.pid() + ": " + p.info().commandName()));JDK 10:局部变量类型推断
2018 年 3 月,JDK 10 发布。虽然改动不大,但 var 关键字彻底改变了 Java 的书写方式。
var 关键字
// JDK 8 及之前
String name = "Alice";
List<String> list = new ArrayList<>();
Map<String, List<Integer>> map = new HashMap<>();
// JDK 10+:类型推断
var name = "Alice";
var list = new ArrayList<String>();
var map = new HashMap<String, List<Integer>>();不是什么魔法:
var 不是动态类型,编译后类型信息依然存在。你只是把「写类型」变成了「编译器推断类型」。
适用场景:
// 场景 1:长长的泛型类型
var map = new HashMap<String, List<CompletableFuture<Response>>>();
// 比写两遍泛型清晰多了
// 场景 2:链式调用返回值
var result = getStream()
.filter(x -> x > 0)
.map(String::valueOf)
.collect(Collectors.joining(","));
// 场景 3:局部变量,类型显而易见
var names = List.of("Alice", "Bob", "Carol");不适用场景:
// 场景 1:没有初始化的变量
var x; // 编译错误:cannot use 'var' without initializer
// 场景 2:类的成员变量
class User {
var name = "default"; // 编译错误:var cannot be used for field
}
// 场景 3:lambda 表达式需要明确目标类型
var func = (String s) -> s.length(); // 编译错误
Function<String, Integer> func = s -> s.length(); // 正确集合 copyOf()
// 创建不可变副本
List<String> original = new ArrayList<>();
original.add("a");
List<String> copy = List.copyOf(original); // 不可变副本
// original.add("b"); // 正常
// copy.add("b"); // 运行时异常:UnsupportedOperationExceptionJDK 11 LTS:HTTP Client 正式登场
2018 年 9 月,JDK 11 发布。这是继 JDK 8 之后的第一个长期支持版(LTS)。
字符串增强
" Hello ".isBlank(); // true - 空白字符
" Hello ".strip(); // "Hello" - 去除首尾空白
" Hello ".stripLeading(); // "Hello " - 只去开头
" Hello ".stripTrailing();// " Hello" - 只去结尾
"Hello\nWorld\n".lines(); // Stream<String> - 按行分割
"Hi".repeat(3); // "HiHiHi" - 重复字符串strip() vs trim():
strip() 能识别 Unicode 空白字符,而 trim() 只处理 ASCII。例如中文全角空格 \u3000,trim() 不会去除,但 strip() 可以。
Files 增强
// JDK 11 之前
String content = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
// JDK 11+
String content = Files.readString(path); // 读取整个文件
Files.writeString(path, "Hello"); // 写入字符串
Files.writeString(path, "Hello",
StandardOpenOption.APPEND); // 追加模式单文件程序运行
// Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, Java 11!");
}
}# JDK 11 之前:必须先 javac 编译,再 java 运行
javac Hello.java
java Hello
# JDK 11+:直接运行
java Hello.javaHTTP Client(正式版)
JDK 9-10 是 Incubator 版本,JDK 11 正式发布:
// 创建 HTTP Client
HttpClient client = HttpClient.newHttpClient();
// 同步 GET 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.GET()
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
// 异步请求
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println);核心优势:
| 特性 | 说明 |
|---|---|
| HTTP/2 支持 | 默认启用,支持多路复用 |
| WebSocket | 内置支持,无需第三方库 |
| 异步编程 | 支持 sendAsync() 非阻塞调用 |
| 统一 API | GET/POST/PUT/DELETE 方法一致 |
ZGC 登场
ZGC(Z Garbage Collector)在 JDK 11 是实验阶段,JDK 15 生产可用。特点是:
- 停顿时间不超过 10ms
- 停顿时间不随堆大小增加而增加
- 支持 TB 级堆内存
# 启用 ZGC
java -XX:+UseZGC -Xmx64g -jar application.jar版本对比一览
| 特性 | JDK 9 | JDK 10 | JDK 11 |
|---|---|---|---|
| 模块化系统 | ✅ | ||
| 接口私有方法 | ✅ | ||
| 集合工厂方法 | ✅ | ||
| Stream takeWhile/dropWhile | ✅ | ||
| var 类型推断 | ✅ | ||
| 集合 copyOf | ✅ | ||
| 字符串 isBlank/repeat | ✅ | ||
| Files readString/writeString | ✅ | ||
| HTTP Client 正式版 | ✅ | ||
| 单文件程序运行 | ✅ | ||
| ZGC 实验版 | ✅ |
面试追问方向
模块化的
requires和requires static有什么区别?requires是强制依赖,运行时也需要requires static是编译时依赖,运行时可选(主要用于 API 兼容性)
exports和opens有什么区别?exports编译时可见opens运行时允许反射访问(用于框架如 Spring)
var能用在 lambda 表达式上吗?为什么?- 不能,因为 lambda 需要目标类型(Target Type)来推断参数类型
Java 9 之后,String 内部实现有什么变化?
- JDK 9 开始使用
byte[]而非char[]存储字符,节省内存
- JDK 9 开始使用
留给你的思考题
Java 9 的模块化系统带来了 requires transitive 语法,允许「传递性依赖」。
如果 module A requires module B,B requires transitive module C,那么 A 是否自动可以访问 C?
这个问题涉及到模块系统的依赖传递规则,值得深入研究。
