本文是「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 8
2014-03
是
Lambda 表达式、Stream API、java.time 日期时间 API、接口默认方法
Java 9
2017-09
否
模块系统 JPMS、集合工厂方法 List.of、JShell、Stream 增强
Java 10
2018-03
否
局部变量类型推断 var、G1 并行 Full GC、应用类数据共享
Java 11
2018-09
是
HttpClient 正式版、单文件源码运行、String 增强(isBlank/lines/repeat)、ZGC 实验版
Java 12
2019-03
否
Switch 表达式(预览)、Shenandoah GC、CompactNumberFormat
Java 13
2019-09
否
文本块(预览)、Switch 表达式二次预览、Socket API 重构
Java 14
2020-03
否
Switch 表达式转正、instanceof 模式匹配(预览)、Record(预览)、Helpful NPE
Java 15
2020-09
否
文本块转正、密封类(预览)、EdDSA 签名、Shenandoah 转正
Java 16
2021-03
否
Record 转正、instanceof 模式匹配转正、Stream.toList()、Unix 域套接字
Java 17
2021-09
是
密封类转正、增强伪随机数发生器、macOS/AArch64 支持、移除 Applet
Java 18
2022-03
否
UTF-8 默认字符集、简单 Web 服务器、模式匹配二次预览
Java 19
2022-09
否
虚拟线程(预览)、结构化并发(预览)、Linux/RISC-V
Java 20
2023-03
否
Scoped Values(预览)、Record 模式(二次预览)、虚拟线程二次预览
Java 21
2023-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) 机制:功能已实现但可能变更,编译与运行都需要显式开启。
1 2 3 4 javac --release 21 --enable-preview Main.java java --enable-preview Main
在 Maven 中可以通过 maven-compiler-plugin 统一配置:
1 2 3 4 5 6 7 8 <plugin > <artifactId > maven-compiler-plugin</artifactId > <configuration > <release > 21</release > <compilerArgs > --enable-preview</compilerArgs > </configuration > </plugin >
预览特性的字节码带有 minor_version = 65535 标记,JVM 启动时会校验是否带了 --enable-preview。这意味着预览特性编译出的 class 不能分发给生产环境 ,只能在本地实验。字符串模板(String Templates)在 JDK 21 就是预览状态,JDK 23 中还经历了 API 调整,千万别写进要交付的代码。
二、Lambda 表达式与方法引用 2.1 函数式接口与基本语法 函数式接口(Functional Interface) 指的是只有一个抽象方法 的接口。Lambda 表达式本质上就是这个函数式接口的实现实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.util.*;public class LambdaBasic { public static void main (String[] args) { Comparator<String> oldWay = new Comparator <String>() { @Override public int compare (String a, String b) { return Integer.compare(a.length(), b.length()); } }; Comparator<String> newWay = (a, b) -> Integer.compare(a.length(), b.length()); List<String> words = new ArrayList <>(List.of("java" , "lambda" , "go" , "rust" )); words.sort(newWay); System.out.println(words); words.forEach(w -> System.out.println(w.toUpperCase())); } }
Lambda 的语法是 (参数列表) -> { 方法体 }。参数类型通常可以省略(编译器根据目标类型推断);单参数可省括号;方法体只有一句表达式时可省 {} 与 return。
2.2 effectively final 与变量捕获 Lambda 可以捕获外部局部变量,但被捕获的变量必须是 final 或 effectively final (即声明后从未被重新赋值)。这是 Java 为了保证线程安全与实现简单性做出的限制:局部变量存放在栈帧中,而 Lambda 可能在另一个线程或稍后的时刻执行,栈帧早已销毁,因此 JVM 只能复制一份值 进入 Lambda 实例。如果允许修改就会出现”副本与本体不一致”的经典 bug。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class CaptureDemo { public static void main (String[] args) { int factor = 10 ; Runnable r = () -> System.out.println(factor * 3 ); new Thread (r).start(); Counter c = new Counter (); Runnable inc = () -> c.value++; inc.run(); System.out.println(c.value); } static class Counter { int value; } }
2.3 方法引用的四种形式 方法引用(Method Reference)是 Lambda 的”语法糖中的语法糖”:当 Lambda 体恰好只是调用一个已存在的方法时,可以用 :: 直接引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import java.util.*;import java.util.function.*;public class MethodRefDemo { public static void main (String[] args) { Function<String, Integer> f1 = Integer::parseInt; List<String> box = new ArrayList <>(); Consumer<String> f2 = box::add; BiFunction<String, String, Boolean> f3 = String::startsWith; Supplier<List<String>> f4 = ArrayList::new ; Function<String, StringBuilder> f5 = StringBuilder::new ; System.out.println(f1.apply("2048" ) + 1 ); f2.accept("hello" ); System.out.println(f3.apply("hello" , "he" )); System.out.println(f4.get().size()); } }
形式
语法
等价 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 与匿名内部类的本质差异 这是面试高频题,差异可以归纳为四点:
this 语义不同 。匿名内部类是一个独立的类,其中的 this 指向匿名类自身;Lambda 不是一个类,它没有自己的 this ,Lambda 体内的 this 指向外围对象 (即定义它的那个对象)。
类文件数量不同 。匿名内部类编译后会生成 Outer$1.class 这样的独立 class 文件;Lambda 不会产生新的 class 文件。
字节码指令不同 。匿名内部类是 new + invokespecial(Java 7 及以前的标准做法);Lambda 是 invokedynamic。
捕获语义不同 。匿名内部类可以捕获并修改外部变量(Java 允许访问 effectively final 的局部变量,内部类字段可写),Lambda 只能读取 effectively final。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ThisSemantics { private String name = "outer" ; void anonymous () { Runnable r = new Runnable () { private String name = "anonymous" ; @Override public void run () { System.out.println(this .name); } }; r.run(); } void lambda () { Runnable r = () -> System.out.println(this .name); r.run(); } public static void main (String[] args) { new ThisSemantics ().anonymous(); new ThisSemantics ().lambda(); } }
早期有一种误解:”Lambda 就是匿名内部类的语法糖”。这是错的 。JDK 8 的设计者刻意避开了为每个 Lambda 生成内部类,原因是内部类方案有三个硬伤:类文件膨胀、类加载开销大、未来无法替换实现策略。
真正的实现是 invokedynamic(indy) ,这条指令原本为动态语言(JRuby、Groovy)在 JVM 上运行而设计(JSR-292,Java 7 引入),Lambda 是它在 Java 语言中的第一个大规模应用。完整链路如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import java.lang.invoke.*;import java.util.function.*;public class LambdaMetafactoryDemo { public static void main (String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType samType = MethodType.methodType(IntBinaryOperator.class); MethodType implType = MethodType.methodType(int .class, int .class, int .class); MethodHandle impl = lookup.findStatic(LambdaMetafactoryDemo.class, "lambda$main$0" , implType); MethodType instantiatedType = MethodType.methodType(int .class, int .class, int .class); CallSite site = LambdaMetafactory.metafactory( lookup, "applyAsInt" , samType, instantiatedType, impl, instantiatedType); IntBinaryOperator op = (IntBinaryOperator) site.getTarget().invoke(); System.out.println(op.applyAsInt(6 , 7 )); } private static int lambda$main$0 (int a, int b) { return a * b; } }
编译器在编译期做三件事:
把 Lambda 体脱糖(desugar) 成一个私有方法,非捕获型编译为 private static 方法,捕获型编译为 private 实例方法(捕获的变量变成参数或字段)。
在类文件中写入一条 invokedynamic 指令,其引导方法(Bootstrap Method) 指向 LambdaMetafactory.metafactory,常量池里带上函数式接口的方法名、方法签名、实现方法的 MethodHandle。
生成 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 个接口,但只需记住四个”根接口”,其余都是它们的变体:
接口
抽象方法
含义
常见派生
Supplier<T>
T get()
无入参、有返回值(生产者)
IntSupplier、BooleanSupplier
Consumer<T>
void accept(T)
有入参、无返回值(消费者)
BiConsumer<T,U>、IntConsumer、ObjIntConsumer<T>
Function<T,R>
R apply(T)
一入参、一返回值(转换器)
BiFunction<T,U,R>、UnaryOperator<T>、IntFunction<R>、IntToLongFunction
Predicate<T>
boolean test(T)
一入参、返回布尔(判定器)
BiPredicate<T,U>、IntPredicate
命名规律要记牢:Bi 表示两个入参;Int/Long/Double 前缀表示入参是基本类型(避免装箱);ToInt/ToLong/ToDouble 后缀表示返回值是基本类型;UnaryOperator 是 Function<T,T> 的特化,BinaryOperator 是 BiFunction<T,T,T> 的特化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.function.*;public class FunctionPkgDemo { public static void main (String[] args) { Predicate<String> notEmpty = s -> s != null && !s.isEmpty(); Function<String, Integer> toLen = String::length; UnaryOperator<Integer> square = x -> x * x; BinaryOperator<Integer> max = Integer::max; IntFunction<String> intToStr = i -> "编号" + i; BiFunction<Integer, Integer, String> fmt = (a, b) -> a + "/" + b; System.out.println(notEmpty.test("java" )); System.out.println(toLen.apply("java" )); System.out.println(square.apply(9 )); System.out.println(max.apply(3 , 7 )); System.out.println(intToStr.apply(17 )); System.out.println(fmt.apply(1 , 2 )); } }
3.2 自定义函数式接口与 @FunctionalInterface @FunctionalInterface 注解不是必需的,但它有两个价值:编译期保护 (一旦你加了第二个抽象方法就报错)和自文档化 。注意它只约束”抽象方法数量”,默认方法与静态方法不计入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.util.function.Function;@FunctionalInterface public interface CheckedFunction <T, R> { R apply (T t) throws Exception; default <V> CheckedFunction<T, V> andThen (CheckedFunction<R, V> after) { return t -> after.apply(apply(t)); } static <T, R> Function<T, R> unchecked (CheckedFunction<T, R> f) { return t -> { try { return f.apply(t); } catch (Exception e) { throw new RuntimeException (e); } }; } }
3.3 函数组合:andThen / compose / negate / and / or 组合是函数式编程的核心武器:把小函数拼成大函数,避免层层嵌套。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import java.util.function.*;import java.util.*;import java.util.stream.*;public class ComposeDemo { public static void main (String[] args) { Function<Integer, Integer> times2 = x -> x * 2 ; Function<Integer, Integer> plus1 = x -> x + 1 ; Function<Integer, Integer> f1 = times2.andThen(plus1); Function<Integer, Integer> f2 = times2.compose(plus1); System.out.println(f1.apply(3 )); System.out.println(f2.apply(3 )); Predicate<String> startWithA = s -> s.startsWith("A" ); Predicate<String> lenGt3 = s -> s.length() > 3 ; List<String> data = List.of("Apple" , "Ant" , "Banana" , "Ax" ); System.out.println(data.stream().filter(startWithA.and(lenGt3)).toList()); System.out.println(data.stream().filter(startWithA.or(lenGt3)).toList()); System.out.println(data.stream().filter(startWithA.negate()).toList()); Consumer<String> log = s -> System.out.println("[log] " + s); Consumer<String> save = s -> System.out.println("[db ] " + s); data.forEach(log.andThen(save)); } }
3.4 重构实战:把命令式代码改写为函数式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import java.util.*;import java.util.stream.*;import java.math.BigDecimal;class Order { private final String id; private final BigDecimal amount; private final String status; Order(String id, String amount, String status) { this .id = id; this .amount = new BigDecimal (amount); this .status = status; } public String getId () { return id; } public BigDecimal getAmount () { return amount; } public String getStatus () { return status; } } public class RefactorDemo { static List<String> before (List<Order> orders) { List<Order> filtered = new ArrayList <>(); for (Order o : orders) { if (o.amount.compareTo(new BigDecimal ("1000" )) > 0 && "PAID" .equals(o.status)) { filtered.add(o); } } filtered.sort(new Comparator <Order>() { @Override public int compare (Order a, Order b) { return b.amount.compareTo(a.amount); } }); List<String> ids = new ArrayList <>(); int n = Math.min(3 , filtered.size()); for (int i = 0 ; i < n; i++) { ids.add(filtered.get(i).id); } return ids; } static List<String> after (List<Order> orders) { return orders.stream() .filter(o -> o.amount.compareTo(new BigDecimal ("1000" )) > 0 ) .filter(o -> "PAID" .equals(o.status)) .sorted(Comparator.comparing(Order::getAmount).reversed()) .limit(3 ) .map(Order::getId) .toList(); } public static void main (String[] args) { List<Order> orders = List.of( new Order ("A01" , "1200" , "PAID" ), new Order ("A02" , "800" , "PAID" ), new Order ("A03" , "3000" , "PAID" ), new Order ("A04" , "5000" , "UNPAID" ), new Order ("A05" , "2000" , "PAID" )); System.out.println(before(orders)); System.out.println(after(orders)); } }
四、Stream API 深入 4.1 Stream 与集合的本质区别
维度
集合(Collection)
流(Stream)
存储
存储全部元素,是内存中的数据结构
不存储数据,按需从源头拉取
求值
急切(eager):add 时元素已在
惰性(lazy):只有终端操作才触发计算
遍历次数
可无限次遍历
一次性 ,消费后即关闭,再调用抛 IllegalStateException
遍历方式
外部迭代(for / iterator,用户控制)
内部迭代(库控制,可并行、可优化)
数据规模
有限
可以是无限流(iterate、generate)
关注点
数据本身
对数据的计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.stream.*;import java.util.*;public class StreamOnceDemo { public static void main (String[] args) { Stream<String> s = Stream.of("a" , "b" , "c" ); System.out.println(s.count()); try { s.forEach(System.out::println); } catch (IllegalStateException e) { System.out.println("错误:" + e.getMessage()); } Supplier<Stream<String>> supplier = () -> Stream.of("a" , "b" , "c" ); System.out.println(supplier.get().count()); supplier.get().forEach(System.out::println); } }
4.2 创建 Stream 的六种方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.io.*;import java.nio.file.*;import java.util.*;import java.util.stream.*;import java.math.BigInteger;public class CreateStreamDemo { public static void main (String[] args) throws IOException { Stream<String> s1 = List.of("a" , "b" ).stream(); IntStream s2 = Arrays.stream(new int []{1 , 2 , 3 }); Stream<Integer> s2b = Arrays.stream(new Integer []{1 , 2 , 3 }); Stream<String> s3 = Stream.of("x" , "y" ); Stream<String> s3b = Stream.ofNullable(null ); Stream<Integer> s4 = Stream.iterate(1 , n -> n * 2 ).limit(5 ); Stream<BigInteger> fib = Stream.iterate(new BigInteger []{BigInteger.ZERO, BigInteger.ONE}, a -> new BigInteger []{a[1 ], a[0 ].add(a[1 ])}) .map(a -> a[0 ]).limit(10 ); Stream<Double> randoms = Stream.generate(Math::random).limit(3 ); try (Stream<String> lines = Files.lines(Path.of("pom.xml" ))) { long n = lines.filter(l -> l.contains("java" )).count(); System.out.println("包含 java 的行数:" + n); } int sum = IntStream.rangeClosed(1 , 100 ).sum(); System.out.println(s4.toList() + " " + fib.toList()); System.out.println(randoms.toList().size() + " " + sum); } }
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 收集结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import java.util.stream.*;import java.util.*;public class LazyDemo { public static void main (String[] args) { System.out.println("=== 没有终端操作,中间操作一行都不执行 ===" ); Stream.of("a1" , "a2" , "b1" , "c2" , "c1" ) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a" ); }); System.out.println("=== 加上终端操作后,元素逐个穿过流水线 ===" ); Stream.of("a1" , "a2" , "b1" , "c2" , "c1" ) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("c" ); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s)); System.out.println("=== 短路优化:limit(2) 让流水线提前结束 ===" ); Stream.of("a1" , "a2" , "a3" , "a4" ) .peek(s -> System.out.println("peek: " + s)) .limit(2 ) .toList(); } }
peek 不执行是最经典的踩坑:很多人写完 stream.peek(System.out::println) 发现控制台什么都没有,就以为 Stream “坏了”。真相是 peek 是中间操作,必须接终端操作。另外在 limit/count 这类短路场景下,JIT 与 Stream 框架可能判定 peek 的结果无人使用而整个跳过 ,所以 peek 只适合调试,绝不要在里面写业务逻辑(count() 在 JDK 9+ 还会直接跳过中间操作优化)。
4.5 常用操作与 Collectors 全家桶 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import java.util.*;import java.util.stream.*;import java.math.BigDecimal;record Order (String id, String category, BigDecimal amount, String status) {}public class CollectorsDemo { public static void main (String[] args) { List<Order> orders = List.of( new Order ("1" , "数码" , new BigDecimal ("2999" ), "PAID" ), new Order ("2" , "数码" , new BigDecimal ("899" ), "PAID" ), new Order ("3" , "图书" , new BigDecimal ("59" ), "PAID" ), new Order ("4" , "图书" , new BigDecimal ("129" ), "UNPAID" ), new Order ("5" , "服饰" , new BigDecimal ("399" ), "PAID" ), new Order ("6" , "数码" , new BigDecimal ("5999" ), "PAID" )); List<String> ids = orders.stream().map(Order::id).toList(); Set<String> cats = orders.stream().map(Order::category).collect(Collectors.toSet()); LinkedList<String> linked = orders.stream().map(Order::id) .collect(Collectors.toCollection(LinkedList::new )); String joined = orders.stream().map(Order::id).collect(Collectors.joining(", " , "[" , "]" )); System.out.println(joined); Map<String, Order> byId = orders.stream() .collect(Collectors.toMap(Order::id, o -> o, (a, b) -> a)); Map<String, List<Order>> byCat = orders.stream() .collect(Collectors.groupingBy(Order::category)); Map<String, Long> countByCat = orders.stream() .collect(Collectors.groupingBy(Order::category, Collectors.counting())); Map<String, BigDecimal> sumByCat = orders.stream() .collect(Collectors.groupingBy(Order::category, Collectors.reducing(BigDecimal.ZERO, Order::amount, BigDecimal::add))); Map<String, IntSummaryStatistics> statByCat = orders.stream() .collect(Collectors.groupingBy(Order::category, Collectors.summarizingInt(o -> o.amount.intValue()))); System.out.println("数码: " + statByCat.get("数码" )); Map<String, Map<String, List<Order>>> multi = orders.stream() .collect(Collectors.groupingBy(Order::status, Collectors.groupingBy(Order::category))); Map<Boolean, List<Order>> paidPartition = orders.stream() .collect(Collectors.partitioningBy(o -> "PAID" .equals(o.status))); Map<String, Optional<Order>> topByCat = orders.stream() .collect(Collectors.groupingBy(Order::category, Collectors.maxBy(Comparator.comparing(Order::amount)))); System.out.println(topByCat); } }
4.5.1 自定义 Collector Collector<T, A, R> 有五个要素:supplier(创建可变容器)、accumulator(累加元素)、combiner(并行时合并容器)、finisher(容器转结果)、characteristics(特性标志,影响框架能否优化)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.*;import java.util.stream.*;public class CustomCollectorDemo { static <T extends Comparable <? super T>> Collector<T, ?, List<T>> toSortedImmutableList() { return Collector.of( ArrayList::new , List::add, (left, right) -> { left.addAll(right); return left; }, list -> { list.sort(null ); return Collections.unmodifiableList(list); }, Collector.Characteristics.UNORDERED ); } public static void main (String[] args) { List<String> result = Stream.of("pear" , "apple" , "orange" , "banana" ) .collect(toSortedImmutableList()); System.out.println(result); } }
4.6 flatMap 的扁平化思维 map 是”一对一的盒子套盒子”,flatMap 是”把盒子拆开摊平”。判断标准:如果 map 之后得到的是 Stream<Stream<T>> 或 Stream<List<T>>,就该用 flatMap 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import java.util.*;import java.util.stream.*;public class FlatMapDemo { public static void main (String[] args) { List<List<String>> nested = List.of( List.of("Java" , "Kotlin" ), List.of("Python" , "Go" ), List.of("Rust" )); List<List<String>> wrong = nested.stream().map(l -> l).toList(); List<String> flat = nested.stream().flatMap(Collection::stream).toList(); System.out.println(flat); List<String> sentences = List.of("Hello world" , "Hello java" , "java stream" ); List<String> words = sentences.stream() .flatMap(s -> Arrays.stream(s.toLowerCase().split(" " ))) .distinct() .sorted() .toList(); System.out.println(words); List<String> colors = List.of("红" , "蓝" ); List<String> sizes = List.of("S" , "L" ); List<String> product = colors.stream() .flatMap(c -> sizes.stream().map(s -> c + "-" + s)) .toList(); System.out.println(product); int [] boxed = IntStream.of(1 , 2 ).flatMap(i -> IntStream.range(0 , i)).toArray(); System.out.println(Arrays.toString(boxed)); } }
4.7 Stream 的坑与避坑指南 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import java.util.*;import java.util.stream.*;import java.util.concurrent.*;public class StreamPitfallDemo { public static void main (String[] args) throws Exception { List<String> dup = List.of("a" , "a" , "b" ); try { dup.stream().collect(Collectors.toMap(s -> s, String::length)); } catch (IllegalStateException e) { System.out.println("坑2:" + e.getMessage()); } Map<String, Integer> ok = dup.stream() .collect(Collectors.toMap(s -> s, String::length, Integer::sum)); long boxedSum = IntStream.range(0 , 10_000_000 ).boxed().mapToLong(i -> i).sum(); long rawSum = LongStream.range(0 , 10_000_000 ).sum(); System.out.println("坑3:" + boxedSum + " " + rawSum); ForkJoinPool custom = new ForkJoinPool (4 ); custom.submit(() -> IntStream.range(0 , 10 ).parallel().forEach(i -> System.out.print(i + " " )) ).get(); custom.shutdown(); IntStream.range(0 , 10 ).parallel().forEach(i -> System.out.print(i + " " )); System.out.println(); IntStream.range(0 , 10 ).parallel().forEachOrdered(i -> System.out.print(i + " " )); System.out.println(); System.out.println("坑2修复:" + ok); } }
4.8 parallelStream 的拆分策略与 Spliterator 并行流的底层是 Fork/Join 框架 (本系列第(十三)篇讲过线程池,这里只聚焦拆分逻辑):
数据源被包装成一个 Spliterator(可拆分迭代器) ,它除了 tryAdvance(遍历)还有 trySplit(把自己一分为二)。
框架递归调用 trySplit,直到子任务小到不值得再拆(阈值约 1024 个元素),形成一棵任务树。
每个叶子任务在 ForkJoinPool 的工作线程上执行,通过 work-stealing 调度。
每个子任务独立执行完整的流水线(各自的 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<T> 的设计初衷是作为方法返回值 ,明确告诉调用者”这里可能没有值”。它不应该 用作类的字段(Serializable 都不支持)、方法参数(会造成调用方必须包装的负担)或集合元素(用空集合表示无元素即可,别用 List<Optional<T>>)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import java.util.*;public class OptionalBasic { static Optional<String> findNameById (int id) { return id > 0 ? Optional.of("user" + id) : Optional.empty(); } public static void main (String[] args) { Optional<String> a = Optional.of("java" ); Optional<String> b = Optional.ofNullable(null ); Optional<String> c = Optional.empty(); if (a.isPresent()) { System.out.println(a.get().toUpperCase()); } a.ifPresent(s -> System.out.println(s.toUpperCase())); a.ifPresentOrElse(s -> System.out.println("有值:" + s), () -> System.out.println("无值" )); } }
5.2 三大取值方法:orElse / orElseGet / orElseThrow 的区别 这是面试必考点,核心差异在求值时机 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import java.util.*;public class OrElseDemo { static String defaultValue () { System.out.println(" >> defaultValue() 被调用了" ); return "默认值" ; } public static void main (String[] args) { Optional<String> present = Optional.of("真实值" ); Optional<String> empty = Optional.empty(); System.out.println("1) orElse(有值时):" ); System.out.println(" 结果 = " + present.orElse(defaultValue())); System.out.println("2) orElseGet(有值时):" ); System.out.println(" 结果 = " + present.orElseGet(OrElseDemo::defaultValue)); System.out.println("3) orElseGet(空值时):" ); System.out.println(" 结果 = " + empty.orElseGet(OrElseDemo::defaultValue)); System.out.println("4) orElseThrow(空值时抛自定义异常):" ); try { empty.orElseThrow(() -> new IllegalStateException ("用户不存在" )); } catch (IllegalStateException e) { System.out.println(" 捕获:" + e.getMessage()); } System.out.println("5) orElseThrow() 无参版(JDK 10+,抛 NoSuchElementException):" ); System.out.println(" " + present.orElseThrow()); } }
结论 :默认值是常量或已存在的变量时用 orElse;默认值需要计算 (查库、拼字符串、构造对象)时务必用 orElseGet,否则白白付出计算代价。
5.3 Optional 方法速查表
方法
JDK
作用
备注
of / ofNullable / empty
8
创建
of(null) 直接 NPE
isPresent / isEmpty
8 / 11
判断是否有值
isEmpty 是 JDK 11 新增
get
8
取值
空值时抛异常,尽量少用
orElse / orElseGet
8
兜底值
前者急切、后者惰性
orElseThrow() / orElseThrow(Supplier)
10 / 8
兜底异常
无参版抛 NoSuchElementException
ifPresent / ifPresentOrElse
8 / 9
消费
返回 void
map / flatMap
8
转换
flatMap 避免 Optional<Optional<T>>
filter
8
条件过滤
不满足则变 empty
stream
9
转 Stream
空则为空流,可无缝接 Stream 链
or
9
链式兜底
a.or(() -> b)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import java.util.*;import java.util.stream.*;public class OptionalChain { static Optional<String> getCity () { return Optional.of(" HangZhou " ); } static Optional<Integer> parse (String s) { try { return Optional.of(Integer.parseInt(s)); } catch (NumberFormatException e) { return Optional.empty(); } } public static void main (String[] args) { String city = getCity() .map(String::strip) .filter(s -> !s.isEmpty()) .map(String::toUpperCase) .orElse("UNKNOWN" ); System.out.println(city); Optional<Integer> nested = getCity().flatMap(OptionalChain::parse); List<Optional<String>> list = List.of(Optional.of("a" ), Optional.empty(), Optional.of("b" )); List<String> flat = list.stream().flatMap(Optional::stream).toList(); System.out.println(flat); String v = Optional.<String>empty() .or(() -> Optional.of("第二备选" )) .orElse("最终兜底" ); System.out.println(v); } }
5.4 与持久层查询的配合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public interface UserMapper { Optional<User> findById (Long id) ; List<User> findByStatus (String status) ; } public interface UserRepository extends JpaRepository <User, Long> { Optional<User> findByEmail (String email) ; } class UserService { private final UserMapper mapper; UserService(UserMapper mapper) { this .mapper = mapper; } User requireUser (Long id) { return mapper.findById(id) .orElseThrow(() -> new IllegalArgumentException ("用户不存在, id=" + id)); } static class User {} }
Optional 误用清单 :① 用 Optional 作为字段或参数;② isPresent()+get() 的老式写法;③ 在 orElse 里放高开销计算;④ 集合查询返回 Optional<List<T>>(应返回空集合);⑤ 用 Optional.of(可能为 null 的值)(应用 ofNullable);⑥ 在 Lambda 里创建 Optional 只为调用 get();⑦ 把 Optional 当 null 一样 return null 回去(比不用 Optional 还糟)。
六、JDK 9-11:工程实用性的飞跃 6.1 集合工厂方法与不可变集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.*;public class FactoryMethodDemo { public static void main (String[] args) { 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 ); Map<String, Integer> map2 = Map.ofEntries(Map.entry("a" , 1 ), Map.entry("b" , 2 )); List<String> mutable = new ArrayList <>(List.of("a" , "b" )); List<String> copy = List.copyOf(mutable); System.out.println(list + " " + set + " " + map + " " + copy); } }
6.2 Stream 增强与 String 新方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.util.*;import java.util.stream.*;public class Jdk9StreamDemo { public static void main (String[] args) { System.out.println(Stream.of(1 , 2 , 3 , 4 , 1 , 2 ).takeWhile(i -> i < 4 ).toList()); System.out.println(Stream.of(1 , 2 , 3 , 4 , 1 , 2 ).dropWhile(i -> i < 4 ).toList()); System.out.println(Stream.iterate(1 , i -> i < 20 , i -> i * 2 ).toList()); System.out.println(Stream.ofNullable(null ).count()); String s = " Hello\nJava\r\n \n" ; System.out.println(s.isBlank()); System.out.println(s.strip()); System.out.println(s.stripLeading()); System.out.println(s.lines().toList()); System.out.println("ab" .repeat(3 )); System.out.println(" " .isBlank()); } }
6.3 var 局部变量类型推断的使用边界 var 是 Java 10 引入的,只用于局部变量 ,编译期推断出确切类型后写入字节码,Java 依然是强类型语言 ,不存在运行期动态类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.util.*;public class VarDemo { public static void main (String[] args) { var name = "java" ; var list = new ArrayList <String>(); try (var reader = new java .io.BufferedReader(new java .io.StringReader("hi" ))) { System.out.println(reader.readLine()); } catch (Exception e) { } for (var s : List.of("a" , "b" )) { System.out.println(s.toUpperCase()); } } }
使用建议 :当类型在右侧已经明确写出(new ArrayList<String>())或类型名极长(Map<String, List<OrderSummary>>)时用 var 能显著提升可读性;当初始化表达式类型不明显(var result = service.query())时不要用 var,那会让读代码的人必须跳到方法签名才知道类型。
6.4 HttpClient:JDK 11 正式版 java.net.http.HttpClient 取代了老旧的 HttpURLConnection,支持 HTTP/2、WebSocket、同步/异步、响应式 Body 处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import java.net.URI;import java.net.http.*;import java.time.Duration;import java.util.concurrent.CompletableFuture;public class HttpClientDemo { public static void main (String[] args) throws Exception { HttpClient client = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(5 )) .followRedirects(HttpClient.Redirect.NORMAL) .build(); HttpRequest get = HttpRequest.newBuilder() .uri(URI.create("https://httpbin.org/get" )) .timeout(Duration.ofSeconds(10 )) .header("Accept" , "application/json" ) .GET() .build(); HttpResponse<String> resp = client.send(get, HttpResponse.BodyHandlers.ofString()); System.out.println("状态码:" + resp.statusCode()); System.out.println("响应体:" + resp.body()); CompletableFuture<HttpResponse<String>> future = client.sendAsync(get, HttpResponse.BodyHandlers.ofString()); future.thenApply(HttpResponse::body) .thenAccept(b -> System.out.println("异步长度:" + b.length())) .join(); HttpRequest post = HttpRequest.newBuilder() .uri(URI.create("https://httpbin.org/post" )) .header("Content-Type" , "application/json" ) .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"java\"}" )) .build(); System.out.println(client.send(post, HttpResponse.BodyHandlers.ofString()).statusCode()); } }
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 表达式用 -> 箭头语法彻底解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class SwitchExprDemo { static int oldDaysInMonth (int month, int year) { int days; switch (month) { case 1 : case 3 : case 5 : case 7 : case 8 : case 10 : case 12 : days = 31 ; break ; case 4 : case 6 : case 9 : case 11 : days = 30 ; break ; case 2 : days = (year % 4 == 0 && year % 100 != 0 ) || year % 400 == 0 ? 29 : 28 ; break ; default : throw new IllegalArgumentException ("非法月份: " + month); } return days; } static int newDaysInMonth (int month, int year) { return switch (month) { case 1 , 3 , 5 , 7 , 8 , 10 , 12 -> 31 ; case 4 , 6 , 9 , 11 -> 30 ; case 2 -> (year % 4 == 0 && year % 100 != 0 ) || year % 400 == 0 ? 29 : 28 ; default -> throw new IllegalArgumentException ("非法月份: " + month); }; } static String describe (int score) { return switch (score / 10 ) { case 10 , 9 -> "优秀" ; case 8 , 7 -> "良好" ; case 6 -> { System.out.println("及格线边缘" ); yield "及格" ; } default -> "不及格" ; }; } public static void main (String[] args) { System.out.println(newDaysInMonth(2 , 2024 )); System.out.println(describe(65 )); } }
7.2 文本块 Text Blocks(JDK 15 转正) 文本块用三重双引号包裹,解决 SQL/JSON/HTML 拼接的地狱。编译期会做两件事:去掉每行末尾的 incidental 空白 (按最小缩进对齐)和规范化换行符 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class TextBlockDemo { public static void main (String[] args) { String oldJson = "{\n" + " \"name\": \"java\",\n" + " \"version\": 21\n" + "}" ; String json = """ { "name": "java", "version": 21, "features": ["record", "sealed", "virtual-thread"] } """ ; String sql = """ SELECT u.id, u.name, COUNT(o.id) AS cnt FROM t_user u LEFT JOIN t_order o ON o.user_id = u.id WHERE u.status = 1 GROUP BY u.id, u.name ORDER BY cnt DESC """ ; String withTrailingSpace = """ line1\s line2 """ ; String tpl = """ <h1>%s</h1> <p>共 %d 条记录</p> """ .formatted("报表" , 42 ); System.out.println(json); System.out.println(sql.split("\n" )[0 ]); System.out.println(tpl); } }
7.3 instanceof 模式匹配(JDK 16 转正) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import java.util.*;public class InstanceofPatternDemo { static String oldWay (Object o) { if (o instanceof String) { String s = (String) o; if (s.length() > 5 ) { return "长字符串:" + s.toUpperCase(); } return "短字符串:" + s; } return "非字符串" ; } static String newWay (Object o) { if (o instanceof String s && s.length() > 5 ) { return "长字符串:" + s.toUpperCase(); } if (o instanceof String s) { return "短字符串:" + s; } if (!(o instanceof Integer i)) { return "非整数" ; } return "整数:" + (i * 2 ); } static boolean safeEquals (Object a, Object b) { return (a instanceof String sa) && (b instanceof String sb) && sa.equals(sb); } public static void main (String[] args) { System.out.println(newWay("hello world" )); System.out.println(newWay(21 )); } }
7.4 Record 记录类(JDK 16 转正) Record 是不可变透明载体(transparent carrier) :你只声明组件(components),编译器自动生成私有 final 字段、公共访问器(name() 而非 getName())、规范构造器、equals/hashCode/toString。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import java.util.*;public record Point (int x, int y) { public Point { if (x < 0 || y < 0 ) { throw new IllegalArgumentException ("坐标不能为负: " + x + "," + y); } } public double distanceTo (Point other) { return Math.hypot(x - other.x, y - other.y); } public static Point origin () { return new Point (0 , 0 ); } } class RecordUsage { public static void main (String[] args) { Point p = new Point (3 , 4 ); System.out.println(p.x()); System.out.println(p); System.out.println(p.equals(new Point (3 , 4 ))); System.out.println(Point.class.isRecord()); System.out.println(Arrays.toString(Point.class.getRecordComponents())); try { new Point (-1 , 2 ); } catch (IllegalArgumentException e) { System.out.println("校验生效:" + e.getMessage()); } } }
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) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public sealed interface Shape permits Circle, Rectangle, Triangle { double area () ; } final class Circle implements Shape { private final double radius; Circle(double radius) { this .radius = radius; } public double area () { return Math.PI * radius * radius; } } sealed class Rectangle implements Shape permits Square { private final double w, h; Rectangle(double w, double h) { this .w = w; this .h = h; } public double area () { return w * h; } } non-sealed class Triangle implements Shape { public double area () { return 42 ; } } final class Square extends Rectangle { Square(double side) { super (side, side); } }
编译器与 JVM 的 permits 校验规则 (硬核点):
permits 列出的类必须存在且可访问 ,必须直接 继承/实现该密封类。
每个被允许的子类必须用 final、sealed、non-sealed 三者之一修饰,没有第四个选项 。这是强制的:否则无限继承链又回来了。
密封类与其子类必须在同一个包 (使用无名模块时)或同一个模块 (使用 JPMS 时)。
校验分两层:javac 在编译期检查;class 文件里写入 PermittedSubclasses 属性,JVM 在类加载时校验——即使你手工改字节码绕过 javac,JVM 也会拒绝加载非法子类 。
反射层面新增 Class.isSealed() 与 Class.getPermittedSubclasses()。
7.6 Helpful NullPointerException JDK 14 起(JEP 358),JVM 可以打印出精确到变量名的 NPE 信息,默认开启(也可显式加 -XX:+ShowCodeDetailsInExceptionMessages)。
1 2 3 4 5 6 7 8 9 10 11 public class HelpfulNpeDemo { record User (String name, Address addr) {} record Address (String city) {} public static void main (String[] args) { User u = new User ("tom" , null ); System.out.println(u.addr().city()); } }
这条信息直接告诉你是哪个方法调用的返回值为 null ,在生产日志里价值极高——再也不用靠”第 108 行”猜了。
八、JDK 18-21:模式匹配的终局与并发新纪元 8.1 switch 模式匹配(JDK 21 转正) switch 终于可以按类型分支 ,配合 when 守卫与 Record 解构,Java 具备了完整的模式匹配能力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import java.util.*;public class PatternSwitchDemo { sealed interface Expr permits NumExpr, AddExpr, MulExpr, NegExpr {} record NumExpr (int value) implements Expr {} record AddExpr (Expr left, Expr right) implements Expr {} record MulExpr (Expr left, Expr right) implements Expr {} record NegExpr (Expr inner) implements Expr {} static int evalOld (Expr e) { if (e instanceof NumExpr) return ((NumExpr) e).value(); if (e instanceof AddExpr) return evalOld(((AddExpr) e).left()) + evalOld(((AddExpr) e).right()); if (e instanceof MulExpr) return evalOld(((MulExpr) e).left()) * evalOld(((MulExpr) e).right()); if (e instanceof NegExpr) return -evalOld(((NegExpr) e).inner()); throw new IllegalArgumentException (); } static int eval (Expr e) { return switch (e) { case NumExpr (int v) -> v; case AddExpr (var l, var r) -> eval(l) + eval(r); case MulExpr (var l, var r) -> eval(l) * eval(r); case NegExpr (var i) -> -eval(i); }; } static String classify (Object o) { return switch (o) { case Integer i when i > 100 -> "大整数 " + i; case Integer i -> "小整数 " + i; case String s when s.isBlank() -> "空串" ; case String s -> "字符串 " + s.length(); case null -> "null 值" ; default -> "其它" ; }; } public static void main (String[] args) { Expr expr = new AddExpr (new NumExpr (2 ), new MulExpr (new NumExpr (3 ), new NumExpr (4 ))); System.out.println(eval(expr)); System.out.println(classify(200 )); } }
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.concurrent.*;public class VirtualThreadQuickLook { public static void main (String[] args) throws Exception { Thread v1 = Thread.ofVirtual().name("vt-1" ).start(() -> System.out.println("方式1" )); Thread v2 = Thread.startVirtualThread(() -> System.out.println("方式2" )); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> System.out.println("方式3" )).get(); } v1.join(); v2.join(); long start = System.currentTimeMillis(); try (var exec = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0 ; i < 100_000 ; i++) { exec.submit(() -> { Thread.sleep(10 ); return null ; }); } } System.out.println("10 万虚拟线程耗时:" + (System.currentTimeMillis() - start) + "ms" ); } }
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)的集合”这一长期缺失的抽象:
接口
新增方法
已有实现
SequencedCollection<E>
getFirst()、getLast()、addFirst()、addLast()、removeFirst()、removeLast()、reversed()
List、Deque、LinkedHashSet、SortedSet/LinkedHashSet
SequencedSet<E>
继承上述(去重语义)
LinkedHashSet、SortedSet
SequencedMap<K,V>
firstEntry()、lastEntry()、pollFirstEntry()、pollLastEntry()、putFirst()、putLast()、sequencedKeySet()、reversed()
LinkedHashMap、SortedMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.util.*;public class SequencedDemo { public static void main (String[] args) { SequencedSet<String> set = new LinkedHashSet <>(List.of("a" , "b" , "c" )); System.out.println(set.getFirst() + " / " + set.getLast()); System.out.println(set.reversed()); SequencedMap<String, Integer> map = new LinkedHashMap <>(); map.put("one" , 1 ); map.put("two" , 2 ); System.out.println(map.firstEntry() + " " + map.lastEntry()); System.out.println(map.pollLastEntry()); System.out.println(map); } }
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 8
invokedynamic + LambdaMetafactory
Stream API
Java 8
惰性流水线 + Sink 链
接口默认方法/静态方法
Java 8
接口可带实现
var 局部变量推断
Java 10
仅局部变量,编译期推断
集合工厂 List.of
Java 9
不可变、不接受 null
Optional.stream/isEmpty/or
Java 9/11/9
Optional 增强
HttpClient
Java 11
替代 HttpURLConnection
switch 表达式
Java 14
-> 箭头 + yield
文本块
Java 15
""" 三重引号
instanceof 模式匹配
Java 16
判断与绑定一步
Record
Java 16
不可变透明载体
Stream.toList()
Java 16
返回不可变 List
密封类
Java 17
permits 精确控制继承
Helpful NPE
Java 14
精确到变量名的 NPE
switch 模式匹配
Java 21
类型模式 + 守卫 + 记录解构
虚拟线程
Java 21
M:N 用户态线程
Sequenced Collections
Java 21
getFirst/getLast/reversed
8.7 升级路线建议
还在 Java 8 :先升到 Java 17 ,这是当前生态支持最完整的 LTS(Spring Boot 3.x 强制要求 17+)。迁移重点:模块强封装导致的反射报错(--add-opens)、javax.* → jakarta.* 命名空间切换、构建插件版本升级。
已在 Java 11/17 :评估 Java 21 ,主要收益是虚拟线程与分代 ZGC;风险点主要是 GC 参数与字节码增强工具(Byte Buddy、ASM、CGLIB)的版本兼容。
语言级别采用顺序 :先用 Record 与 switch 表达式(收益最大、风险最小),再上文本块与模式匹配,最后是密封类与预览特性。
永远在 CI 上跑一遍最新非 LTS 版本 的编译与测试,提前半年发现兼容性问题。
8.8 高频面试题十问十答
Lambda 表达式的底层实现是什么? 不是匿名内部类。编译期把 Lambda 体脱糖为一个私有方法(非捕获为 static),在原位置写入 invokedynamic 指令,引导方法是 LambdaMetafactory.metafactory;首次执行时由它用 ASM 在内存中动态生成实现函数式接口的类,并永久链接到调用点。
为什么 Lambda 捕获的局部变量必须是 effectively final? 局部变量在栈帧中,Lambda 可能在其它线程或稍后执行,JVM 只能复制一份值;若允许修改会造成副本与本体不一致。
Lambda 里的 this 指向谁? 指向外围对象 (定义它的那个实例),因为 Lambda 不是类、没有自己的 this。匿名内部类的 this 指向匿名类自身。
Stream 为什么是惰性的? 中间操作只往流水线链表上挂 StatelessOp/StatefulOp 节点,不触发计算;只有终端操作才反向回溯组装 Sink 链,由数据源 Spliterator 推送元素逐个穿过整条链。
Stream 能复用吗? 不能。消费后流即关闭,再次调用抛 IllegalStateException。需要多次遍历就重新创建或用 Supplier<Stream<T>>。
什么情况下不该用 parallelStream? 数据量小、单元素处理很轻、数据源难拆分(LinkedList、IO 流)、任务阻塞(会污染 ForkJoinPool.commonPool())、依赖顺序、有共享可变状态。
Record 和 Lombok @Data 有什么区别? Record 是语言级特性、编译期生成、不可变、访问器无 get 前缀、不能继承与加实例字段;Lombok 是注解处理器、可变、可继承。DTO/值对象用 Record,JPA 实体用 Lombok。
switch 表达式和旧 switch 语句有什么区别? 箭头语法无需 break(无 fall-through)、可作为表达式直接返回值、多值用逗号、case 块用 yield 返回、配合密封类型可省略 default 且编译器能检查穷举性。
orElse 与 orElseGet 的区别? orElse 的参数总会 被求值(急切),orElseGet 的 Supplier 只在值为空时才执行(惰性)。默认值需要计算时必须用 orElseGet。
密封类的子类必须满足什么约束? 必须在 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 反编译本篇的示例代码,亲眼看看编译器到底生成了什么——这是把”知道”变成”理解”的最短路径。