Java 从入门到精通(十七):Java 8~21 新特性全景——Lambda、Stream、Record 与虚拟线程

本文是「Java 从入门到精通」系列的第 17 篇,假定你已经掌握集合框架、泛型、异常与并发基础(对应本系列第(七)(十二)(十三)篇)。全文代码以 JDK 21 为准,涉及预览特性的部分会用 --enable-preview 标注。阅读建议:Lambda 与 Stream 两章请务必动手跑一遍代码,尤其是 peek 不执行、流一次性消费、toMap 键冲突这三个坑,只有亲眼见过异常栈才会真正记住。

一、版本演进全景:从 Java 8 到 Java 21

2014 年发布的 Java 8 是 Java 历史上最重要的分水岭:它把函数式编程正式引入 Java 语法,Lambda、Stream、默认方法、新的日期时间 API 四件套同时落地。此后 Oracle 改为六个月一个版本的发布节奏,每两年挑一个版本作为 LTS(长期支持版)。到今天,Java 8 已经全面停止免费商业支持,而 Java 21 成为新一代的主流基线。

1.1 关键特性时间线

版本发布时间LTS关键特性(各取 2-3 个)
Java 82014-03是Lambda 表达式、Stream API、java.time 日期时间 API、接口默认方法
Java 92017-09否模块系统 JPMS、集合工厂方法 List.of、JShell、Stream 增强
Java 102018-03否局部变量类型推断 var、G1 并行 Full GC、应用类数据共享
Java 112018-09是HttpClient 正式版、单文件源码运行、String 增强(isBlank/lines/repeat)、ZGC 实验版
Java 122019-03否Switch 表达式(预览)、Shenandoah GC、CompactNumberFormat
Java 132019-09否文本块(预览)、Switch 表达式二次预览、Socket API 重构
Java 142020-03否Switch 表达式转正、instanceof 模式匹配(预览)、Record(预览)、Helpful NPE
Java 152020-09否文本块转正、密封类(预览)、EdDSA 签名、Shenandoah 转正
Java 162021-03否Record 转正、instanceof 模式匹配转正、Stream.toList()、Unix 域套接字
Java 172021-09是密封类转正、增强伪随机数发生器、macOS/AArch64 支持、移除 Applet
Java 182022-03否UTF-8 默认字符集、简单 Web 服务器、模式匹配二次预览
Java 192022-09否虚拟线程(预览)、结构化并发(预览)、Linux/RISC-V
Java 202023-03否Scoped Values(预览)、Record 模式(二次预览)、虚拟线程二次预览
Java 212023-09是虚拟线程转正、switch 模式匹配转正、Record 模式转正、Sequenced Collections、分代 ZGC、字符串模板(预览)
要点:LTS 并不是”功能更多”的版本,而是”支持周期更长”的版本。Oracle 对 Java 8/11/17/21 提供八年以上的商业支持,非 LTS 版本在下一个版本发布后即停止更新。生产环境选 LTS,但不要抗拒在开发和 CI 中使用最新非 LTS 版本做兼容性预演。

1.2 升级收益与迁移成本的权衡

升级的收益往往是隐性的、长期的:更少的样板代码(Record、var、文本块)、更强的表达力(模式匹配、密封类)、更高的吞吐(虚拟线程、分代 ZGC)、更低的内存占用(紧凑字符串、G1 改进)。而成本是显性的、短期的:依赖库兼容性、sun.misc.Unsafe 等内部 API 的强封装(JDK 9 起)、模块化的拆分、字节码增强工具(如部分老版本 CGLIB、ASM)的失效。

实际项目中比较稳妥的做法是分阶段升级:先把编译目标停留在低版本、运行时升级(-release 8 编译 + JDK 17 运行),验证稳定后再放开语言级别;遇到类加载或反射报错时,优先排查 IllegalAccessError 与 --add-opens 需要开放的包。

1.3 预览特性开关

