主题
界面
主题
从 Java 8 到 Java 21,语言发生了不小的变化:Record 可以一行替代几十行样板代码,switch 变成了表达式,虚拟线程让"一个任务一个线程"重新变得可行。
本节是 S1 的最后一节,也是你以后写的代码长什么样的预览。重点不是背特性清单,而是知道哪些特性值得马上用、哪些要谨慎用。
完成本节后,你应该能够:
Record 定义不可变数据载体,并用紧凑构造器做校验;Record 的适用与不适用场景;if-else 链;sealed + 模式匹配让编译器帮你检查"分支是否漏了";看两个版本的对比。先说"一个不可变数据类":
// 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 16+:一行
public record Point(int x, int y) { }这不是"少打字",而是消除一整类 bug:手写的 equals / hashCode 极其容易写错(尤其是漏了字段或忘了改),而 Record 由编译器保证正确。
再看并发:虚拟线程让"每个请求一个线程"这种简单直白的写法重新变得可行——第 7 节的实测显示,同样 10000 个任务,虚拟线程比 200 个平台线程的线程池快约 10 倍。
| 版本 | 类型 | 值得记住的 |
|---|---|---|
| Java 8 | LTS | Lambda、Stream、Optional、新日期时间 API(《Java 8 核心特性总览》) |
| Java 9 | 模块系统、List.of()、接口私有方法 | |
| Java 10 | var 局部变量类型推断 | |
| Java 11 | LTS | String 新方法(isBlank、strip、repeat)、HttpClient |
| Java 14 | switch 表达式(正式) | |
| Java 15 | 文本块(正式) | |
| Java 16 | Record、instanceof 模式匹配 | |
| Java 17 | LTS | sealed 类、Spring Boot 3 的最低要求 |
| Java 21 | LTS | 虚拟线程、switch 模式匹配、SequencedCollection |
学习建议:以 Java 21 为基线(本项目就是),知道 Java 8 / 17 的差异即可——面试会问,存量项目会遇到。
public record Point(int x, int y) { }编译器自动生成:构造方法、x() / y() accessor、equals、hashCode、toString。
四条规则:
private final——不可变;java.lang.Record,不能继承别的类(可以实现接口);x() 而不是 getX();紧凑构造器(做校验,不重复写赋值):
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+) | 构造方法参数太长,可读性差(考虑建造者模式) |
String sql = """
SELECT id, name
FROM users
WHERE status = 'ACTIVE'
ORDER BY id""";三个要点:
""" 开始与结束;""" 放在最左,内容就没多余缩进;\n,不要在行尾加 \(那是取消换行)。最适合:SQL、JSON、HTML 片段、测试数据。别用它写"要拼接变量的长字符串"——那个场景用 formatted() 或模板引擎更合适。
String type = switch (day) {
case "SAT", "SUN" -> "周末";
case "MON", "TUE", "WED", "THU", "FRI" -> "工作日";
default -> "未知";
};与旧 switch 的三个差别:
旧 switch 语句 | 新 switch 表达式 | |
|---|---|---|
| 返回值 | 不能直接返回 | 是表达式,有值 |
| 穿透 | 默认穿透,要写 break | -> 语法不穿透 |
| 穷尽检查 | 无 | 对枚举/sealed 编译期检查 |
代码块要返回值时用 yield:
String grade = switch (score / 10) {
case 10, 9 -> "优秀";
case 8 -> "良好";
default -> {
String note = "需要补考";
yield "不及格(" + note + ")"; // yield 而不是 return
}
};return 会直接退出整个方法,yield 只是"这个分支的值"——混用会导致意外行为。
// 旧写法:判断 + 强转 + 声明变量,三步
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 的作用域只在判断为真的分支内——编译器帮你避免"用了没转型成功的变量"。
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 是否穷尽了所有可能。
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)可以在模式上再筛一层:
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 分支必须写在同类模式的普通分支之前,否则永远是普通分支命中。
var names = List.of("a", "b"); // 推断为 List<String>
var cache = new HashMap<String, Integer>(); // 右侧写了泛型,左侧才能省四条限制:
var x = null; 编译错误)。该不该用:判断标准是**"右侧是否已经说清了类型"**。
var user = new User(); // 好:一眼知道是 User
var list = new ArrayList<User>(); // 好
var result = service.process(order, config); // 差:看不出返回什么类型var 不是"动态类型",它仍然是静态类型,只是让编译器去推断。不要为了少打字而牺牲可读性。
问题:平台线程(传统线程)一个约 1 MB 栈,创建 1 万个就吃 10 GB 内存。所以只能用"少量线程 + 线程池",代码被写成异步回调或复杂的响应式风格。
虚拟线程:由 JVM 调度,一个只要几 KB,可以创建几十万个。
// 平台线程池: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 调用),这才是它最大的价值。
使用的三条注意:
newVirtualThreadPerTaskExecutor() 就是标准用法);synchronized 结合时可能出现"固定"(pinning)——Java 21 中应优先用 ReentrantLock(S4 并发章节详述)。record Point(int x, int y) { }
Point point = new Point(3, 4);
point.x(); // 3
point.equals(new Point(3, 4)); // true(自动生成)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 都会编译失败,逼你去处理它。
// 反例一: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 等待,虚拟线程不会更快
}文件:labs/version-features/src/main/java/cn/kb/learn/version/ModernJavaLesson.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();
}
}运行命令:
cd labs
./mvnw -pl version-features compile
java -cp version-features/target/classes cn.kb.learn.version.ModernJavaLesson预期输出(虚拟线程两行的毫秒数依机器而变,请关注两者的数量级差异):
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 的时间。
new PositivePoint(-1, 0) 处按 F7 进入,观察紧凑构造器先执行校验、再赋值的顺序;point.equals(new Point(3, 4)) 处 F7 进入,看编译器生成的是逐字段比较(不是 Objects.equals 包装);area(shape) 的 switch 处断点,观察每个分支命中时的具体类型(Circle / Rectangle / Triangle),这就是"模式匹配"在运行时的样子;sql.lines().count() 处断点,展开 sql 字符串,观察它是普通字符串(文本块只是编译期的书写形式,运行时没有特殊类型);虚拟线程@xxx,数量上万但平台线程只有几十个——这正是"多对一"的调度关系。yield 与 return 混用 String grade = switch (score) {
default -> {
return "不及格"; // 错:直接退出了整个方法
}
};修复:switch 表达式内部用 yield,只有确实想结束方法时才用 return。
var 降低了可读性 见 6.3 节。修复:右侧已经说明类型时用 var,否则写明确类型。
症状:Hibernate 报"没有默认构造方法"或"无法设置字段"。原因:实体需要无参构造 + 可变字段 + 非 final 类(用于代理)。修复:实体用普通类 + getter/setter,DTO 用 Record。
Executors.newFixedThreadPool(200, Thread.ofVirtual().factory()); // 反模式原因:池化的价值在于"复用昂贵资源",而虚拟线程便宜到不需要复用——池化反而限制了并发量。修复:用 newVirtualThreadPerTaskExecutor()。
虚拟线程的收益来自IO 等待时的让出。纯计算任务没有等待可以让,调度开销反而可能让它略慢——CPU 密集任务本来就该用固定大小的平台线程池。
return switch (shape) {
case Circle circle -> "圆"; // 永远先命中这里
case Circle circle when circle.radius() > 10 -> "大圆"; // 永远不会执行
};修复:把带 when 的特殊分支写在前面。这一点与 if-else 的顺序敏感性相同。
目标:用现代语法重写一段传统代码。
步骤:
score = 55 改成 95、85、75、65、55 各跑一次,验证五个分支都能正确命中;Point 类(40 行),再用 Record 写一遍(1 行),对比两者的 equals 与 hashCode;sealed interface Shape 新增一个 record Pentagon(double side) implements Shape,先不要改 area 方法,观察编译错误信息;virtualThreadDemo 里的 Thread.sleep(10) 改成一段纯计算(如累加 10 万次),再跑一次——观察虚拟线程的耗时优势是否还在。验收方式:能解释第 4 步的编译错误为什么是"好事",以及第 5 步为什么优势消失。
扩展任务:用 sealed + Record + switch 模式匹配实现一个极简的表达式求值器(支持数字、加法、乘法三种节点),要求"漏处理任何一种节点就编译失败"。
基础题
Record 会自动生成 setter 方法。(对 / 错)代码阅读题
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));Range[from=10, to=1]?编程题
sealed interface PaymentResult permits Success, Failed, Pending;Success 带交易号与金额;Failed 带错误码与原因;Pending 带预计完成时间;String describe(PaymentResult result),用 switch 模式匹配返回中文描述;default 分支也能编译通过。综合任务
long runPlatform(List<Runnable> tasks, int threads):用固定大小线程池跑完所有任务,返回耗时毫秒;long runVirtual(List<Runnable> tasks):用虚拟线程跑完所有任务,返回耗时;Thread.sleep(10));提示 1(方向):第 4 题的关键是紧凑构造器里的校验会在对象构造完成前执行。
提示 2(关键点):第 5 题的 switch 之所以能省略 default,是因为 sealed 已经穷举了所有实现。
提示 3(接近答案):第 6 题的差距来自"排队"——平台线程池的并发度被线程数卡死,任务必须分批;虚拟线程没有这个上限。
private final,只有 accessor(x()),没有 setter——这正是它不可变的保证方式。"""(文本块);yield。10
Exception in thread "main" java.lang.IllegalArgumentException: 下界不能大于上界第二个不是正常输出,而是抛出了异常:紧凑构造器在赋值前执行,from > to 时直接抛 IllegalArgumentException,对象根本没有被创建出来。
这正是"用 Record 做值对象"的好处:非法状态压根无法被构造。换成传统类,你得记得在每个 setter 里校验,而漏一个就会出现"坏对象"在系统里流转。
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(...),这里会立刻编译失败,强制你处理退款场景——把"运行时才发现漏了"变成"编译时就发现"。
预期输出:
支付成功,交易号 T20260921001,金额 199.0 元
支付失败[BALANCE_NOT_ENOUGH]: 余额不足
处理中,预计完成时间 2026-09-21T15:00package 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 密集(计算、加密、压缩) | 固定大小平台线程池(约 = 核数) | 没有等待可以让出,线程数超过核数只会增加切换开销 |
| 需要限制并发量(保护下游) | 平台线程池 / 信号量 | 虚拟线程太多可能压垮下游服务 |
最后一行值得记住:虚拟线程不是"越多越好"。它解决的是"线程数上限",但不解决"下游服务能承受多少并发"——限流仍然要做。
典型输出(数值依机器而变):
任务数 平台线程池(200) 虚拟线程
1000 48 ms 14 ms
5000 254 ms 30 ms
20000 1006 ms 89 msyield)、instanceof 模式匹配;sealed 带来的编译期穷尽检查的价值;S1 编程语言基础到此完成(31/31)。
接下来按路线推进:
暂无