线上一个查询接口偶发空指针,堆栈最后一行落在 list.stream().anyMatch(x -> x.getId().equals(id))。看代码谁都觉得不会错,问题出在 list 里有 null,元素一取字段就炸。
它只是判断有没有,不负责取
anyMatch 接收一个 Predicate,找到第一个满足条件的元素就立刻返回 true,后面的元素不再处理。所以别在里面写发消息、写库这类副作用,执行次数取决于数据顺序,很难预期。要拿到元素本身,用 filter 加 findFirst。
空指针的两个来源
一是集合里有 null 元素,二是 getId 本身返回 null,再调 equals 就炸。稳妥的写法是先 filter(Objects::nonNull),再把常量放前面,比如 id.equals(x.getId()),这样即使字段为空也只是返回 false。
性能上的实际情况
- anyMatch 比 filter 加 count 快,因为短路
- 大数据量先把集合转成 Set 再判断,O(1) 比遍历划算
- 并行流只在元素很多、判断很耗时的时候才有意义,小集合反而更慢
还有一点,在并行流的 anyMatch 里修改共享集合,结果不可控,真需要收集就用 collect 或 forEachOrdered。代码 review 时看到 anyMatch 里带赋值,基本都要打回去。
几种写法对比一下
判断 id 是否存在,最省事的是把集合先转成 HashSet,再调 contains,十万条数据也就几十毫秒,不用流。anyMatch 的价值在带复杂条件,比如同时判状态和类型。要返回元素本身用 filter(...).findFirst().orElse(null),拿到 Optional 后别直接 get,用 isPresent 或 orElse 处理。
还有个细节:anyMatch 传入 null 参数会直接抛 NPE,集合本身是 empty 则返回 false。自己写工具方法时,把判空放在入口,调用方就不用每处都操心。Integer 的 equals 也要注意,拿 int 直接比会拆箱,两边都是包装类型时比的是引用,这个坑和 anyMatch 无关但经常一起出现。
这个 API 本身没坑,坑在于默认集合干净、字段非空。加一行非空过滤,线上那类偶发 NPE 就没了。