Java 对尚未定稿的语法提供预览(Preview)机制:功能已实现但可能变更,编译与运行都需要显式开启。

在 Maven 中可以通过 maven-compiler-plugin 统一配置:

预览特性的字节码带有 minor_version = 65535 标记,JVM 启动时会校验是否带了 --enable-preview。这意味着预览特性编译出的 class 不能分发给生产环境,只能在本地实验。字符串模板(String Templates)在 JDK 21 就是预览状态,JDK 23 中还经历了 API 调整,千万别写进要交付的代码。

二、Lambda 表达式与方法引用

2.1 函数式接口与基本语法

函数式接口(Functional Interface)指的是只有一个抽象方法的接口。Lambda 表达式本质上就是这个函数式接口的实现实例。

Lambda 的语法是 (参数列表) -> { 方法体 }。参数类型通常可以省略(编译器根据目标类型推断);单参数可省括号;方法体只有一句表达式时可省 {} 与 return。

2.2 effectively final 与变量捕获

Lambda 可以捕获外部局部变量,但被捕获的变量必须是 final 或 effectively final(即声明后从未被重新赋值)。这是 Java 为了保证线程安全与实现简单性做出的限制:局部变量存放在栈帧中,而 Lambda 可能在另一个线程或稍后的时刻执行,栈帧早已销毁,因此 JVM 只能复制一份值进入 Lambda 实例。如果允许修改就会出现”副本与本体不一致”的经典 bug。

2.3 方法引用的四种形式

方法引用(Method Reference)是 Lambda 的”语法糖中的语法糖”:当 Lambda 体恰好只是调用一个已存在的方法时,可以用 :: 直接引用。

形式语法等价 Lambda示例
静态方法引用类名::静态方法(a, b) -> 类名.静态方法(a, b)Integer::parseInt
特定对象实例方法对象::实例方法(a) -> 对象.实例方法(a)System.out::println
特定类型任意对象实例方法类名::实例方法(a, b) -> a.实例方法(b)String::compareToIgnoreCase
构造器引用类名::new(a) -> new 类名(a)ArrayList::new、int[]::new

2.4 Lambda 与匿名内部类的本质差异

这是面试高频题,差异可以归纳为四点:

  1. this 语义不同。匿名内部类是一个独立的类,其中的 this 指向匿名类自身;Lambda 不是一个类,它没有自己的 this,Lambda 体内的 this 指向外围对象(即定义它的那个对象)。
  2. 类文件数量不同。匿名内部类编译后会生成 Outer$1.class 这样的独立 class 文件;Lambda 不会产生新的 class 文件。
  3. 字节码指令不同。匿名内部类是 new + invokespecial(Java 7 及以前的标准做法);Lambda 是 invokedynamic。
  4. 捕获语义不同。匿名内部类可以捕获并修改外部变量(Java 允许访问 effectively final 的局部变量,内部类字段可写),Lambda 只能读取 effectively final。

2.5 invokedynamic 与 LambdaMetafactory 的执行链路

早期有一种误解:”Lambda 就是匿名内部类的语法糖”。这是错的。JDK 8 的设计者刻意避开了为每个 Lambda 生成内部类,原因是内部类方案有三个硬伤:类文件膨胀、类加载开销大、未来无法替换实现策略。

真正的实现是 invokedynamic(indy),这条指令原本为动态语言(JRuby、Groovy)在 JVM 上运行而设计(JSR-292,Java 7 引入),Lambda 是它在 Java 语言中的第一个大规模应用。完整链路如下:

编译器在编译期做三件事:

  1. 把 Lambda 体脱糖(desugar)成一个私有方法,非捕获型编译为 private static 方法,捕获型编译为 private 实例方法(捕获的变量变成参数或字段)。
  2. 在类文件中写入一条 invokedynamic 指令,其引导方法(Bootstrap Method)指向 LambdaMetafactory.metafactory,常量池里带上函数式接口的方法名、方法签名、实现方法的 MethodHandle。
  3. 生成 BootstrapMethods 属性表。

