背景
最近上线组件之后突然发现某个空指针问题频繁出现,于是想着去定位一下具体问题原因,发现问题来自下面的代码(已经省略无关代码):
java
public static void main(String[] args) {
List<StationInfoDO> stationInfos = new ArrayList<>();
StationInfoDO stationInfoDO = new StationInfoDO();
stationInfoDO.setBmStationId("bm-0001");
stationInfos.add(stationInfoDO);
Map<String, String> bmStationIdOperatorIdMap = stationInfos.stream()
.collect(Collectors.toMap(StationInfoDO::getBmStationId, StationInfoDO::getOperatorId,(e1, e2) -> e1));
System.out.println(bmStationIdOperatorIdMap);
}
这里熟悉toMap方法的坑的同学,一定一眼就能看出问题,但是笔者在遇到这个场景的时候还没踩过这个坑,于是开始我的排查之路
遇事不决问AI
AI时代的程序员嘛, 原来遇到问题就谷歌,现在遇到问题直接丢给大模型好了,于是我就去问了号称编码最聪明的大模型 Claude-3.7-Sonnet,好家伙直接告诉我这段代码不会报错....

完了,大模型又在胡说八道了,要不是自己分别已经用Jdk8和Jdk21测试过这段代码,还真的信了这段分析,此刻我真想直接一个空指针甩它脸上。
拿回程序员的判断力
好了 既然AI靠不住 那么只好靠自己了,毕竟这bug确实也是我自己写出来的,简单看下源码是怎么回事
我们直接看toMap方法的实现
java
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
从上面的代码可以看出确实toMap方法最后确实返回了一个HashMap,这说明AI在关于返回的Map类型方面确实说的没问题,那就很奇怪了啊,背过很多年八股文的都知道 HashMap value确实是允许为null的,甚至key都可以是null,只有ConcurrentHashMap 才不允许Key和value为null
看来一定是toMap方法的实现有问题,继续向下追踪代码实现:
java
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}
看到上面的这段代码,发现核心逻辑肯定出现在这个merge方法里面,这个merge方法取出来了key和value并传进去了合并函数,于是查看下merge代码的实现即可,从源码直接点进去看到的merge方法实际上是Map类的default方法,HashMap 对其进行了复写,所以我们直接看下HashMap的merge方法的实现
java
@Override
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
if (old != null) {
V v;
if (old.value != null)
v = remappingFunction.apply(old.value, value);
else
v = value;
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
return v;
}
if (value != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return value;
}
好嘛,第一行就给我写明了value是null的时候就会直接甩空指针,问题显然就出现在这里。但是仔细看下这段代码如果value是null其实完全没问题,不影响整个代码的逻辑,只不过这组value没有进入HashMap而已,于是我问了大模型为啥merge函数规定value不能为null的问题
大模型的回答如下:
在 HashMap 的 merge 方法中限制 value 不能为 null,有以下几个原因:
1、方法语义明确性:merge 方法的目的是合并值,如果允许 null 值,就会模糊这个方法的语义。当 value 为 null 时,无法确定是要删除键值对还是设置值为 null。
2、避免歧义:在 merge 方法中,如果键已存在,会调用 remappingFunction 将旧值和新值合并。如果新值为 null,remappingFunction 可能无法正确处理。
3、API 设计一致性:Java 集合框架中,有些方法明确规定了 null 的处理方式。例如,在 HashMap 中,put 方法允许 value 为 null,而 compute 和 merge 等方法则禁止 value 为 null,这样可以避免混淆。
emm... 这回答读者自己评判吧
toMap方法的使用规范
最后总结下toMap方法的使用规范:
1、 key不能有重复,否则会报错。java.lang.IllegalStateException: Duplicate key
这个问题笔者之前是遇到过的,问题原因在于如果不传递toMap的第三个mergeFunction,实际上会默认启用一个叫做throwingMerger的合并函数来处理同样的key出现后的合并办法,这个处理办法也是简单暴力就是抛出异常。
java
private static <T> BinaryOperator<T> throwingMerger() {
return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}
2、value不能为空,否则报空指针。java.lang.NullPointerException
如果确实存在value为null的可能,则可以像下面这样去改写代码
java
//解决方案一,使用Optional类处理null 若value为空,设置默认值""
HashMap<String, String> map02 = list.stream()
.collect(Collectors.toMap(User::getName, s -> Optional.ofNullable(s.getSex()).orElse(""), (a, b) -> b, HashMap::new));
System.out.println(map02);
//解决方案二,直接使用collect()方法进行规约操作 调用hashMap putAll方法, 注意key相同时,value会覆盖。
HashMap<String, String> map03 = list.stream()
.collect(HashMap::new, (map, item) -> map.put(item.getName(), item.getSex()), HashMap::putAll);
最后笔者还拿着这段代码考察了GPT-4o 的推理能力,发现它确实能够给出这段代码存在空指针问题,但是给出来的原因说的比较模糊,让人有点摸不着头脑。

很多时候说我们常说模型A比模型B厉害,但是当下对于使用者而言,无论使用多么厉害的模型来指导工作,对于自己拿不准的问题实际写代码试验一下往往才是最稳妥的办法