先 filter 再 map 是核心原则,旨在避免空指针、减少无效计算并使逻辑贴近业务直觉;filter 负责筛选元素,map 负责转换元素,顺序颠倒会导致性能浪费与异常风险。

先 filter 再 map 是核心原则。不是语法限制,而是避免空指针、减少无效计算、让逻辑更贴近业务直觉的实践共识。
Filter 负责“留哪些”
它用 Predicate 判断每个元素是否保留,不改变元素本身,只缩小后续操作的数据范围。
- 写法示例:
user -> user != null && user.getAge() >= 18,显式防 null 是常见且必要的 - 多个条件别堆在一个 lambda 里,拆成链式调用更清晰,比如先
filter(Objects::nonNull),再filter(u -> u.isActive()) - 效率敏感场景下,把开销小、命中率低的条件放前面,比如先按 ID 区间过滤,再做正则匹配
- 没接终端操作(如 collect、count),filter 不会真正执行——这是惰性求值的特点
Map 负责“怎么变”
它用 Function 对每个留下的元素做一对一转换,输出数量与输入严格一致,哪怕返回 null 也会进入结果流。
- 典型用法:提取字段(
User::getEmail)、转类型(String::toUpperCase)、数值计算(n -> n * 2) - map 里抛异常(比如空指针或格式错误)会中断整个流,所以建议前置校验,或用 Optional 包裹易错逻辑
- 别在 map 里做 IO、远程调用或修改外部状态——这违反无副作用原则,并行流下尤其危险
- 想把 List<List<String>> 摊平成单层?用 flatMap,不是 map
组合顺序不能颠倒
把 map 放 filter 前面,容易导致两方面问题:
立即学习“Java免费学习笔记(深入)”;
- 所有元素都执行 map 逻辑,包括那些本该被筛掉的——浪费 CPU,尤其当 map 含复杂计算时
- 对 null 或非法对象调用方法,直接触发 NullPointerException,而 filter 原本可以提前拦截
- 语义混乱:“先把所有人姓名取出来,再从中挑成年用户的姓名”,不如“先找出成年人,再取他们的姓名”自然
- 原始类型处理更明显:用
mapToInt()替代map()+Integer::intValue可省装箱开销,但要注意它返回 IntStream,后续不能再直接接 map
特殊结构 Map 的过滤要额外小心
对 Map 做流处理,本质是对其 entrySet() 流化,但 Collectors.toMap 默认拒绝 null 键和 null 值。
- 必须在 filter 阶段就排除 key 或 value 为 null 的 entry,否则 collect 时直接报
NullPointerException: key must not be null - 如果源 Map 可能含重复 key(比如按 value 分组后重建),默认 toMap 会抛 IllegalStateException,需指定 mergeFunction,例如
(old, new) -> old - 需要保持插入顺序?用
LinkedHashMap::new作为 mapFactory 参数传给 toMap


