运行期第一次执行到这条 invokedynamic 时,JVM 调用引导方法,LambdaMetafactory 用 ASM 在内存中动态生成一个类(类名形如 LambdaMetafactoryDemo$Lambda$14,是 LambdaMetafactoryDemo 的隐藏类 hidden class),该类实现目标函数式接口,其 applyAsInt 方法体直接转发到那个私有静态方法;随后把生成的类与调用点永久绑定(Linkage),后续调用就是一次普通的接口调用,可被 JIT 完全内联。

要点:为什么要绕这一圈?因为链接策略被延后到运行期。Lambda 的具体实现类是运行期生成的,JVM 未来可以换成”生成真正轻量的值对象””使用常量折叠”等更优策略,而所有已编译的 class 文件无需重新编译就能受益。这正是 invokedynamic 的设计精髓:把策略选择权从编译器交还给运行时。

2.6 Lambda 的性能

  • 首次链接有成本:第一次执行 invokedynamic 时要触发引导方法、生成类、链接,这是毫秒级的。之后就是常量开销。
  • 非捕获型 Lambda 会被缓存:没有捕获任何外部变量的 Lambda 是无状态的,JVM 会复用同一个实例(LambdaMetafactory 的生成类里有个静态单例字段)。捕获型 Lambda 每次执行都会 new 一个实例。
  • JIT 可以完全内联:Lambda 调用路径很短,C2 编译后通常与普通方法调用无差别,逃逸分析还可能把捕获型实例标量替换掉。
  • 结论:在热点代码中不要为了”性能”而拒绝 Lambda;真正的性能风险来自在循环里反复创建捕获型 Lambda 以及 Stream 的装箱开销,而不是 Lambda 本身。

三、函数式接口体系:java.util.function

3.1 四大基础接口与派生

java.util.function 包提供了 43 个接口,但只需记住四个”根接口”,其余都是它们的变体:

接口抽象方法含义常见派生
SupplierT get()无入参、有返回值(生产者)IntSupplier、BooleanSupplier
Consumervoid accept(T)有入参、无返回值(消费者)BiConsumer、IntConsumer、ObjIntConsumer
FunctionR apply(T)一入参、一返回值(转换器)BiFunction、UnaryOperator、IntFunction、IntToLongFunction
Predicateboolean test(T)一入参、返回布尔(判定器)BiPredicate、IntPredicate

命名规律要记牢:Bi 表示两个入参;Int/Long/Double 前缀表示入参是基本类型(避免装箱);ToInt/ToLong/ToDouble 后缀表示返回值是基本类型;UnaryOperator 是 Function 的特化,BinaryOperator 是 BiFunction 的特化。

3.2 自定义函数式接口与 @FunctionalInterface

@FunctionalInterface 注解不是必需的,但它有两个价值:编译期保护(一旦你加了第二个抽象方法就报错)和自文档化。注意它只约束”抽象方法数量”,默认方法与静态方法不计入。

3.3 函数组合:andThen / compose / negate / and / or

组合是函数式编程的核心武器:把小函数拼成大函数,避免层层嵌套。

3.4 重构实战:把命令式代码改写为函数式

四、Stream API 深入

4.1 Stream 与集合的本质区别

维度集合(Collection)流(Stream)
存储存储全部元素,是内存中的数据结构不存储数据,按需从源头拉取
求值急切(eager):add 时元素已在惰性(lazy):只有终端操作才触发计算
遍历次数可无限次遍历一次性,消费后即关闭,再调用抛 IllegalStateException
遍历方式外部迭代(for / iterator,用户控制)内部迭代(库控制,可并行、可优化)
数据规模有限可以是无限流(iterate、generate)
关注点数据本身对数据的计算

4.2 创建 Stream 的六种方式

4.3 中间操作与终端操作对照表

