Skip to content

JDK 9~11 新特性:模块化时代来临

想象一下:你写了一个工具类,被其他项目引用。几年后,你删除了某个方法——结果导致引用方编译失败。这种「脆弱的依赖」问题,在 Java 9 之前几乎无解。

直到 Jigsaw Project 带来的模块化系统,彻底改变了 Java 应用程序的结构。


JDK 9:模块化元年

2017 年,Java 9 发布。这是自 Java 8 以来最大的一次变革。

模块化系统(Module System)

模块化是 Java 9 最重要的特性。它不仅仅是把代码组织成模块,更是重新定义了 Java 的可访问性边界。

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 就意味着「全世界都能访问」。模块化让开发者可以精确控制哪些类对外可见:

java
// 未模块化:只要是 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 支持接口私有方法:

java
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);
}

集合工厂方法

告别繁琐的集合创建方式:

java
// 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 增强

java
// 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 增强

java
// 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 改进

java
// 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 关键字

java
// 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 不是动态类型,编译后类型信息依然存在。你只是把「写类型」变成了「编译器推断类型」。

适用场景

java
// 场景 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");

不适用场景

java
// 场景 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()

java
// 创建不可变副本
List<String> original = new ArrayList<>();
original.add("a");

List<String> copy = List.copyOf(original);  // 不可变副本
// original.add("b");  // 正常
// copy.add("b");       // 运行时异常:UnsupportedOperationException

JDK 11 LTS:HTTP Client 正式登场

2018 年 9 月,JDK 11 发布。这是继 JDK 8 之后的第一个长期支持版(LTS)。

字符串增强

java
"  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。例如中文全角空格 \u3000trim() 不会去除,但 strip() 可以。

Files 增强

java
// 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);                 // 追加模式

单文件程序运行

java
// Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java 11!");
    }
}
bash
# JDK 11 之前:必须先 javac 编译,再 java 运行
javac Hello.java
java Hello

# JDK 11+:直接运行
java Hello.java

HTTP Client(正式版)

JDK 9-10 是 Incubator 版本,JDK 11 正式发布:

java
// 创建 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() 非阻塞调用
统一 APIGET/POST/PUT/DELETE 方法一致

ZGC 登场

ZGC(Z Garbage Collector)在 JDK 11 是实验阶段,JDK 15 生产可用。特点是:

  • 停顿时间不超过 10ms
  • 停顿时间不随堆大小增加而增加
  • 支持 TB 级堆内存
bash
# 启用 ZGC
java -XX:+UseZGC -Xmx64g -jar application.jar

版本对比一览

特性JDK 9JDK 10JDK 11
模块化系统
接口私有方法
集合工厂方法
Stream takeWhile/dropWhile
var 类型推断
集合 copyOf
字符串 isBlank/repeat
Files readString/writeString
HTTP Client 正式版
单文件程序运行
ZGC 实验版

面试追问方向

  1. 模块化的 requiresrequires static 有什么区别?

    • requires 是强制依赖,运行时也需要
    • requires static 是编译时依赖,运行时可选(主要用于 API 兼容性)
  2. exportsopens 有什么区别?

    • exports 编译时可见
    • opens 运行时允许反射访问(用于框架如 Spring)
  3. var 能用在 lambda 表达式上吗?为什么?

    • 不能,因为 lambda 需要目标类型(Target Type)来推断参数类型
  4. Java 9 之后,String 内部实现有什么变化?

    • JDK 9 开始使用 byte[] 而非 char[] 存储字符,节省内存

留给你的思考题

Java 9 的模块化系统带来了 requires transitive 语法,允许「传递性依赖」。

如果 module A requires module B,B requires transitive module C,那么 A 是否自动可以访问 C?

这个问题涉及到模块系统的依赖传递规则,值得深入研究。

基于 VitePress 构建