实践出真知,大模型也会犯糊涂

背景

最近上线组件之后突然发现某个空指针问题频繁出现,于是想着去定位一下具体问题原因,发现问题来自下面的代码(已经省略无关代码):

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厉害,但是当下对于使用者而言,无论使用多么厉害的模型来指导工作,对于自己拿不准的问题实际写代码试验一下往往才是最稳妥的办法

相关推荐
lifallen21 分钟前
Java BitSet类解析:高效位向量实现
java·开发语言·后端·算法
子恒20052 小时前
警惕GO的重复初始化
开发语言·后端·云原生·golang
daiyunchao2 小时前
如何理解"LLM并不理解用户的需求,只是下一个Token的预测,但他能很好的完成任务,比如写对你想要的代码"
后端·ai编程
Android洋芋2 小时前
SettingsActivity.kt深度解析
后端
onejason2 小时前
如何利用 PHP 爬虫按关键字搜索 Amazon 商品
前端·后端·php
令狐冲不冲2 小时前
常用设计模式介绍
后端
Java水解2 小时前
深度解析MySQL中的Join算法:原理、实现与优化
后端·mysql
一语长情2 小时前
关于Netty的DefaultEventExecutorGroup使用
java·后端·架构
易元2 小时前
设计模式-状态模式
后端·设计模式
bug菌2 小时前
🤔强一致性 VS 高可用性:你为啥没get到延迟预算的真正意义?
分布式·后端·架构