类别操作说明是否有状态
中间filter按 Predicate 过滤无状态
中间map / mapToInt/Long/Double一对一转换 / 转基本类型流无状态
中间flatMap / flatMapToInt...一对多转换并扁平化无状态
中间peek调试用,观察元素流过无状态
中间distinct去重,依赖 equals/hashCode有状态
中间sorted / sorted(cmp)排序,需缓冲全部元素有状态
中间limit / skip截断 / 跳过(有序流可短路)有状态
中间takeWhile / dropWhile(JDK 9+)按条件取前缀 / 丢前缀有状态
终端forEach / forEachOrdered遍历,后者保证有序—
终端collect汇聚为集合/字符串/Map/自定义—
终端reduce归约为单个值—
终端count / min / max计数 / 最值(min/max 返回 Optional)—
终端anyMatch / allMatch / noneMatch短路匹配—
终端findFirst / findAny查找(返回 Optional)—
终端toArray / toList(JDK 16+)转为数组 / 不可变 List—

4.4 流水线结构与惰性求值

Stream 的内部实现是一条双向链表:源码位于 java.util.stream,核心类是 AbstractPipeline 的三个子类。

  • Head:流水线的头节点,持有数据源(Spliterator)与是否并行的标志。
  • StatelessOp(无状态中间操作,如 filter/map):处理单个元素即可输出结果,不需要知道其它元素。
  • StatefulOp(有状态中间操作,如 sorted/distinct/limit):必须缓冲或看到多个元素才能输出结果。

终端操作触发时,Stream 从最后一个操作开始反向回溯,把每个中间操作包装成一个 Sink,通过 opWrapSink 方法把后一个 Sink 传给前一个,形成一条 Sink 链(本质上是职责链模式)。数据源通过 Spliterator.forEachRemaining 把元素一个个喂给链头,元素依次穿过所有 Sink,最后由终端操作的 Sink 收集结果。

peek 不执行是最经典的踩坑:很多人写完 stream.peek(System.out::println) 发现控制台什么都没有,就以为 Stream “坏了”。真相是 peek 是中间操作,必须接终端操作。另外在 limit/count 这类短路场景下,JIT 与 Stream 框架可能判定 peek 的结果无人使用而整个跳过,所以 peek 只适合调试,绝不要在里面写业务逻辑(count() 在 JDK 9+ 还会直接跳过中间操作优化)。

4.5 常用操作与 Collectors 全家桶

4.5.1 自定义 Collector

Collector 有五个要素:supplier(创建可变容器)、accumulator(累加元素)、combiner(并行时合并容器)、finisher(容器转结果)、characteristics(特性标志,影响框架能否优化)。

4.6 flatMap 的扁平化思维

map 是”一对一的盒子套盒子”,flatMap 是”把盒子拆开摊平”。判断标准:如果 map 之后得到的是 Stream> 或 Stream>,就该用 flatMap。

4.7 Stream 的坑与避坑指南

4.8 parallelStream 的拆分策略与 Spliterator

并行流的底层是 Fork/Join 框架(本系列第(十三)篇讲过线程池,这里只聚焦拆分逻辑):

  1. 数据源被包装成一个 Spliterator(可拆分迭代器),它除了 tryAdvance(遍历)还有 trySplit(把自己一分为二)。
  2. 框架递归调用 trySplit,直到子任务小到不值得再拆(阈值约 1024 个元素),形成一棵任务树。
  3. 每个叶子任务在 ForkJoinPool 的工作线程上执行,通过 work-stealing 调度。
  4. 每个子任务独立执行完整的流水线(各自的 Sink 链),最后由 combiner 合并结果。

因此并行流生效的前提是:数据源易拆分(ArrayList、数组、IntStream.range 的 Spliterator 拆分是 O(1);LinkedList、Files.lines、BufferedReader.lines 拆分代价高甚至无法有效拆分)、操作无状态无副作用、结果可合并。

