栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

工作积累——stream().toMap 空指针异常解决问题时发现的小坑

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

工作积累——stream().toMap 空指针异常解决问题时发现的小坑

引子

今天测试环境一处代码使用toMap出现了空指针异常,看了下其实很多经常使用lambda表达式进行转换的开发大多遇见过这种问题,本来这个也没什么研究了,现成的解决方案,但是大概就是好久没写东西了,天天忙的焦头烂额的时候突然想写点啥,于是在看到所有文章给出了一个几乎一样的解决方案时,想看看源码是否有其他方案。

问题

问题很简单就是List转换为Map的时候空指针报错了。

大概是一段这样的逻辑,使用toMap的三个参数:键映射、值映射、冲突解决逻辑

    public static void main(String[] args) {
        List loop = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                // 这条数据进入 JavaMain::covertName 回返回空
                User user = new User(String.valueOf(5),"");
                loop.add(user);
            } else {
                User user = new User(String.valueOf(i),"name" + String.valueOf(i));
                loop.add(user);
            }
        }
        Map rest = loop.stream()
                .collect(Collectors.toMap(User::getUserId, JavaMain::covertName, (o,n) -> n));

    }

    public static String covertName(User user) {
        if (StringUtils.isEmpty(user.getName())) {
            return null;
        }
        return user.getName() + user.getUserId();
    }

在循环过程中需要根据数据进行value值的设置,某些逻辑下会被设置为null,然后会无情的抛出了空指针

Connected to the target VM, address: '127.0.0.1:13641', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:13641', transport: 'socket'
Exception in thread "main" java.lang.NullPointerException
	at java.util.HashMap.merge(HashMap.java:1225)
	at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
	at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1382)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
	at dai.samples.jpa.config.JavaMain.main(JavaMain.java:31)
解决方式

解决方式很多人都给出了方案

HashMap hashMap = loop.stream().collect(HashMap::new,
               (m, v) -> m.put(v.getUserId(), covertName(v)), HashMap::putAll);

虽然系统问题解决了,但是我的问题却出来了,正常情况我们认为HashMap并不会限制我们对键和值设置为null。那既然如此为什么会出现空指针?哪里抛出的空指针?为什么另外一种方式就可以?

哪里出现了问题?

我们看下toMap的方法,它最终使用的是这个方法

    public static >
    Collector toMap(Function keyMapper,
                                Function valueMapper,
                                BinaryOperator mergeFunction,
                                Supplier mapSupplier) {
        BiConsumer accumulator
                = (map, element) -> map.merge(keyMapper.apply(element),
                                              valueMapper.apply(element), mergeFunction);
        return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
    }

这里会发现toMap最终使用的是Map的merge方法进行值的设置,而不是我们以为的put。那么在HashMap的merge中,方法的第一行就告诉你,我们不接受null的值。

  @Override
    public V merge(K key, V value,
                   BiFunction remappingFunction) {
        if (value == null)
            throw new NullPointerException();
		......
        return value;
    }
为什么另外一个方法就可以呢?

首先看另外一个方法是什么样的,首先会发现三个参数会被包裹到ReduceOps中

    @Override
    public final  R collect(Supplier supplier,
                               BiConsumer accumulator,
                               BiConsumer combiner) {
        return evaluate(ReduceOps.makeRef(supplier, accumulator, combiner));
    }

这个ReduceOps干嘛的?说实话好久没看源码了我也不知道,不管了继续往下看,到这一步就很明显了,原来第一个参数seedFactory会创建一个Map,然后第二个参数将state和循环对象作为参数传递到accumulator,而第三个方法在参数注释以及入参的ReducingSink 已经提醒你了这个是用来并发流中合并数据的内容。

    public static  TerminalOp
    makeRef(Supplier seedFactory,
            BiConsumer accumulator,
            BiConsumer reducer) {
        Objects.requireNonNull(seedFactory);
        Objects.requireNonNull(accumulator);
        Objects.requireNonNull(reducer);
        class ReducingSink extends Box
                implements AccumulatingSink {
            @Override
            public void begin(long size) {
                state = seedFactory.get();
            }

            @Override
            public void accept(T t) {
                accumulator.accept(state, t);
            }

            @Override
            public void combine(ReducingSink other) {
                reducer.accept(state, other.state);
            }
        }
        return new ReduceOp(StreamShape.REFERENCE) {
            @Override
            public ReducingSink makeSink() {
                return new ReducingSink();
            }
        };
    }

现在明白为什么使用第二个方式可以了,重点在于第二个参数。使用toMap的时候java默认使用Map的merge方法这使得我们没办法去设置null的值,而第二种方式我们可以在第二个参数中自己实现设置值的方式,比如我们可以下面这样,让我们用另外一种方式再错一次,笑:)

HashMap hashMap = 
                loop.parallelStream().collect(HashMap::new, 
                        (m, v) -> m.merge(v.getUserId(), Objects.requireNonNull(covertName(v)), (o,n) -> n), 
                        HashMap::putAll);
toMap的另外一个问题

回到这个方法,我们没办法设置值为null,那么如果在最后一个参数(o,n) -> n中返回null会如何呢?

 Map rest = loop.stream()
                .collect(Collectors.toMap(User::getUserId, JavaMain::covertName, (o,n) -> Objects.equals(n,o)?null:n));

在HashMap的merge中有这么一段逻辑

    @Override
    public V merge(K key, V value,
                   BiFunction remappingFunction) {
      ......
        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;
        }
     .....
        return value;
    }

这个时候会发现如果合并后的值为null的时候并不会将null设置进去而是直接移除掉这个节点。就像下面的例子:

    public static void main(String[] args) {
        List loop = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            if (i == 5 || i == 6) {
                User user = new User(String.valueOf(5),"name" + String.valueOf(5));
                loop.add(user);
            } else {
                User user = new User(String.valueOf(i),"name" + String.valueOf(i));
                loop.add(user);
            }
        }
        Map rest = loop.stream()
                .collect(Collectors.toMap(User::getUserId, JavaMain::covertName,(n, o) -> Objects.equals(n,o)?null:n));
        System.out.println(JSON.toJSON(rest.keySet()));
    }

这个时候返回的key中不仅仅6不见了连5也不见了。

["0","1","2","3","4","7","8","9"]

所以这个时候使用的时候一定要注意否则在后续进行key值进行判断的时候,有可能导致错误的数量。

ps.磨磨唧唧写了一个多小时,emmmm,感觉没啥卵用的知识+1了

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/836523.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号