Skip to content

Lambda、Optional 与 Stream

Lambda、函数式接口、Optional、Stream 是 Java 8 之后非常常用的能力。它们能让集合处理、回调、过滤、转换、聚合更简洁,但如果只会写链式调用,不理解原理,也容易写出难排查、性能差、空指针隐藏的问题。

为什么需要 Lambda

在 Lambda 出现前,传递一段行为通常要写匿名内部类。

java
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("执行任务");
    }
};

Lambda 可以把它简化为:

java
Runnable task = () -> System.out.println("执行任务");

Lambda 的核心价值:把“行为”当作参数传递。

Lambda 底层怎么理解

Lambda 不是“匿名内部类换个写法”这么简单。Java 8 的 Lambda 依赖函数式接口,编译器根据目标接口的唯一抽象方法判断参数和返回值。

java
Runnable task = () -> System.out.println("hello");

这段代码能成立,是因为 Runnable 只有一个抽象方法 run()。Lambda 的参数列表和返回值要能匹配这个方法。

运行机制可以这样理解:

mermaid
flowchart TD
    A["源代码 Lambda"] --> B["编译器检查目标函数式接口"]
    B --> C["生成 invokedynamic 调用点"]
    C --> D["运行期 LambdaMetafactory 创建函数对象"]
    D --> E["调用函数式接口方法"]

JDK 8 通常使用 invokedynamicLambdaMetafactory 在运行期链接 Lambda。面试不需要背字节码细节,但要知道:Lambda 的类型来自函数式接口,不是随便一段代码都能赋给变量。

Lambda 捕获变量

Lambda 可以访问外部局部变量,但这个变量必须是 final 或 effectively final。

java
public class LambdaCaptureDemo {
    public static void main(String[] args) {
        String prefix = "订单";

        Runnable task = () -> System.out.println(prefix + "处理");

        // prefix = "支付"; // 编译失败,prefix 被 Lambda 捕获后不能再修改
        task.run();
    }
}

为什么有限制?因为局部变量在线程栈里,方法执行结束后就没了。Lambda 如果要在之后执行,必须捕获一份稳定值。允许外部局部变量随意变化,会让生命周期和并发语义变得混乱。

注意:引用不能变,不代表对象内容不能变。

java
import java.util.ArrayList;
import java.util.List;

public class LambdaSideEffectDemo {
    public static void main(String[] args) {
        List<String> result = new ArrayList<>();
        Runnable task = () -> result.add("A");
        task.run();
        System.out.println(result);
    }
}

这种写法能编译,但要小心副作用。尤其在 Stream 或并发场景中,Lambda 修改外部集合容易引发线程安全和可读性问题。

函数式接口

函数式接口是只有一个抽象方法的接口。

java
@FunctionalInterface
public interface OrderFilter {
    boolean match(Order order);
}

使用:

java
import java.math.BigDecimal;

public class OrderFilterDemo {
    public static void main(String[] args) {
        OrderFilter highAmount = order -> order.getAmount().compareTo(new BigDecimal("1000")) > 0;
    }
}

@FunctionalInterface 不是必须的,但建议加上。它能让编译器帮你检查这个接口是否真的只有一个抽象方法。

函数式接口可以有默认方法和静态方法,只要抽象方法只有一个即可。

java
@FunctionalInterface
public interface OrderFilter {
    boolean match(Order order);

    default OrderFilter and(OrderFilter other) {
        return order -> this.match(order) && other.match(order);
    }
}

这也是 Predicateandornegate 的原因。

常见函数式接口

接口抽象方法含义
Predicate<T>boolean test(T t)判断条件
Function<T,R>R apply(T t)类型转换
Consumer<T>void accept(T t)消费数据
Supplier<T>T get()提供数据

Demo:

java
import java.util.function.Function;
import java.util.function.Predicate;

public class FunctionInterfaceDemo {
    public static void main(String[] args) {
        Predicate<String> hasText = value -> value != null && value.trim().length() > 0;
        Function<String, Integer> length = String::length;

        System.out.println(hasText.test("Java"));
        System.out.println(length.apply("Java"));
    }
}

Stream 是什么

Stream 是对集合数据进行流水线处理的工具。它不存储数据,而是描述数据如何被过滤、转换、聚合。

mermaid
flowchart TD
    A["集合数据"] --> B["stream"]
    B --> C["filter 过滤"]
    C --> D["map 转换"]
    D --> E["sorted 排序"]
    E --> F["collect 收集结果"]