并行流不是性能优化的万能药。数据量小于一万、操作本身很轻(如简单 map)、数据源是 LinkedList、涉及 I/O 阻塞这四种情况下,并行往往比串行更慢。真正的收益场景是:数据量大(十万级以上)、单个元素处理耗时(CPU 密集)、数据源可均匀拆分。另外请记住 parallelStream() 走的是 ForkJoinPool.commonPool(),一旦有任务阻塞,会影响到 JVM 中所有使用并行流的代码。

五、Optional 与空值处理

5.1 定位:返回值的容器

Optional 的设计初衷是作为方法返回值,明确告诉调用者”这里可能没有值”。它不应该用作类的字段(Serializable 都不支持)、方法参数(会造成调用方必须包装的负担)或集合元素(用空集合表示无元素即可,别用 List>)。

5.2 三大取值方法:orElse / orElseGet / orElseThrow 的区别

这是面试必考点,核心差异在求值时机。

结论:默认值是常量或已存在的变量时用 orElse;默认值需要计算(查库、拼字符串、构造对象)时务必用 orElseGet,否则白白付出计算代价。

5.3 Optional 方法速查表

方法JDK作用备注
of / ofNullable / empty8创建of(null) 直接 NPE
isPresent / isEmpty8 / 11判断是否有值isEmpty 是 JDK 11 新增
get8取值空值时抛异常,尽量少用
orElse / orElseGet8兜底值前者急切、后者惰性
orElseThrow() / orElseThrow(Supplier)10 / 8兜底异常无参版抛 NoSuchElementException
ifPresent / ifPresentOrElse8 / 9消费返回 void
map / flatMap8转换flatMap 避免 Optional>
filter8条件过滤不满足则变 empty
stream9转 Stream空则为空流,可无缝接 Stream 链
or9链式兜底a.or(() -> b)

5.4 与持久层查询的配合

Optional 误用清单:① 用 Optional 作为字段或参数;② isPresent()+get() 的老式写法;③ 在 orElse 里放高开销计算;④ 集合查询返回 Optional>(应返回空集合);⑤ 用 Optional.of(可能为 null 的值)(应用 ofNullable);⑥ 在 Lambda 里创建 Optional 只为调用 get();⑦ 把 Optional 当 null 一样 return null 回去(比不用 Optional 还糟)。

六、JDK 9-11:工程实用性的飞跃

6.1 集合工厂方法与不可变集合

6.2 Stream 增强与 String 新方法

6.3 var 局部变量类型推断的使用边界

var 是 Java 10 引入的,只用于局部变量,编译期推断出确切类型后写入字节码,Java 依然是强类型语言,不存在运行期动态类型。

使用建议:当类型在右侧已经明确写出(new ArrayList())或类型名极长(Map>)时用 var 能显著提升可读性;当初始化表达式类型不明显(var result = service.query())时不要用 var,那会让读代码的人必须跳到方法签名才知道类型。

6.4 HttpClient:JDK 11 正式版

java.net.http.HttpClient 取代了老旧的 HttpURLConnection,支持 HTTP/2、WebSocket、同步/异步、响应式 Body 处理。

6.5 单文件源码运行与 JVM 改进

JVM 层面值得一提的改进:G1 从 JDK 9 起成为默认垃圾收集器(取代 Parallel GC),并把原本单线程的 Full GC 改为并行;AppCDS(Application Class-Data Sharing) 可以把类元数据存档复用,显著缩短启动时间(对 Spring Boot 这类大应用收益明显);Epsilon GC 是一个”只分配不回收”的 No-op 收集器,用于性能测试与超短生命周期任务;ZGC 在 JDK 11 以实验特性登场,JDK 15 转正。

七、JDK 12-17:语言表达力的黄金五年

7.1 switch 表达式(JDK 14 转正)

旧 switch 语句有三大痛点:必须写 break(忘了就 fall-through)、每个 case 是独立作用域导致变量泄漏、作为表达式使用时需要临时变量。新 switch 表达式用 -> 箭头语法彻底解决。

7.2 文本块 Text Blocks(JDK 15 转正)

