Skip to content

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)双参
java
@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)); // 3

1.1 Lambda 不是匿名内部类 ​

这是最大的误区。匿名内部类会生成一个 Outer$1.class;而 Lambda 在编译期只是生成一条 invokedynamic 指令,运行时由 LambdaMetafactory 动态生成实现类,并可通过 MethodHandle 直接调用——不生成额外 class 文件,性能更好。

text
// javap -c 反编译后能看到(关键:invokedynamic + LambdaMetafactory)
invokedynamic #7,  0  // InvokeDynamic #0:calc:()LCalculator;
// BootstrapMethods:
//   0: #55 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory

Lambda 的「无状态可共享」

由于靠 invokedynamic 生成,Lambda 表达式在无捕获变量时会被缓存为同一个实例,不像匿名内部类引用外部变量那样每次 new 一个。

二、方法引用 ​

方法引用是 Lambda 的语法糖,四种形式:

java
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::parseInts -> Integer.parseInt(s)
特定对象的实例方法System.out::printlns -> System.out.println(s)
任意对象的实例方法String::lengths -> s.length()
构造器ArrayList::new() -> new ArrayList<>()

三、Stream 流水线:源 → 中间操作 → 终止操作 ​

Stream 的核心是惰性求值:中间操作只记录操作、不执行;只有遇到终止操作才一次遍历把整条流水线跑完。

java
List<String> result = users.stream()          // 源
    .filter(u -> u.getAge() > 18)             // 中间操作(惰性)
    .map(User::getName)                       // 中间操作(惰性)
    .distinct()
    .sorted()
    .collect(Collectors.toList());            // 终止操作(触发)

惰性求值:不写终止操作等于白写

java
list.stream().filter(x -> {
    System.out.println("执行了");   // 只有加终止操作后才会打印
    return true;
});

3.1 无状态 vs 有状态操作 ​

类型操作特点
无状态filter、map、peek每个元素独立处理,可流水线并行
有状态sorted、distinct、limit依赖全部/部分元素,会产生缓冲屏障

sorted 会把所有元素缓冲下来排序,是流水线里的「屏障」——这也是它比 filter 贵得多的原因。

3.2 常用收集器 ​

java
// 分组
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 共享的。

并行流三大坑

  1. 共享 commonPool:某个任务里做了阻塞 IO,会拖垮整个 JVM 的并行流。
  2. 线程不安全:在 forEach 里往普通 ArrayList 添加会丢数据/报错,要用 Collectors.toList() 或并发容器。
  3. 有状态操作 + 顺序依赖:findFirst、limit 在并行下代价高;forEach 不保证顺序(要顺序用 forEachOrdered)。

判断是否用并行流:数据量大、计算密集、无共享可变状态、无阻塞——四条都满足才考虑;否则老实用串行或自己建线程池。

五、最佳实践 ​

  • 默认用串行流,并行流只在压测证明有收益时才用,且私有线程池化(ForkJoinPool 自定义)。
  • map 里别做重 IO/远程调用:Stream 不是异步框架,阻塞会拖慢整条流水线。
  • 避免在 Stream 里做副作用赋值(peek 里改外部变量),用 collect 产出结果。
  • IntStream/LongStream 替代 Stream<Integer>,避免装箱开销。
  • Optional 配合 findFirst/findAny,别直接 .get()。
java
// 原始类型流,避免装箱
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);

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. Lambda 表达式编译后是匿名内部类吗?用 javap -c 看看 invokedynamic 指令。
  2. 为什么 stream().filter(...) 不加终止操作什么都不执行?这体现了什么设计模式?
  3. 用 Stream 把一个 List<User> 按城市分组,并统计每个城市的人数,一次 collect 完成。
  4. 什么情况下 parallelStream() 反而更慢?举两个具体场景。

本节小结

Lambda 靠 invokedynamic + LambdaMetafactory 动态实现,不是匿名内部类;Stream 是惰性流水线,中间操作只记录、终止操作才触发;并行流共享 commonPool,数据大/计算密集/无副作用/无阻塞四条同时满足才值得用。