Stream 操作分为两类:

类型示例是否立即执行
中间操作filtermapsorted不立即执行
终止操作collectcountforEach触发执行

Stream 不是集合。集合负责存数据,Stream 负责描述处理流程。一个 Stream 通常只能消费一次:

java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamOnceDemo {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Tom", "Jerry");
        Stream<String> stream = names.stream();

        System.out.println(stream.count());
        // System.out.println(stream.count()); // IllegalStateException
    }
}

原因是 Stream 像一条流水线,终止操作执行后,这条流水线已经被消费完。需要再次处理时,应重新从集合创建 Stream。

Stream 商业 Demo:订单筛选和统计

java
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class OrderStreamDemo {
    public static void main(String[] args) {
        List<Order> orders = Arrays.asList(
                new Order(1L, "PAID", new BigDecimal("99.00")),
                new Order(2L, "PAID", new BigDecimal("1200.00")),
                new Order(3L, "CANCELED", new BigDecimal("50.00"))
        );

        List<Long> highPaidOrderIds = orders.stream()
                .filter(order -> "PAID".equals(order.getStatus()))
                .filter(order -> order.getAmount().compareTo(new BigDecimal("1000")) > 0)
                .map(Order::getId)
                .collect(Collectors.toList());

        System.out.println(highPaidOrderIds);
    }
}

class Order {
    private Long id;
    private String status;
    private BigDecimal amount;

    public Order(Long id, String status, BigDecimal amount) {
        this.id = id;
        this.status = status;
        this.amount = amount;
    }

    public Long getId() {
        return id;
    }

    public String getStatus() {
        return status;
    }

    public BigDecimal getAmount() {
        return amount;
    }
}

读法:从订单列表中,筛选已支付订单,再筛选金额大于 1000 的订单,最后取出订单 ID。

Stream 惰性执行原理

java
import java.util.Arrays;
import java.util.List;

public class StreamLazyDemo {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Tom", "Jerry", "Alice");

        names.stream()
                .filter(name -> {
                    System.out.println("filter " + name);
                    return name.length() > 3;
                });

        System.out.println("没有终止操作,上面的 filter 不会执行");
    }
}

只有加上 collectcountforEach 这类终止操作,流水线才会真正执行。

更重要的是,Stream 会把多个中间操作融合成一次遍历,而不是每个操作都完整遍历一遍集合。

java
import java.util.Arrays;
import java.util.List;

public class StreamPipelineDemo {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Tom", "Jerry", "Alice");

        names.stream()
                .filter(name -> {
                    System.out.println("filter " + name);
                    return name.length() > 3;
                })
                .map(name -> {
                    System.out.println("map " + name);
                    return name.toUpperCase();
                })
                .findFirst()
                .ifPresent(System.out::println);
    }
}

findFirst 是短路终止操作。找到第一个满足条件的元素后,后面的元素可能不会继续处理。

mermaid
flowchart TD
    A["取第 1 个元素"] --> B["执行 filter"]
    B --> C{"是否通过"}
    C -- "否" --> D["取下一个元素"]
    C -- "是" --> E["执行 map"]
    E --> F["findFirst 得到结果"]
    F --> G["短路结束"]

这解释了为什么 Stream 的日志顺序有时不像“先全部 filter,再全部 map”。它是按流水线逐个元素推进。

map 和 flatMap

map 是一对一转换,flatMap 是把嵌套结构摊平。

java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FlatMapDemo {
    public static void main(String[] args) {
        List<List<String>> roles = Arrays.asList(
                Arrays.asList("USER", "ADMIN"),
                Arrays.asList("USER", "AUDITOR")
        );

        List<String> allRoles = roles.stream()
                .flatMap(List::stream)
                .distinct()
                .collect(Collectors.toList());

        System.out.println(allRoles);
    }
}

常用收集器

商业项目中最常用的不是简单 toList,而是分组、映射、求和、转 Map。

java
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CollectorDemo {
    public static void main(String[] args) {
        List<Order> orders = Arrays.asList(
                new Order(1L, "PAID", new BigDecimal("99.00")),
                new Order(2L, "PAID", new BigDecimal("1200.00")),
                new Order(3L, "CANCELED", new BigDecimal("50.00"))
        );

        Map<String, List<Order>> byStatus = orders.stream()
                .collect(Collectors.groupingBy(Order::getStatus));

        Map<Long, Order> byId = orders.stream()
                .collect(Collectors.toMap(Order::getId, order -> order));

        BigDecimal paidTotal = orders.stream()
                .filter(order -> "PAID".equals(order.getStatus()))
                .map(Order::getAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        System.out.println(byStatus.keySet());
        System.out.println(byId.keySet());
        System.out.println(paidTotal);
    }
}

toMap 重复 key 的坑

Collectors.toMap 如果遇到重复 key,默认会抛异常。

java
Map<String, Order> byStatus = orders.stream()
        .collect(Collectors.toMap(Order::getStatus, order -> order)); // 多个 PAID 会报错

需要明确合并规则:

java
Map<String, Order> byStatus = orders.stream()
        .collect(Collectors.toMap(
                Order::getStatus,
                order -> order,
                (oldValue, newValue) -> newValue
        ));

商业项目里,用户 ID、订单号、状态、医院编码等字段是否唯一必须想清楚,不能随便 toMap

Optional 是什么

Optional 用来表达“可能有值,也可能没有值”。它不是为了消灭所有 null,而是让返回值的缺失语义更清楚。

java
import java.util.Optional;

public class OptionalDemo {
    public static Optional<String> findUsername(Long userId) {
        if (userId == null || userId <= 0) {
            return Optional.empty();
        }
        return Optional.of("Tom");
    }

    public static void main(String[] args) {
        String username = findUsername(1L)
                .orElse("匿名用户");
        System.out.println(username);
    }
}

推荐用在方法返回值,不建议把实体字段定义成 Optional

Optional 常见坑

of 和 ofNullable

java
Optional.of("Tom");          // 不能传 null
Optional.ofNullable(null);   // 可以传 null,得到 Optional.empty()

如果值可能为空,用 ofNullable

orElse 和 orElseGet

orElse 的默认值会先计算,orElseGet 只有在没值时才执行 Supplier。

java
import java.util.Optional;

public class OptionalElseDemo {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Tom");

        String value1 = name.orElse(createDefault());
        String value2 = name.orElseGet(() -> createDefault());

        System.out.println(value1);
        System.out.println(value2);
    }

    private static String createDefault() {
        System.out.println("create default");
        return "anonymous";
    }
}

如果默认值需要查数据库、调接口、做复杂计算,应该用 orElseGet

不要直接 get

java
Optional<String> value = Optional.empty();
// value.get(); // NoSuchElementException

更清楚的写法:

java
String username = value.orElseThrow(() -> new IllegalArgumentException("用户不存在"));

Optional 不能替代参数校验,也不能让空指针彻底消失。它只是让“可能没有值”这件事在返回值上更明确。

商业 Demo:采集记录聚合

下面模拟医疗采集平台中按医院统计成功记录数量。

java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CollectRecordDemo {
    public static void main(String[] args) {
        List<CollectRecord> records = Arrays.asList(
                new CollectRecord("H001", "SUCCESS"),
                new CollectRecord("H001", "FAILED"),
                new CollectRecord("H002", "SUCCESS"),
                new CollectRecord("H001", "SUCCESS")
        );

        Map<String, Long> successCountByHospital = records.stream()
                .filter(record -> "SUCCESS".equals(record.getStatus()))
                .collect(Collectors.groupingBy(
                        CollectRecord::getHospitalCode,
                        Collectors.counting()
                ));

        System.out.println(successCountByHospital);
    }
}

class CollectRecord {
    private final String hospitalCode;
    private final String status;

    CollectRecord(String hospitalCode, String status) {
        this.hospitalCode = hospitalCode;
        this.status = status;
    }

    public String getHospitalCode() {
        return hospitalCode;
    }

    public String getStatus() {
        return status;
    }
}

这类场景适合 Stream:数据已经在内存里,处理逻辑是过滤、分组、计数。
不适合 Stream 的场景:每个元素都要查数据库或调远程接口。那样应该先批量查询,或者使用明确的线程池并发模型。

parallelStream 为什么要谨慎

parallelStream 会使用并行执行,但不是所有场景都会更快。

mermaid
flowchart TD
    A["parallelStream"] --> B["拆分任务"]
    B --> C["多个线程执行"]
    C --> D["合并结果"]
    D --> E["返回结果"]

风险:

风险说明
线程池不可控默认使用公共 ForkJoinPool
IO 任务不一定适合阻塞可能拖慢公共线程池
共享变量不安全并行写集合可能出错
数据量小反而慢拆分和合并也有开销

业务系统里,涉及数据库、Redis、HTTP 调用时,不要随便用 parallelStream