文本块用三重双引号包裹,解决 SQL/JSON/HTML 拼接的地狱。编译期会做两件事:去掉每行末尾的 incidental 空白(按最小缩进对齐)和规范化换行符。

7.3 instanceof 模式匹配(JDK 16 转正)

7.4 Record 记录类(JDK 16 转正)

Record 是不可变透明载体(transparent carrier):你只声明组件(components),编译器自动生成私有 final 字段、公共访问器(name() 而非 getName())、规范构造器、equals/hashCode/toString。

Record 的字节码产物:javap -v Point.class 可以看到 ①类是 final 且 extends java.lang.Record;②每个组件生成 private final 字段与同名无参访问器方法;③生成 equals/hashCode/toString 三个方法(基于 Objects.hash 与 invokedynamic ObjectMethods.bootstrap,后者的策略同样可在运行期替换);④生成一个 Class.getRecordComponents 所需的 Record 属性表。Record 不能继承任何类(已隐式继承 Record),但可以实现接口。

Record vs Lombok @Data:① Record 是语言级特性,字节码由编译器生成,没有注解处理器、没有依赖、没有 IDE 插件也能编译;② Record 不可变(字段全是 final,没有 setter),Lombok 的 @Data 生成 setter;③ Record 的访问器是 x() 不是 getX(),Jackson 等框架需要额外适配(新版已支持);④ Record 不能继承、不能加实例字段,Lombok 的 @Data 可以;⑤ 需要 JPA 实体、需要可变、需要继承 → 用 Lombok;需要 DTO、值对象、方法多返回值、Map 复合键 → 用 Record。

7.5 密封类 sealed(JDK 17 转正)

密封类用来精确控制谁能继承我,配合模式匹配就能实现编译期可穷举的代数数据类型(ADT)。

编译器与 JVM 的 permits 校验规则(硬核点):

  1. permits 列出的类必须存在且可访问,必须直接继承/实现该密封类。
  2. 每个被允许的子类必须用 final、sealed、non-sealed 三者之一修饰,没有第四个选项。这是强制的:否则无限继承链又回来了。
  3. 密封类与其子类必须在同一个包(使用无名模块时)或同一个模块(使用 JPMS 时)。
  4. 校验分两层:javac 在编译期检查;class 文件里写入 PermittedSubclasses 属性,JVM 在类加载时校验——即使你手工改字节码绕过 javac,JVM 也会拒绝加载非法子类。
  5. 反射层面新增 Class.isSealed() 与 Class.getPermittedSubclasses()。

7.6 Helpful NullPointerException

JDK 14 起(JEP 358),JVM 可以打印出精确到变量名的 NPE 信息,默认开启(也可显式加 -XX:+ShowCodeDetailsInExceptionMessages)。

这条信息直接告诉你是哪个方法调用的返回值为 null,在生产日志里价值极高——再也不用靠”第 108 行”猜了。

八、JDK 18-21:模式匹配的终局与并发新纪元

8.1 switch 模式匹配(JDK 21 转正)

switch 终于可以按类型分支,配合 when 守卫与 Record 解构,Java 具备了完整的模式匹配能力。

type switch 的编译产物:编译器会对每个 case 生成 instanceof 判断 + 类型模式绑定(等价手写的强转),并把整条 switch 编译成先按类型分派再按条件判断的字节码;JDK 21 的实现会把匹配逻辑编译为 invokedynamic SwitchBootstraps.typeSwitch(同样是 indy 引导),使得匹配策略可以在运行期优化(比如按类型哈希做跳转表)。当 switch 的选择器类型是密封接口且所有子类型都被穷举时,编译器不需要 default 分支也不会报”缺少返回语句”——这是密封类带来的最大红利。

8.2 虚拟线程:简要回顾

虚拟线程(Virtual Threads,JEP 444,JDK 21 转正)是JVM 管理的用户态线程,由载体线程(carrier thread,即平台线程)承载,遇到阻塞(如 LockSupport.park、Socket I/O)时会自动从载体线程上卸载,从而用一个平台线程支撑成千上万个并发任务。完整的原理、调度模型与线程池用法见本系列第(十四)篇,这里只做横向对比。

维度平台线程(Thread)虚拟线程(VirtualThread)
实现对操作系统线程的 1:1 包装JVM 管理,M:N 映射到载体线程
默认栈约 1MB(-Xss)按需增长的堆上栈,初始几百字节
创建成本约 1ms,数量受限(千级)微秒级,可达百万级
适用场景CPU 密集型、需要线程局部状态的场景I/O 密集型、高并发请求处理(典型 Web 服务)
池化建议必须池化不要池化,用完即建
注意事项—synchronized 会 pin 住载体线程(JDK 21 有此问题,JDK 24 已大幅改善,优先用 ReentrantLock);ThreadLocal 内存放大,考虑用 Scoped Values

8.3 结构化并发与 Scoped Values(预览)

  • 结构化并发(Structured Concurrency,JDK 21 预览):把一组并发子任务视为一个工作单元,StructuredTaskScope 保证子任务的生命周期不会超出父任务的作用域,任一子任务失败即可整体取消,解决”线程泄漏 + 取消传播”的老问题。
  • Scoped Values(JDK 21 预览):用于在线程内及子线程间共享不可变数据,是 ThreadLocal 的轻量替代,尤其适合虚拟线程场景(ThreadLocal 在百万级虚拟线程下内存开销惊人)。

由于两者在 JDK 21 仍是预览特性,生产环境请等待转正版本,本文不再展开代码。

8.4 Sequenced Collections(JDK 21)

JDK 21 新增了三个接口,补齐了”有确定遭遇顺序(encounter order)的集合”这一长期缺失的抽象:

接口新增方法已有实现
SequencedCollectiongetFirst()、getLast()、addFirst()、addLast()、removeFirst()、removeLast()、reversed()List、Deque、LinkedHashSet、SortedSet/LinkedHashSet
SequencedSet继承上述(去重语义)LinkedHashSet、SortedSet
SequencedMapfirstEntry()、lastEntry()、pollFirstEntry()、pollLastEntry()、putFirst()、putLast()、sequencedKeySet()、reversed()LinkedHashMap、SortedMap

8.5 其它重要演进

  • 分代 ZGC(JDK 21,JEP 439):ZGC 引入年轻代/老年代分代,大幅降低 GC 期间的内存分配开销与 CPU 占用,同时保持亚毫秒级停顿。
  • 字符串模板(String Templates,JDK 21 预览,JEP 430):用 STR." Hello \{name} " 语法做安全的字符串插值,配合 FMT 处理器可做格式化;注意它在 JDK 23 中 API 再次调整,切勿用于生产代码。
  • Vector API(JDK 21 第六次孵化):提供平台无关的 SIMD 向量计算能力,让 Java 能利用 CPU 的 AVX 指令做并行数值计算。
  • UTF-8 默认字符集(JDK 18,JEP 400):FileReader、InputStreamReader 等默认编码终于统一为 UTF-8,跨平台行为一致。

8.6 语法糖版本速查表

