工作积累——stream

工作积累——stream,第1张

引子

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

问题

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

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


    public static void main(String[] args) {
        List<User> 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<String, String> 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<Object, Object> hashMap = loop.stream().collect(HashMap::new,
               (m, v) -> m.put(v.getUserId(), covertName(v)), HashMap::putAll);

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

哪里出现了问题?

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

    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);
    }

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

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

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

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

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

    public static <T, R> TerminalOp<T, R>
    makeRef(Supplier<R> seedFactory,
            BiConsumer<R, ? super T> accumulator,
            BiConsumer<R,R> reducer) {
        Objects.requireNonNull(seedFactory);
        Objects.requireNonNull(accumulator);
        Objects.requireNonNull(reducer);
        class ReducingSink extends Box<R>
                implements AccumulatingSink<T, R, ReducingSink> {
            @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<T, R, ReducingSink>(StreamShape.REFERENCE) {
            @Override
            public ReducingSink makeSink() {
                return new ReducingSink();
            }
        };
    }

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

HashMap<Object, Object> 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<String, String> 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<? super V, ? super V, ? extends V> 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<User> 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<String, String> 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了

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/739263.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-28
下一篇 2022-04-28

发表评论

登录后才能评论

评论列表(0条)

保存