更具体地说,parallelStream 默认使用公共 ForkJoinPool.commonPool()。如果你在并行流里做阻塞 IO,可能把公共线程池堵住,影响同 JVM 内其他也依赖公共池的任务。

错误示例:

java
orders.parallelStream()
        .map(order -> remoteClient.query(order.getId()))
        .collect(Collectors.toList());

问题:

  1. 线程池不可控,不知道并发量是否会打爆下游。
  2. 没有统一超时、限流、隔离和监控。
  3. 一个慢接口可能拖住公共池线程。
  4. 异常处理和链路追踪不清晰。

生产里如果确实要并发调用下游,更推荐显式线程池、CompletableFuture、限流和超时控制。

Stream 调试和性能判断

Stream 不一定比 for 快。它的优势是表达“过滤、映射、分组、聚合”的数据处理意图。

判断方式:

场景建议
简单循环、极致性能热点for 循环更直接
内存集合过滤转换Stream 可读性好
复杂业务分支拆成方法,不要硬写长链
每个元素远程调用不要直接 Stream 里调,先批量或显式并发
大数据量统计优先数据库 SQL、ES 聚合或流式处理

如果 Stream 链太长,可以用 peek 临时调试,但不要把 peek 当业务逻辑。

java
orders.stream()
        .peek(order -> System.out.println("before filter " + order.getId()))
        .filter(order -> "PAID".equals(order.getStatus()))
        .peek(order -> System.out.println("after filter " + order.getId()))
        .collect(Collectors.toList());

peek 只有在终止操作触发后才会执行。

常见风险

问题后果建议
Stream 链太长难调试复杂逻辑拆方法
map 里做远程调用性能不可控批量查询或明确并发模型
Optional.get() 直接取没值时报错orElseorElseThrow
滥用 parallelStream线程和性能问题明确评估后再用
Lambda 里修改外部集合并发和可读性问题使用收集器
Stream 被重复消费IllegalStateException需要时重新创建 Stream
toMap key 重复抛异常或覆盖不明确明确唯一性和合并规则
orElse 默认值很重有值时也执行默认值计算使用 orElseGet

线上排查流程

mermaid
flowchart TD
    A["Lambda/Stream 问题"] --> B{"表现是什么"}
    B -- "链路没执行" --> C["检查是否缺少终止操作"]
    B -- "结果少或提前结束" --> D["检查 findFirst、anyMatch 等短路操作"]
    B -- "toMap 报错" --> E["检查 key 是否重复并配置 merge 函数"]
    B -- "CPU 或线程异常" --> F["检查 parallelStream 和公共 ForkJoinPool"]
    B -- "性能变慢" --> G["检查 Stream 中是否做远程调用或慢 SQL"]
    B -- "Optional 报错" --> H["检查是否直接 get 或 of(null)"]

面试常问

Lambda 底层怎么理解?
Lambda 必须依赖函数式接口,编译器根据接口唯一抽象方法检查参数和返回值。JDK 8 通常通过 invokedynamicLambdaMetafactory 在运行期创建函数对象,它的核心价值是把行为作为参数传递。

函数式接口是什么?
函数式接口是只有一个抽象方法的接口,可以用 Lambda 表达式实现。@FunctionalInterface 不是必须的,但建议加上,编译器会帮你检查接口是否仍然满足函数式接口规则。

Stream 为什么是惰性执行?
中间操作只是描述流水线,不会立即遍历数据。只有遇到终止操作,比如 collectcountforEachfindFirst,流水线才真正执行。惰性执行可以融合多个操作,并支持短路。

map 和 flatMap 区别?
map 是一对一转换,一个元素变成另一个元素;flatMap 是把每个元素转换成一个流,再把多个流摊平成一个流,适合处理嵌套集合。

Optional 能彻底解决空指针吗?
不能。Optional 只是让方法返回值的“可能没有值”更明确。它本身也可能被错误使用,比如 Optional 变量设为 null、of(null)、直接 get()。实体字段和 DTO 字段通常不建议使用 Optional。

parallelStream 为什么慎用?
它默认使用公共 ForkJoinPool,线程池隔离、并发量、超时、监控都不够明确。IO 调用、数据库访问、HTTP 请求放进 parallelStream 可能拖慢公共线程池甚至打爆下游。

本章小结

Lambda 让行为可以作为参数传递,函数式接口定义行为形状,Stream 用流水线处理集合,Optional 表达可能缺失的返回值。学习它们不能只追求链式写法,要理解惰性执行、终止操作、副作用和并行流风险。