Lambda、Optional 与 Stream
Lambda、函数式接口、Optional、Stream 是 Java 8 之后非常常用的能力。它们能让集合处理、回调、过滤、转换、聚合更简洁,但如果只会写链式调用,不理解原理,也容易写出难排查、性能差、空指针隐藏的问题。
为什么需要 Lambda
在 Lambda 出现前,传递一段行为通常要写匿名内部类。
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("执行任务");
}
};Lambda 可以把它简化为:
Runnable task = () -> System.out.println("执行任务");Lambda 的核心价值:把“行为”当作参数传递。
Lambda 底层怎么理解
Lambda 不是“匿名内部类换个写法”这么简单。Java 8 的 Lambda 依赖函数式接口,编译器根据目标接口的唯一抽象方法判断参数和返回值。
Runnable task = () -> System.out.println("hello");这段代码能成立,是因为 Runnable 只有一个抽象方法 run()。Lambda 的参数列表和返回值要能匹配这个方法。
运行机制可以这样理解:
flowchart TD
A["源代码 Lambda"] --> B["编译器检查目标函数式接口"]
B --> C["生成 invokedynamic 调用点"]
C --> D["运行期 LambdaMetafactory 创建函数对象"]
D --> E["调用函数式接口方法"]JDK 8 通常使用 invokedynamic 和 LambdaMetafactory 在运行期链接 Lambda。面试不需要背字节码细节,但要知道:Lambda 的类型来自函数式接口,不是随便一段代码都能赋给变量。
Lambda 捕获变量
Lambda 可以访问外部局部变量,但这个变量必须是 final 或 effectively final。
public class LambdaCaptureDemo {
public static void main(String[] args) {
String prefix = "订单";
Runnable task = () -> System.out.println(prefix + "处理");
// prefix = "支付"; // 编译失败,prefix 被 Lambda 捕获后不能再修改
task.run();
}
}为什么有限制?因为局部变量在线程栈里,方法执行结束后就没了。Lambda 如果要在之后执行,必须捕获一份稳定值。允许外部局部变量随意变化,会让生命周期和并发语义变得混乱。
注意:引用不能变,不代表对象内容不能变。
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 修改外部集合容易引发线程安全和可读性问题。
函数式接口
函数式接口是只有一个抽象方法的接口。
@FunctionalInterface
public interface OrderFilter {
boolean match(Order order);
}使用:
import java.math.BigDecimal;
public class OrderFilterDemo {
public static void main(String[] args) {
OrderFilter highAmount = order -> order.getAmount().compareTo(new BigDecimal("1000")) > 0;
}
}@FunctionalInterface 不是必须的,但建议加上。它能让编译器帮你检查这个接口是否真的只有一个抽象方法。
函数式接口可以有默认方法和静态方法,只要抽象方法只有一个即可。
@FunctionalInterface
public interface OrderFilter {
boolean match(Order order);
default OrderFilter and(OrderFilter other) {
return order -> this.match(order) && other.match(order);
}
}这也是 Predicate 有 and、or、negate 的原因。
常见函数式接口
| 接口 | 抽象方法 | 含义 |
|---|---|---|
Predicate<T> | boolean test(T t) | 判断条件 |
Function<T,R> | R apply(T t) | 类型转换 |
Consumer<T> | void accept(T t) | 消费数据 |
Supplier<T> | T get() | 提供数据 |
Demo:
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 是对集合数据进行流水线处理的工具。它不存储数据,而是描述数据如何被过滤、转换、聚合。
flowchart TD
A["集合数据"] --> B["stream"]
B --> C["filter 过滤"]
C --> D["map 转换"]
D --> E["sorted 排序"]
E --> F["collect 收集结果"]Stream 操作分为两类:
| 类型 | 示例 | 是否立即执行 |
|---|---|---|
| 中间操作 | filter、map、sorted | 不立即执行 |
| 终止操作 | collect、count、forEach | 触发执行 |
Stream 不是集合。集合负责存数据,Stream 负责描述处理流程。一个 Stream 通常只能消费一次:
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:订单筛选和统计
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 惰性执行原理
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 不会执行");
}
}只有加上 collect、count、forEach 这类终止操作,流水线才会真正执行。
更重要的是,Stream 会把多个中间操作融合成一次遍历,而不是每个操作都完整遍历一遍集合。
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 是短路终止操作。找到第一个满足条件的元素后,后面的元素可能不会继续处理。
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 是把嵌套结构摊平。
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。
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,默认会抛异常。
Map<String, Order> byStatus = orders.stream()
.collect(Collectors.toMap(Order::getStatus, order -> order)); // 多个 PAID 会报错需要明确合并规则:
Map<String, Order> byStatus = orders.stream()
.collect(Collectors.toMap(
Order::getStatus,
order -> order,
(oldValue, newValue) -> newValue
));商业项目里,用户 ID、订单号、状态、医院编码等字段是否唯一必须想清楚,不能随便 toMap。
Optional 是什么
Optional 用来表达“可能有值,也可能没有值”。它不是为了消灭所有 null,而是让返回值的缺失语义更清楚。
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
Optional.of("Tom"); // 不能传 null
Optional.ofNullable(null); // 可以传 null,得到 Optional.empty()如果值可能为空,用 ofNullable。
orElse 和 orElseGet
orElse 的默认值会先计算,orElseGet 只有在没值时才执行 Supplier。
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
Optional<String> value = Optional.empty();
// value.get(); // NoSuchElementException更清楚的写法:
String username = value.orElseThrow(() -> new IllegalArgumentException("用户不存在"));Optional 不能替代参数校验,也不能让空指针彻底消失。它只是让“可能没有值”这件事在返回值上更明确。
商业 Demo:采集记录聚合
下面模拟医疗采集平台中按医院统计成功记录数量。
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 会使用并行执行,但不是所有场景都会更快。
flowchart TD
A["parallelStream"] --> B["拆分任务"]
B --> C["多个线程执行"]
C --> D["合并结果"]
D --> E["返回结果"]风险:
| 风险 | 说明 |
|---|---|
| 线程池不可控 | 默认使用公共 ForkJoinPool |
| IO 任务不一定适合 | 阻塞可能拖慢公共线程池 |
| 共享变量不安全 | 并行写集合可能出错 |
| 数据量小反而慢 | 拆分和合并也有开销 |
业务系统里,涉及数据库、Redis、HTTP 调用时,不要随便用 parallelStream。
更具体地说,parallelStream 默认使用公共 ForkJoinPool.commonPool()。如果你在并行流里做阻塞 IO,可能把公共线程池堵住,影响同 JVM 内其他也依赖公共池的任务。
错误示例:
orders.parallelStream()
.map(order -> remoteClient.query(order.getId()))
.collect(Collectors.toList());问题:
- 线程池不可控,不知道并发量是否会打爆下游。
- 没有统一超时、限流、隔离和监控。
- 一个慢接口可能拖住公共池线程。
- 异常处理和链路追踪不清晰。
生产里如果确实要并发调用下游,更推荐显式线程池、CompletableFuture、限流和超时控制。
Stream 调试和性能判断
Stream 不一定比 for 快。它的优势是表达“过滤、映射、分组、聚合”的数据处理意图。
判断方式:
| 场景 | 建议 |
|---|---|
| 简单循环、极致性能热点 | for 循环更直接 |
| 内存集合过滤转换 | Stream 可读性好 |
| 复杂业务分支 | 拆成方法,不要硬写长链 |
| 每个元素远程调用 | 不要直接 Stream 里调,先批量或显式并发 |
| 大数据量统计 | 优先数据库 SQL、ES 聚合或流式处理 |
如果 Stream 链太长,可以用 peek 临时调试,但不要把 peek 当业务逻辑。
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() 直接取 | 没值时报错 | 用 orElse、orElseThrow |
滥用 parallelStream | 线程和性能问题 | 明确评估后再用 |
| Lambda 里修改外部集合 | 并发和可读性问题 | 使用收集器 |
| Stream 被重复消费 | 抛 IllegalStateException | 需要时重新创建 Stream |
toMap key 重复 | 抛异常或覆盖不明确 | 明确唯一性和合并规则 |
orElse 默认值很重 | 有值时也执行默认值计算 | 使用 orElseGet |
线上排查流程
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 通常通过 invokedynamic 和 LambdaMetafactory 在运行期创建函数对象,它的核心价值是把行为作为参数传递。
函数式接口是什么?
函数式接口是只有一个抽象方法的接口,可以用 Lambda 表达式实现。@FunctionalInterface 不是必须的,但建议加上,编译器会帮你检查接口是否仍然满足函数式接口规则。
Stream 为什么是惰性执行?
中间操作只是描述流水线,不会立即遍历数据。只有遇到终止操作,比如 collect、count、forEach、findFirst,流水线才真正执行。惰性执行可以融合多个操作,并支持短路。
map 和 flatMap 区别?map 是一对一转换,一个元素变成另一个元素;flatMap 是把每个元素转换成一个流,再把多个流摊平成一个流,适合处理嵌套集合。
Optional 能彻底解决空指针吗?
不能。Optional 只是让方法返回值的“可能没有值”更明确。它本身也可能被错误使用,比如 Optional 变量设为 null、of(null)、直接 get()。实体字段和 DTO 字段通常不建议使用 Optional。
parallelStream 为什么慎用?
它默认使用公共 ForkJoinPool,线程池隔离、并发量、超时、监控都不够明确。IO 调用、数据库访问、HTTP 请求放进 parallelStream 可能拖慢公共线程池甚至打爆下游。
本章小结
Lambda 让行为可以作为参数传递,函数式接口定义行为形状,Stream 用流水线处理集合,Optional 表达可能缺失的返回值。学习它们不能只追求链式写法,要理解惰性执行、终止操作、副作用和并行流风险。
