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

背景

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

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

相关推荐
爱勇宝42 分钟前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒5 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码6 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo6 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志7 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..7 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects7 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰7 小时前
一些基础和问题解决
后端
陈随易8 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN8 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp