Skip to content

Java 9-21 新特性与 Record ​

S1-030建议学进阶约 90 分钟JDK 21编程语言基础 · Java 版本特性

1. 本节定位 ​

从 Java 8 到 Java 21,语言发生了不小的变化:Record 可以一行替代几十行样板代码,switch 变成了表达式,虚拟线程让"一个任务一个线程"重新变得可行。

本节是 S1 的最后一节,也是你以后写的代码长什么样的预览。重点不是背特性清单,而是知道哪些特性值得马上用、哪些要谨慎用。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出 Java 9-21 中最重要的几个特性及其版本;
  • 用 Record 定义不可变数据载体,并用紧凑构造器做校验;
  • 说清 Record 的适用与不适用场景;
  • 用文本块写多行字符串(SQL、JSON);
  • 用 switch 表达式与模式匹配替代冗长的 if-else 链;
  • 用 sealed + 模式匹配让编译器帮你检查"分支是否漏了";
  • 说出虚拟线程解决什么问题,以及它与线程池的区别。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

看两个版本的对比。先说"一个不可变数据类":

java
// Java 8 时代:手写 40 行
public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) { this.x = x; this.y = y; }
    public int getX() { return x; }
    public int getY() { return y; }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point)) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override public int hashCode() { return Objects.hash(x, y); }

    @Override public String toString() { return "Point{x=" + x + ", y=" + y + "}"; }
}
java
// Java 16+:一行
public record Point(int x, int y) { }

这不是"少打字",而是消除一整类 bug:手写的 equals / hashCode 极其容易写错(尤其是漏了字段或忘了改),而 Record 由编译器保证正确。

再看并发:虚拟线程让"每个请求一个线程"这种简单直白的写法重新变得可行——第 7 节的实测显示,同样 10000 个任务,虚拟线程比 200 个平台线程的线程池快约 10 倍。

5. 核心概念 ​

5.1 时间线:哪些版本值得记住 ​

版本类型值得记住的
Java 8LTSLambda、Stream、Optional、新日期时间 API(《Java 8 核心特性总览》)
Java 9模块系统、List.of()、接口私有方法
Java 10var 局部变量类型推断
Java 11LTSString 新方法(isBlank、strip、repeat)、HttpClient
Java 14switch 表达式(正式)
Java 15文本块(正式)
Java 16Record、instanceof 模式匹配
Java 17LTSsealed 类、Spring Boot 3 的最低要求
Java 21LTS虚拟线程、switch 模式匹配、SequencedCollection

学习建议:以 Java 21 为基线(本项目就是),知道 Java 8 / 17 的差异即可——面试会问,存量项目会遇到。

5.2 Record:不可变数据载体 ​

java
public record Point(int x, int y) { }

编译器自动生成:构造方法、x() / y() accessor、equals、hashCode、toString。

四条规则:

  1. 字段隐式 private final——不可变;
  2. 自动继承 java.lang.Record,不能继承别的类(可以实现接口);
  3. accessor 名字是 x() 而不是 getX();
  4. 可以加方法、静态方法、紧凑构造器。

紧凑构造器(做校验,不重复写赋值):

java
record PositivePoint(int x, int y) {
    PositivePoint {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("坐标不能为负: " + x + "," + y);
        }
    }
}

注意写法:没有参数列表、没有 this.x = x——赋值由编译器在最后自动完成。

什么时候用 Record:

适合例子
DTO / 值对象接口请求响应、配置项
多返回值方法要返回"结果 + 错误信息"
模式匹配的载体与 sealed 配合做代数数据类型
Map 的 key / 集合元素需要正确的 equals / hashCode

什么时候不用:

不适合原因
JPA 实体需要无参构造 + 可变字段 + 代理
需要继承的类Record 无法被继承(final)
字段会被修改的对象不可变是硬约束
字段很多(10+)构造方法参数太长,可读性差(考虑建造者模式)

5.3 文本块:多行字符串 ​

java
String sql = """
        SELECT id, name
        FROM users
        WHERE status = 'ACTIVE'
        ORDER BY id""";

三个要点:

  1. 用 """ 开始与结束;
  2. 结束分隔符的位置决定缩进基准——把 """ 放在最左,内容就没多余缩进;
  3. 换行是 \n,不要在行尾加 \(那是取消换行)。

最适合:SQL、JSON、HTML 片段、测试数据。别用它写"要拼接变量的长字符串"——那个场景用 formatted() 或模板引擎更合适。

5.4 switch 表达式 ​

java
String type = switch (day) {
    case "SAT", "SUN" -> "周末";
    case "MON", "TUE", "WED", "THU", "FRI" -> "工作日";
    default -> "未知";
};

与旧 switch 的三个差别:

旧 switch 语句新 switch 表达式
返回值不能直接返回是表达式,有值
穿透默认穿透,要写 break-> 语法不穿透
穷尽检查无对枚举/sealed 编译期检查

代码块要返回值时用 yield:

java
String grade = switch (score / 10) {
    case 10, 9 -> "优秀";
    case 8 -> "良好";
    default -> {
        String note = "需要补考";
        yield "不及格(" + note + ")";       // yield 而不是 return
    }
};

return 会直接退出整个方法,yield 只是"这个分支的值"——混用会导致意外行为。

5.5 instanceof 模式匹配 ​

java
// 旧写法:判断 + 强转 + 声明变量,三步
if (value instanceof String) {
    String text = (String) value;
    System.out.println(text.toUpperCase());
}

// 新写法:一步
if (value instanceof String text && text.length() > 2) {
    System.out.println(text.toUpperCase());
}

text 的作用域只在判断为真的分支内——编译器帮你避免"用了没转型成功的变量"。

5.6 sealed 类:明确的继承边界 ​

java
sealed interface Shape permits Circle, Rectangle, Triangle { }

record Circle(double radius) implements Shape { }
record Rectangle(double width, double height) implements Shape { }
record Triangle(double base, double height) implements Shape { }

sealed 声明"只允许这几个类实现我"——这带来了一个关键能力:编译器能检查 switch 是否穷尽了所有可能。

java
static double area(Shape shape) {
    return switch (shape) {                     // 不需要 default!
        case Circle circle -> Math.PI * circle.radius() * circle.radius();
        case Rectangle rectangle -> rectangle.width() * rectangle.height();
        case Triangle triangle -> triangle.base() * triangle.height() / 2;
    };
}

对比旧写法的差别很大:以前用接口 + 多实现时,switch 必须写 default(否则编译不过),于是"新增一种实现却忘了改某个 switch"这个 bug 只能等到运行时才发现。sealed 把它变成了编译错误。

守卫条件(when)可以在模式上再筛一层:

java
static String describe(Shape shape) {
    return switch (shape) {
        case Circle circle when circle.radius() > 10 -> "大圆";
        case Circle circle -> "圆(半径 " + circle.radius() + ")";
        case Rectangle rectangle when rectangle.width() == rectangle.height() -> "正方形";
        case Rectangle rectangle -> "矩形";
        case Triangle triangle -> "三角形";
    };
}

注意分支顺序:when 分支必须写在同类模式的普通分支之前,否则永远是普通分支命中。

5.7 var:局部变量类型推断 ​

java
var names = List.of("a", "b");              // 推断为 List<String>
var cache = new HashMap<String, Integer>(); // 右侧写了泛型,左侧才能省

四条限制:

  1. 只能用于局部变量(含 for 循环变量、try-with-resources);
  2. 必须有初始值(否则无从推断);
  3. 不能用于字段、方法参数、方法返回值;
  4. 不能声明为 null(var x = null; 编译错误)。

该不该用:判断标准是**"右侧是否已经说清了类型"**。

java
var user = new User();                                  // 好:一眼知道是 User
var list = new ArrayList<User>();                       // 好
var result = service.process(order, config);            // 差:看不出返回什么类型

var 不是"动态类型",它仍然是静态类型,只是让编译器去推断。不要为了少打字而牺牲可读性。

5.8 虚拟线程:Java 21 最重要的变化 ​

问题:平台线程(传统线程)一个约 1 MB 栈,创建 1 万个就吃 10 GB 内存。所以只能用"少量线程 + 线程池",代码被写成异步回调或复杂的响应式风格。

虚拟线程:由 JVM 调度,一个只要几 KB,可以创建几十万个。

java
// 平台线程池:200 个线程处理 1 万个任务,任务排队等待
try (ExecutorService pool = Executors.newFixedThreadPool(200)) {
    for (int i = 0; i < 10_000; i++) {
        pool.submit(task);
    }
}

// 虚拟线程:每个任务一个线程,几乎立刻都能跑起来
try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        pool.submit(task);
    }
}

关键区别:

平台线程虚拟线程
内存约 1 MB / 个约几 KB / 个
数量级几百到几千几十万
适合CPU 密集IO 密集(等待)
要不要池化要(贵)不要(便宜,池化反而有害)

为什么快:虚拟线程阻塞在 IO 时,JVM 会把底层的平台线程让出来去跑其他虚拟线程——所以"阻塞"不再浪费资源。代码写法完全不变(还是那个简单的 Thread.sleep / JDBC 调用),这才是它最大的价值。

使用的三条注意:

  1. 不要池化虚拟线程(newVirtualThreadPerTaskExecutor() 就是标准用法);
  2. CPU 密集型任务没有收益(没有等待可以让出);
  3. 与 synchronized 结合时可能出现"固定"(pinning)——Java 21 中应优先用 ReentrantLock(S4 并发章节详述)。

6. 递进示例 ​

6.1 最小示例:Record ​

java
record Point(int x, int y) { }

Point point = new Point(3, 4);
point.x();                     // 3
point.equals(new Point(3, 4)); // true(自动生成)

6.2 业务示例:用 sealed + Record 表达"消息类型" ​

java
sealed interface Message permits TextMessage, ImageMessage { }
record TextMessage(String content) implements Message { }
record ImageMessage(String url, int width, int height) implements Message { }

static String preview(Message message) {
    return switch (message) {
        case TextMessage text -> text.content().substring(0, Math.min(10, text.content().length()));
        case ImageMessage image -> "[图片 " + image.width() + "x" + image.height() + "]";
    };
}

这种写法把"可能的消息类型"变成了编译期可检查的封闭集合——新增一种消息,所有相关的 switch 都会编译失败,逼你去处理它。

6.3 反例:滥用新特性 ​

java
// 反例一:var 用在了看不出类型的地方
var data = repository.load(id);          // data 到底是什么?

// 反例二:Record 当 JPA 实体
@Entity
public record User(@Id Long id, String name) { }    // 不行:需要无参构造与可变字段

// 反例三:给 CPU 密集任务上虚拟线程
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
    // 纯计算任务:没有 IO 等待,虚拟线程不会更快
}

7. 完整可运行代码 ​

文件:labs/version-features/src/main/java/cn/kb/learn/version/ModernJavaLesson.java(节选核心部分)

java
package cn.kb.learn.version;

import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Stream;

public class ModernJavaLesson {
    /** Record:一行拿到构造器、accessor、equals、hashCode、toString */
    record Point(int x, int y) {
    }

    /** 紧凑构造器:在赋值前做校验 */
    record PositivePoint(int x, int y) {
        PositivePoint {
            if (x < 0 || y < 0) {
                throw new IllegalArgumentException("坐标不能为负: " + x + "," + y);
            }
        }
    }

    /** sealed 接口:明确列出允许的实现类 */
    sealed interface Shape permits Circle, Rectangle, Triangle {
    }

    record Circle(double radius) implements Shape {
    }

    record Rectangle(double width, double height) implements Shape {
    }

    record Triangle(double base, double height) implements Shape {
    }

    /** switch 模式匹配:sealed 让编译器能检查是否穷尽 */
    static double area(Shape shape) {
        return switch (shape) {
            case Circle circle -> Math.PI * circle.radius() * circle.radius();
            case Rectangle rectangle -> rectangle.width() * rectangle.height();
            case Triangle triangle -> triangle.base() * triangle.height() / 2;
        };
    }

    /** 带守卫条件的模式匹配 */
    static String describe(Shape shape) {
        return switch (shape) {
            case Circle circle when circle.radius() > 10 -> "大圆";
            case Circle circle -> "圆(半径 " + circle.radius() + ")";
            case Rectangle rectangle when rectangle.width() == rectangle.height() -> "正方形";
            case Rectangle rectangle -> "矩形";
            case Triangle triangle -> "三角形";
        };
    }

    /** 虚拟线程:同样的任务,两种线程模型的对比 */
    static void virtualThreadDemo() throws InterruptedException {
        int tasks = 10_000;

        long start = System.currentTimeMillis();
        try (ExecutorService pool = Executors.newFixedThreadPool(200)) {
            for (int i = 0; i < tasks; i++) {
                pool.submit(() -> {
                    Thread.sleep(10);
                    return null;
                });
            }
        }
        long platformCost = System.currentTimeMillis() - start;

        start = System.currentTimeMillis();
        try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < tasks; i++) {
                pool.submit(() -> {
                    Thread.sleep(10);
                    return null;
                });
            }
        }
        long virtualCost = System.currentTimeMillis() - start;

        System.out.println("平台线程池(200 个线程)处理 " + tasks + " 个任务: " + platformCost + " ms");
        System.out.println("虚拟线程(每任务一个线程)处理 " + tasks + " 个任务: " + virtualCost + " ms");
    }

    public static void main(String[] args) throws InterruptedException {
        // 1. Record
        Point point = new Point(3, 4);
        System.out.println("Record: " + point);
        System.out.println("自动的 accessor: " + point.x() + ", " + point.y());
        System.out.println("自动的 equals: " + point.equals(new Point(3, 4)));
        System.out.println("自动的 hashCode 一致: " + (point.hashCode() == new Point(3, 4).hashCode()));

        try {
            new PositivePoint(-1, 0);
        } catch (IllegalArgumentException exception) {
            System.out.println("紧凑构造器校验: " + exception.getMessage());
        }

        // 2. var:局部变量类型推断
        var names = List.of("a", "b");
        var cache = new HashMap<String, Integer>();
        System.out.println("var 推断: " + names.getClass().getSimpleName() + " / " + cache.getClass().getSimpleName());

        // 3. 文本块
        String sql = """
                SELECT id, name
                FROM users
                WHERE status = 'ACTIVE'
                ORDER BY id""";
        System.out.println("文本块:");
        System.out.println(sql);
        System.out.println("文本块行数: " + sql.lines().count());

        // 4. switch 表达式
        String day = "SAT";
        String type = switch (day) {
            case "SAT", "SUN" -> "周末";
            case "MON", "TUE", "WED", "THU", "FRI" -> "工作日";
            default -> "未知";
        };
        System.out.println(day + " 是 " + type);

        int score = 55;
        String grade = switch (score / 10) {
            case 10, 9 -> "优秀";
            case 8 -> "良好";
            case 7 -> "中等";
            case 6 -> "及格";
            default -> {
                String note = "需要补考";
                yield "不及格(" + note + ")";
            }
        };
        System.out.println(score + " 分: " + grade);

        // 5. instanceof 模式匹配
        Object value = "知识库";
        if (value instanceof String text && text.length() > 2) {
            System.out.println("模式匹配: " + text.toUpperCase() + "(长度 " + text.length() + ")");
        }

        // 6. sealed + 模式匹配:漏掉一个分支编译就报错
        List<Shape> shapes = List.of(
                new Circle(5), new Circle(20), new Rectangle(4, 4), new Triangle(6, 2));
        for (Shape shape : shapes) {
            System.out.println(describe(shape) + ",面积 " + Math.round(area(shape) * 100) / 100.0);
        }

        // 7. Stream 的补充方法
        System.out.println("Stream.toList(): " + Stream.of("x", "y").toList());

        // 8. 虚拟线程
        virtualThreadDemo();
    }
}

运行命令:

powershell
cd labs
./mvnw -pl version-features compile
java -cp version-features/target/classes cn.kb.learn.version.ModernJavaLesson

预期输出(虚拟线程两行的毫秒数依机器而变,请关注两者的数量级差异):

text
Record: Point[x=3, y=4]
自动的 accessor: 3, 4
自动的 equals: true
自动的 hashCode 一致: true
紧凑构造器校验: 坐标不能为负: -1,0
var 推断: List12 / HashMap
文本块:
SELECT id, name
FROM users
WHERE status = 'ACTIVE'
ORDER BY id
文本块行数: 4
SAT 是 周末
55 分: 不及格(需要补考)
模式匹配: 知识库(长度 3)
圆(半径 5.0),面积 78.54
大圆,面积 1256.64
正方形,面积 16.0
三角形,面积 6.0
Stream.toList(): [x, y]
平台线程池(200 个线程)处理 10000 个任务: 580 ms
虚拟线程(每任务一个线程)处理 10000 个任务: 60 ms

三处值得对照:

输出含义
Record: Point[x=3, y=4]自动生成的 toString,格式是 类型名[字段=值]
var 推断: List12 / HashMap编译后仍是具体类型(List12 是 List.of 的内部实现)——var 不是动态类型
580 ms vs 60 ms同样 10000 个"睡 10 毫秒"的任务,虚拟线程快约 10 倍

虚拟线程那两行怎么理解:平台线程池只有 200 个线程,10000 个任务要分批执行(每批 200 个,每批约 10 ms,共约 500 ms);虚拟线程则是一万个线程同时睡,所以总耗时约等于一次 sleep 的时间。

8. Debug 观察点 ​

  1. 在 new PositivePoint(-1, 0) 处按 F7 进入,观察紧凑构造器先执行校验、再赋值的顺序;
  2. 在 point.equals(new Point(3, 4)) 处 F7 进入,看编译器生成的是逐字段比较(不是 Objects.equals 包装);
  3. 在 area(shape) 的 switch 处断点,观察每个分支命中时的具体类型(Circle / Rectangle / Triangle),这就是"模式匹配"在运行时的样子;
  4. 在 sql.lines().count() 处断点,展开 sql 字符串,观察它是普通字符串(文本块只是编译期的书写形式,运行时没有特殊类型);
  5. 运行虚拟线程演示时,在 IDEA 的线程面板观察:虚拟线程显示为 虚拟线程@xxx,数量上万但平台线程只有几十个——这正是"多对一"的调度关系。

9. 常见错误 ​

错误 1:yield 与 return 混用 ​

java
String grade = switch (score) {
    default -> {
        return "不及格";       // 错:直接退出了整个方法
    }
};

修复:switch 表达式内部用 yield,只有确实想结束方法时才用 return。

错误 2:var 降低了可读性 ​

见 6.3 节。修复:右侧已经说明类型时用 var,否则写明确类型。

错误 3:把 Record 当 JPA 实体 ​

症状:Hibernate 报"没有默认构造方法"或"无法设置字段"。原因:实体需要无参构造 + 可变字段 + 非 final 类(用于代理)。修复:实体用普通类 + getter/setter,DTO 用 Record。

错误 4:给虚拟线程做池化 ​

java
Executors.newFixedThreadPool(200, Thread.ofVirtual().factory());    // 反模式

原因:池化的价值在于"复用昂贵资源",而虚拟线程便宜到不需要复用——池化反而限制了并发量。修复:用 newVirtualThreadPerTaskExecutor()。

错误 5:以为虚拟线程能加速 CPU 密集任务 ​

虚拟线程的收益来自IO 等待时的让出。纯计算任务没有等待可以让,调度开销反而可能让它略慢——CPU 密集任务本来就该用固定大小的平台线程池。

错误 6:switch 模式匹配的分支顺序错误 ​

java
return switch (shape) {
    case Circle circle -> "圆";                              // 永远先命中这里
    case Circle circle when circle.radius() > 10 -> "大圆";   // 永远不会执行
};

修复:把带 when 的特殊分支写在前面。这一点与 if-else 的顺序敏感性相同。

10. 动手实验 ​

目标:用现代语法重写一段传统代码。

步骤:

  1. 运行第 7 节代码,核对输出,记下虚拟线程那两行的耗时比值;
  2. 把 7 节代码里的 score = 55 改成 95、85、75、65、55 各跑一次,验证五个分支都能正确命中;
  3. 手写一个 Java 8 风格的 Point 类(40 行),再用 Record 写一遍(1 行),对比两者的 equals 与 hashCode;
  4. 给 sealed interface Shape 新增一个 record Pentagon(double side) implements Shape,先不要改 area 方法,观察编译错误信息;
  5. 把 virtualThreadDemo 里的 Thread.sleep(10) 改成一段纯计算(如累加 10 万次),再跑一次——观察虚拟线程的耗时优势是否还在。

验收方式:能解释第 4 步的编译错误为什么是"好事",以及第 5 步为什么优势消失。

扩展任务:用 sealed + Record + switch 模式匹配实现一个极简的表达式求值器(支持数字、加法、乘法三种节点),要求"漏处理任何一种节点就编译失败"。

11. 练习题 ​

基础题

  1. 判断:Record 会自动生成 setter 方法。(对 / 错)
  2. 填空:多行字符串用 ______ 语法;switch 表达式在代码块里返回值用 ______。
  3. 简答:虚拟线程适合什么类型的任务?为什么不需要池化?

代码阅读题

java
record Range(int from, int to) {
    Range {
        if (from > to) {
            throw new IllegalArgumentException("下界不能大于上界");
        }
    }

    int size() {
        return to - from + 1;
    }
}

System.out.println(new Range(1, 10).size());
System.out.println(new Range(10, 1));
  1. 两行输出分别是什么?第二个为什么不是 Range[from=10, to=1]?

编程题

  1. 用 Record 与 sealed 实现一个"支付结果"模型:
    • sealed interface PaymentResult permits Success, Failed, Pending;
    • Success 带交易号与金额;Failed 带错误码与原因;Pending 带预计完成时间;
    • 写方法 String describe(PaymentResult result),用 switch 模式匹配返回中文描述;
    • 要求:不写 default 分支也能编译通过。

综合任务

  1. 实现一个"任务执行器"对比工具:
    • 方法 long runPlatform(List<Runnable> tasks, int threads):用固定大小线程池跑完所有任务,返回耗时毫秒;
    • 方法 long runVirtual(List<Runnable> tasks):用虚拟线程跑完所有任务,返回耗时;
    • 每个任务做一次"IO 模拟"(Thread.sleep(10));
    • 分别用 1000 / 5000 / 20000 个任务测试,输出对比表;
    • 说明:任务数增大时,两者的差距为什么越来越大?什么时候该用虚拟线程、什么时候用平台线程池?
分级提示

提示 1(方向):第 4 题的关键是紧凑构造器里的校验会在对象构造完成前执行。

提示 2(关键点):第 5 题的 switch 之所以能省略 default,是因为 sealed 已经穷举了所有实现。

提示 3(接近答案):第 6 题的差距来自"排队"——平台线程池的并发度被线程数卡死,任务必须分批;虚拟线程没有这个上限。

12. 参考答案 ​

基础题
  1. 错。Record 的字段是 private final,只有 accessor(x()),没有 setter——这正是它不可变的保证方式。
  2. """(文本块);yield。
  3. IO 密集型任务(网络请求、数据库查询、文件读写——总之处在"等待"状态的任务)。不需要池化是因为虚拟线程极其轻量(约几 KB 而非约 1 MB),创建成本低到"用完即弃"完全可接受;池化反而会限制并发总量,抵消它的优势。
代码阅读题(第 4 题)
text
10
Exception in thread "main" java.lang.IllegalArgumentException: 下界不能大于上界

第二个不是正常输出,而是抛出了异常:紧凑构造器在赋值前执行,from > to 时直接抛 IllegalArgumentException,对象根本没有被创建出来。

这正是"用 Record 做值对象"的好处:非法状态压根无法被构造。换成传统类,你得记得在每个 setter 里校验,而漏一个就会出现"坏对象"在系统里流转。

编程题(第 5 题)
java
package cn.kb.learn.version;

import java.time.LocalDateTime;

public class PaymentResultDemo {
    sealed interface PaymentResult permits Success, Failed, Pending {
    }

    record Success(String transactionId, long amountInCents) implements PaymentResult {
    }

    record Failed(String errorCode, String reason) implements PaymentResult {
    }

    record Pending(LocalDateTime estimatedCompletion) implements PaymentResult {
    }

    static String describe(PaymentResult result) {
        return switch (result) {
            case Success success -> "支付成功,交易号 " + success.transactionId()
                    + ",金额 " + success.amountInCents() / 100.0 + " 元";
            case Failed failed -> "支付失败[" + failed.errorCode() + "]: " + failed.reason();
            case Pending pending -> "处理中,预计完成时间 " + pending.estimatedCompletion();
        };
    }

    public static void main(String[] args) {
        System.out.println(describe(new Success("T20260921001", 19_900)));
        System.out.println(describe(new Failed("BALANCE_NOT_ENOUGH", "余额不足")));
        System.out.println(describe(new Pending(LocalDateTime.of(2026, 9, 21, 15, 0))));
    }
}

为什么可以不写 default:sealed 已经明确列出只允许 Success、Failed、Pending 三种实现,编译器据此判断"三个分支已经穷尽"。

收益:以后新增 record Refunded(...),这里会立刻编译失败,强制你处理退款场景——把"运行时才发现漏了"变成"编译时就发现"。

预期输出:

text
支付成功,交易号 T20260921001,金额 199.0 元
支付失败[BALANCE_NOT_ENOUGH]: 余额不足
处理中,预计完成时间 2026-09-21T15:00
综合任务(第 6 题)
java
package cn.kb.learn.version;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorCompare {
    static long runPlatform(List<Runnable> tasks, int threads) throws InterruptedException {
        long start = System.currentTimeMillis();
        try (ExecutorService pool = Executors.newFixedThreadPool(threads)) {
            tasks.forEach(pool::submit);
        }
        return System.currentTimeMillis() - start;
    }

    static long runVirtual(List<Runnable> tasks) throws InterruptedException {
        long start = System.currentTimeMillis();
        try (ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) {
            tasks.forEach(pool::submit);
        }
        return System.currentTimeMillis() - start;
    }

    static List<Runnable> buildTasks(int count) {
        return java.util.stream.IntStream.range(0, count)
                .mapToObj(i -> (Runnable) () -> {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException exception) {
                        Thread.currentThread().interrupt();
                    }
                })
                .toList();
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("任务数   平台线程池(200)   虚拟线程");
        for (int count : new int[]{1_000, 5_000, 20_000}) {
            List<Runnable> tasks = buildTasks(count);
            long platform = runPlatform(tasks, 200);
            long virtual = runVirtual(tasks);
            System.out.println(count + "   " + platform + " ms" + "   " + virtual + " ms");
        }
    }
}

差距为什么越来越大:

平台线程池的并发度被线程数卡死——200 个线程一次只能跑 200 个任务,20000 个任务要分 100 批,每批至少 10 ms,理论下限约 1000 ms。任务数翻倍,等待时间就翻倍。

虚拟线程没有这个上限:1000 个任务就是 1000 个线程同时睡,20000 个也是同时睡——耗时几乎恒定在"一次 sleep 的时间"加上调度开销。

怎么选:

任务类型选择理由
IO 密集(等网络、等数据库)虚拟线程等待时让出载体线程,并发度不再受线程数限制
CPU 密集(计算、加密、压缩)固定大小平台线程池(约 = 核数)没有等待可以让出,线程数超过核数只会增加切换开销
需要限制并发量(保护下游)平台线程池 / 信号量虚拟线程太多可能压垮下游服务

最后一行值得记住:虚拟线程不是"越多越好"。它解决的是"线程数上限",但不解决"下游服务能承受多少并发"——限流仍然要做。

典型输出(数值依机器而变):

text
任务数   平台线程池(200)   虚拟线程
1000   48 ms   14 ms
5000   254 ms   30 ms
20000   1006 ms   89 ms

13. 掌握标准 ​

  • [ ] 能说出 Java 9-21 的关键特性及其版本;
  • [ ] 能用 Record 定义不可变数据类,并知道四种不该用的场景;
  • [ ] 会用文本块、switch 表达式(含 yield)、instanceof 模式匹配;
  • [ ] 能说出 sealed 带来的编译期穷尽检查的价值;
  • [ ] 能说清虚拟线程适合什么任务、为什么不要池化、与平台线程池如何选择;
  • [ ] 判断新特性是否使用时,标准是"是否让代码更清楚",而不是"是否更新"。

14. 扩展资源 ​

15. 下一步学习 ​

S1 编程语言基础到此完成(31/31)。

接下来按路线推进:

  • 补 《Java 语言与运行机制》(若尚未学习)
  • 进入 S3 企业开发基础:MySQL、Spring、Git 与 Linux、前端基础(该阶段页面将在产出时建立)
  • 或先完成测试专项 TA 单元测试

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引