likes
comments
collection
share

活久见,java8 lamdba Collectors.toMap()报NPE

作者站长头像
站长
· 阅读数 36

前言

事情是这样的,今天调试程序时,有个功能是需要将查询的结果集:List<Map<String, Object>> 中的key转换成java的驼峰命名规则模式,比如:USER_NAME => userName 。代码如下:

List<Map<String, Object>> result = query();
result = result.stream().map(m -> m.entrySet().stream()
.collect(Collectors.toMap(e -> JavaUtil.getJavaModeColumnName(e.getKey()), Map.Entry::getValue)))
                    .collect(Collectors.toList());

然后就遇到了如下报错信息:

活久见,java8 lamdba Collectors.toMap()报NPE

一开始的时候还挺郁闷的,用了这么lamdba的API,也没遇到这个错误啊,这是为什么呢?我们去查看这行:

HashMap.merge(HashMap.java:1225) ~[na:1.8.0_201]的源代码是什么:

活久见,java8 lamdba Collectors.toMap()报NPE

判断是:如果value==null,就抛出NPE。

过程分析

复现

Map<String, Object> objectMap = new HashMap<>();
objectMap.put("ADC_ADC", null);
// 报错: value 不能为空
objectMap.entrySet().stream().collect(
    Collectors.toMap(e -> e.getKey().toLowerCase(), Map.Entry::getValue));

上述代码复现了我们上面提到的原始问题出现的错误,我们分析一下调用栈和查看jdk源码就会发现:

Collectors.toMap中调用了这个方法:

活久见,java8 lamdba Collectors.toMap()报NPE

而上述方法中的 BiConsumer<M, T> accumulator 调用了 map.merge()。

问题找到了,那么怎么解决呢?

处理方式

既然是accumulator方法有问题,那么我们就替换掉accumulator。我们找一下 Collectors类中的toMap()方法并没有提供可以传入accumulator参数的方法。那么我们退而求其次,从collect()方法下手,代码如下:

Map<String, Object> objectMap = new HashMap<>();
objectMap.put("USER_NAME", null);

Map<String, Object> collect = objectMap.entrySet().stream()
        .collect(HashMap::new, 
                 (map, entry) -> map.put(Util.getJavaModeColumnName(entry.getKey()), entry.getValue()),
                 HashMap::putAll);

R collect(Supplier supplier,

BiConsumer<R, ? super T> accumulator,

BiConsumer<R, R> combiner);

还函数的参数都是什么意思呢:

supplier: 用于创建容器的工厂函数。在收集元素之前,该函数将被调用一次,用于创建一个空的容器。

accumulator: 用于将 Stream 中的元素添加到容器中的累加器函数。该函数接受两个参数,第一个参数是容器,第二个参数是 Stream 中的元素。该函数将 Stream 中的每个元素添加到容器中。

combiner: 用于合并两个容器的函数。在多个线程并行执行收集操作时,将在每个线程中创建一个容器,并使用该函数将它们合并为一个容器。如果 Stream 是串行的,该函数将不会被调用。

那么在我们使用lamdba的收集器,jdk自带的不能满足我们的需求时,我们就可以使用该方法自定义手机规则和模式。

官方示例:String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,StringBuilder::append).toString(); 将数据拼接 。

运行结果如下图:

活久见,java8 lamdba Collectors.toMap()报NPE

这种方式的问题是:就是当你的key值重复的时候,这将会覆盖上一个值。但是在我当前的使用场景下是不存在的,因为表名称字段肯定是唯一的。

其他

网上有讨论说:更新JDK,我用JDK16,JDK17时再次测试改代码时,都无法得到正确的答案。

活久见,java8 lamdba Collectors.toMap()报NPE

遇到这个问题时,我就在想,java为什么要如此设计?value的值为什么不能为null?

后来我搜索了相关问题的讨论,在 Stack Overflow上找到了这篇:

stackoverflow.com/questions/4…

其中有一段讨论是这么说的:

活久见,java8 lamdba Collectors.toMap()报NPE

大意就是:没想到null值对API的影响如此之大,有人则认为这是一个jdk的缺陷。

这之后我不死心,之后我找到了在官网上的讨论的:bugs.openjdk.org/browse/JDK-…

不幸的是:目前该bug并未被解决,不论出于什么原因。这与我在JDK16,JDK17下的测试是保持一致的。

后续

大家也可以看一下下面这个示例的返回值是什么:

List<User> list = new ArrayList<>();
list.add(new User(12, "张三"));
list.add(new User(16, "李四"));
list.add(new User(16, "王五"));
Map<Integer, String> collect1 = list.stream().
    collect(Collectors.toMap(User::getAge, User::getName));
转载自:https://juejin.cn/post/7221470013872980024
评论
请登录