Lambda 与 Stream API
Java 中级 · Java 进阶
Lambda 与 Stream 是 Java 8 的分水岭,它把「命令式循环」变成「声明式流水线」。但很多人只会 list.stream().filter(...).collect(...) 的套路,讲不清Lambda 编译成了什么、Stream 为什么是惰性的、并行流为什么会踩坑。本篇从字节码和流水线模型讲透。
一、函数式接口:Lambda 的类型
Lambda 必须有明确的目标类型——函数式接口(只有一个抽象方法的接口)。@FunctionalInterface 只是编译期校验,可有可无。
| 接口 | 签名 | 用途 |
|---|---|---|
Function<T,R> | R apply(T) | 映射 |
Consumer<T> | void accept(T) | 消费 |
Supplier<T> | T get() | 生产 |
Predicate<T> | boolean test(T) | 判断 |
UnaryOperator<T> | T apply(T) | 一元运算(Function 特化) |
BiFunction<T,U,R> | R apply(T,U) | 双参 |
@FunctionalInterface
interface Calculator { int calc(int a, int b); }
Calculator add = (a, b) -> a + b; // Lambda 实现了唯一抽象方法
Calculator mul = (a, b) -> a * b;
System.out.println(add.calc(1, 2)); // 31.1 Lambda 不是匿名内部类
这是最大的误区。匿名内部类会生成一个 Outer$1.class;而 Lambda 在编译期只是生成一条 invokedynamic 指令,运行时由 LambdaMetafactory 动态生成实现类,并可通过 MethodHandle 直接调用——不生成额外 class 文件,性能更好。
// javap -c 反编译后能看到(关键:invokedynamic + LambdaMetafactory)
invokedynamic #7, 0 // InvokeDynamic #0:calc:()LCalculator;
// BootstrapMethods:
// 0: #55 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactoryLambda 的「无状态可共享」
由于靠 invokedynamic 生成,Lambda 表达式在无捕获变量时会被缓存为同一个实例,不像匿名内部类引用外部变量那样每次 new 一个。
二、方法引用
方法引用是 Lambda 的语法糖,四种形式:
List<String> list = Arrays.asList("b", "a", "c");
list.forEach(System.out::println); // 1. 静态/实例方法引用
list.stream().map(String::toUpperCase); // 2. 实例方法(以元素为接收者)
list.stream().sorted(String::compareTo); // 3. 任意对象的实例方法
list.stream().map(ArrayList::new); // 4. 构造器引用| 形式 | 语法 | 等价 Lambda |
|---|---|---|
| 静态方法 | Integer::parseInt | s -> Integer.parseInt(s) |
| 特定对象的实例方法 | System.out::println | s -> System.out.println(s) |
| 任意对象的实例方法 | String::length | s -> s.length() |
| 构造器 | ArrayList::new | () -> new ArrayList<>() |
三、Stream 流水线:源 → 中间操作 → 终止操作
Stream 的核心是惰性求值:中间操作只记录操作、不执行;只有遇到终止操作才一次遍历把整条流水线跑完。
List<String> result = users.stream() // 源
.filter(u -> u.getAge() > 18) // 中间操作(惰性)
.map(User::getName) // 中间操作(惰性)
.distinct()
.sorted()
.collect(Collectors.toList()); // 终止操作(触发)惰性求值:不写终止操作等于白写
list.stream().filter(x -> {
System.out.println("执行了"); // 只有加终止操作后才会打印
return true;
});3.1 无状态 vs 有状态操作
| 类型 | 操作 | 特点 |
|---|---|---|
| 无状态 | filter、map、peek | 每个元素独立处理,可流水线并行 |
| 有状态 | sorted、distinct、limit | 依赖全部/部分元素,会产生缓冲屏障 |
sorted 会把所有元素缓冲下来排序,是流水线里的「屏障」——这也是它比 filter 贵得多的原因。
3.2 常用收集器
// 分组
Map<String, List<User>> byCity = users.stream().collect(Collectors.groupingBy(User::getCity));
// 分区(只能二分)
Map<Boolean, List<User>> adult = users.stream().collect(Collectors.partitioningBy(u -> u.getAge() >= 18));
// 映射后分组 + 下游收集器
Map<String, Set<String>> namesByCity = users.stream().collect(
Collectors.groupingBy(User::getCity, Collectors.mapping(User::getName, Collectors.toSet())));
// joining
String cities = users.stream().map(User::getCity).distinct().collect(Collectors.joining(", "));
// toMap 注意 key 冲突(否则抛 IllegalStateException)
Map<Long, User> idMap = users.stream().collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a));四、并行流:为什么容易踩坑
parallelStream() 底层用 ForkJoinPool.commonPool(),默认线程数 = CPU 核数 - 1,且是全 JVM 共享的。
并行流三大坑
- 共享 commonPool:某个任务里做了阻塞 IO,会拖垮整个 JVM 的并行流。
- 线程不安全:在
forEach里往普通ArrayList添加会丢数据/报错,要用Collectors.toList()或并发容器。 - 有状态操作 + 顺序依赖:
findFirst、limit在并行下代价高;forEach不保证顺序(要顺序用forEachOrdered)。
判断是否用并行流:数据量大、计算密集、无共享可变状态、无阻塞——四条都满足才考虑;否则老实用串行或自己建线程池。
五、最佳实践
- 默认用串行流,并行流只在压测证明有收益时才用,且私有线程池化(
ForkJoinPool自定义)。 map里别做重 IO/远程调用:Stream 不是异步框架,阻塞会拖慢整条流水线。- 避免在 Stream 里做副作用赋值(
peek里改外部变量),用collect产出结果。 IntStream/LongStream替代Stream<Integer>,避免装箱开销。Optional配合findFirst/findAny,别直接.get()。
// 原始类型流,避免装箱
int sum = IntStream.rangeClosed(1, 100).sum();
// 危险:findFirst 在空流上 get 会抛 NoSuchElementException
Optional<User> first = users.stream().filter(u -> u.getAge() > 18).findFirst();
first.ifPresent(System.out::println);📚 官方文档与参考资料
- Java SE 21 · java.util.stream 包
- Java 教程 · Aggregate Operations
- Java SE 21 · java.util.function
- Oracle · Parallelism(并行流官方说明)
✏️ 练习与自测
- Lambda 表达式编译后是匿名内部类吗?用
javap -c看看invokedynamic指令。 - 为什么
stream().filter(...)不加终止操作什么都不执行?这体现了什么设计模式? - 用 Stream 把一个
List<User>按城市分组,并统计每个城市的人数,一次collect完成。 - 什么情况下
parallelStream()反而更慢?举两个具体场景。
本节小结
Lambda 靠 invokedynamic + LambdaMetafactory 动态实现,不是匿名内部类;Stream 是惰性流水线,中间操作只记录、终止操作才触发;并行流共享 commonPool,数据大/计算密集/无副作用/无阻塞四条同时满足才值得用。