目标:确保 Stream 使用安全、可读、可维护,避免空指针、重复 key、边界问题、并发隐患等常见坑。Stream 提供了优雅的函数式编程方式,但不当使用容易引入隐蔽 bug。团队统一规范是关键。
一、空集合与边界条件处理
1. allMatch / anyMatch / noneMatch
- 规范 :空集合时,
allMatch和noneMatch返回 true ,anyMatch返回 false 。如果业务语义上空集合应视为"不成立"或需特殊处理,必须显式判空。 - 示例
java
// 错误:空列表时返回 true,可能误导业务(如"所有人都成年")
boolean allAdult = list.stream().allMatch(p -> p.getAge() >= 18);
// 推荐:显式处理空集合
boolean allAdult = list.isEmpty() ? false : list.stream()
.allMatch(p -> p.getAge() >= 18);
// 或者使用 anyMatch 的场景
boolean hasMinor = !list.isEmpty() && list.stream().anyMatch(p -> p.getAge() < 18);
- 原因:vacuous truth(空集真理)导致逻辑偏差。实际项目中常因忽略空集合而埋下 bug。
2. findFirst / findAny 与 null 元素
- 规范 :Stream 中可能包含 null 元素时,必须先过滤 null,否则在排序、比较或 Optional 处理时可能 NPE。
- 示例
java
Optional<Person> firstPerson = list.stream()
.filter(Objects::nonNull) // 先过滤 null
.findFirst();
- 原因 :
findFirst()返回 Optional,但后续操作(如 comparator)遇到 null 会抛 NPE。
二、Collectors.toMap() 安全使用
1. key / value 不允许 null
- 规范 :
Collectors.toMap()对 key 或 value 为 null 会抛 NullPointerException。必须提前过滤或提供默认值。 - 示例
java
Map<String, String> map = list.stream()
.filter(p -> p.getName() != null && p.getSex() != null) // 过滤 null
.collect(Collectors.toMap(
Person::getName,
p -> Objects.requireNonNullElse(p.getSex(), "未知"),
(v1, v2) -> v1 // 合并函数,必备
));
- 原因:HashMap 的 merge 方法不允许 null value(尽管 HashMap 本身允许)。
2. duplicate key 处理
- 规范 :始终提供 merge function,即使当前数据无重复,也要显式定义策略(保留第一个/最后一个/合并/抛异常)。
- 示例
java
// 保留第一个
.collect(Collectors.toMap(Person::getId, Function.identity(), (existing, replacement) -> existing));
// 保留最后一个
.collect(Collectors.toMap(Person::getId, Function.identity(), (existing, replacement) -> replacement));
// 合并(如求和)
.collect(Collectors.toMap(Person::getId, Person::getScore, Integer::sum));
- 原因 :无 merge function 时,duplicate key 抛 IllegalStateException。并行流下更易触发。
3. 并行场景补充
- 规范 :并行流下建议使用
Collectors.toConcurrentMap()以获得线程安全 Map。
三、Optional 的使用规范
1. map vs flatMap
- 规范 :
- 字段不可能 null → 用
map - 字段可能 null → 用
flatMap(Optional.ofNullable(...))
- 字段不可能 null → 用
- 示例
java
Optional<String> sexOpt = optionalPerson
.flatMap(p -> Optional.ofNullable(p.getSex())); // 安全处理 null 字段
Optional<Integer> ageOpt = optionalPerson
.map(Person::getAge); // age 不为 null
2. 避免盲用 get()
- 规范 :禁止直接调用
get()无判断。优先orElse/orElseGet/orElseThrow。 - 示例
java
String sex = sexOpt.orElse("未知");
String sex = sexOpt.orElseGet(() -> computeDefaultSex());
四、map / peek / forEach 的副作用规范
1. map
- 用途:纯转换元素,返回新值。
- 规范 :禁止在 map 中产生副作用(如修改原对象、IO、日志)。
- 示例
java
List<Integer> ages = list.stream()
.map(Person::getAge) // 纯转换
.toList();
2. peek
- 用途 :仅用于调试或日志,观察中间元素。
- 规范 :
- 禁止在 peek 中修改元素或外部状态(副作用)。
- 并行流中 peek 执行顺序不可控,可能被优化掉(Java 9+)。
- 示例
java
list.stream()
.filter(p -> p.getAge() > 18)
.peek(p -> System.out.println("过滤后: " + p)) // 仅调试
.map(Person::getName)
.toList();
3. forEach
- 规范:终端操作,仅用于最终消费。避免复杂业务逻辑。
五、并行流(parallelStream)使用规范
- 规范 :
- 仅限 CPU 密集型、纯计算、无副作用、无状态的任务(如大数据量数值计算)。
- 禁止 用于 IO、DB、RPC、锁操作或有副作用的场景。
- 确保操作 stateless(无共享可变状态)和 non-interfering(不修改源)。
- 数据结构引用局部性好(如原始数组)时收益更高;链表等差。
- 示例
java
// 适合:纯计算
long sum = longList.parallelStream().reduce(0L, Long::sum);
// 不适合:IO 操作
list.parallelStream().forEach(this::saveToDb); // 禁止!
- 原因:副作用导致数据竞争、不可预测结果;开销大时性能反而下降。
六、空指针防御通用模式
- Stream 元素可能 null →
filter(Objects::nonNull) - 字段可能 null →
Optional.ofNullable+flatMap/orElse - 集合空 → 判空或用
Stream.ofNullable(Java 9+) toMapkey/value → 过滤或兜底 + merge function- 短路操作(如
findFirst)返回 Optional → 安全消费
七、可读性与可维护性
- 链式调用不超过 4-5 个操作,否则拆分成独立方法或使用中间变量。
- 复杂逻辑优先用传统 for 循环,Stream 仅用于简单数据转换/聚合。
- 必须添加注释 说明边界处理(如空集合、null、duplicate key)。
- 优先使用不可变对象和纯函数风格。
- 代码审查重点检查:副作用、并行流使用、merge function 是否缺失。
八、总结(团队执行要点)
- 空集合/null 元素/字段 → 永远防御,不过度信任 Stream 默认行为。
- toMap/toConcurrentMap → 必提供 merge function,防御 null 和 duplicate。
- Optional → 禁止盲 get(),善用 flatMap/orElse。
- map/peek/forEach → map 无副作用,peek 仅调试,forEach 仅终端消费。
- parallelStream → 严格限定场景,避免副作用和 IO。
- 可读性 → 短链、注释、优先传统循环处理复杂逻辑。
Stream 优雅强大,但"优雅"不等于"安全"。团队规范是避免坑的最后防线,违反规范等于自埋雷区。定期审查 Stream 代码,养成防御式编程习惯!