语法/API转正版本一句话说明
Lambda / 方法引用Java 8invokedynamic + LambdaMetafactory
Stream APIJava 8惰性流水线 + Sink 链
接口默认方法/静态方法Java 8接口可带实现
var 局部变量推断Java 10仅局部变量,编译期推断
集合工厂 List.ofJava 9不可变、不接受 null
Optional.stream/isEmpty/orJava 9/11/9Optional 增强
HttpClientJava 11替代 HttpURLConnection
switch 表达式Java 14-> 箭头 + yield
文本块Java 15""" 三重引号
instanceof 模式匹配Java 16判断与绑定一步
RecordJava 16不可变透明载体
Stream.toList()Java 16返回不可变 List
密封类Java 17permits 精确控制继承
Helpful NPEJava 14精确到变量名的 NPE
switch 模式匹配Java 21类型模式 + 守卫 + 记录解构
虚拟线程Java 21M:N 用户态线程
Sequenced CollectionsJava 21getFirst/getLast/reversed

8.7 升级路线建议

  1. 还在 Java 8:先升到 Java 17,这是当前生态支持最完整的 LTS(Spring Boot 3.x 强制要求 17+)。迁移重点:模块强封装导致的反射报错(--add-opens)、javax.* → jakarta.* 命名空间切换、构建插件版本升级。
  2. 已在 Java 11/17:评估 Java 21,主要收益是虚拟线程与分代 ZGC;风险点主要是 GC 参数与字节码增强工具(Byte Buddy、ASM、CGLIB)的版本兼容。
  3. 语言级别采用顺序:先用 Record 与 switch 表达式(收益最大、风险最小),再上文本块与模式匹配,最后是密封类与预览特性。
  4. 永远在 CI 上跑一遍最新非 LTS 版本的编译与测试,提前半年发现兼容性问题。

8.8 高频面试题十问十答

  1. Lambda 表达式的底层实现是什么? 不是匿名内部类。编译期把 Lambda 体脱糖为一个私有方法(非捕获为 static),在原位置写入 invokedynamic 指令,引导方法是 LambdaMetafactory.metafactory;首次执行时由它用 ASM 在内存中动态生成实现函数式接口的类,并永久链接到调用点。
  2. 为什么 Lambda 捕获的局部变量必须是 effectively final? 局部变量在栈帧中,Lambda 可能在其它线程或稍后执行,JVM 只能复制一份值;若允许修改会造成副本与本体不一致。
  3. Lambda 里的 this 指向谁? 指向外围对象(定义它的那个实例),因为 Lambda 不是类、没有自己的 this。匿名内部类的 this 指向匿名类自身。
  4. Stream 为什么是惰性的? 中间操作只往流水线链表上挂 StatelessOp/StatefulOp 节点,不触发计算;只有终端操作才反向回溯组装 Sink 链,由数据源 Spliterator 推送元素逐个穿过整条链。
  5. Stream 能复用吗? 不能。消费后流即关闭,再次调用抛 IllegalStateException。需要多次遍历就重新创建或用 Supplier>。
  6. 什么情况下不该用 parallelStream? 数据量小、单元素处理很轻、数据源难拆分(LinkedList、IO 流)、任务阻塞(会污染 ForkJoinPool.commonPool())、依赖顺序、有共享可变状态。
  7. Record 和 Lombok @Data 有什么区别? Record 是语言级特性、编译期生成、不可变、访问器无 get 前缀、不能继承与加实例字段;Lombok 是注解处理器、可变、可继承。DTO/值对象用 Record,JPA 实体用 Lombok。
  8. switch 表达式和旧 switch 语句有什么区别? 箭头语法无需 break(无 fall-through)、可作为表达式直接返回值、多值用逗号、case 块用 yield 返回、配合密封类型可省略 default 且编译器能检查穷举性。
  9. orElse 与 orElseGet 的区别? orElse 的参数总会被求值(急切),orElseGet 的 Supplier 只在值为空时才执行(惰性)。默认值需要计算时必须用 orElseGet。
  10. 密封类的子类必须满足什么约束? 必须在 permits 列表中(或同包/同模块下自动推断),且必须用 final、sealed、non-sealed 之一修饰,否则编译失败;JVM 类加载时还会依据 class 文件中的 PermittedSubclasses 属性二次校验。

本篇覆盖 Java 8~21 的主干特性,但每个主题都还能往深处再走一层:Lambda 的隐藏类(Hidden Class)机制、Stream 的 AbstractPipeline.opWrapSink 源码、Record 的 ObjectMethods.bootstrap 引导逻辑、Fork/Join 的 work-stealing 细节。建议用 javap -v -p 反编译本篇的示例代码,亲眼看看编译器到底生成了什么——这是把”知道”变成”理解”的最短路径。

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论