在使用 Java 的新特性 Collectors.toMap() 将 List 转换为 Map 时存在一些不容易发现的问题,这里总结一下备查。

空指针风险

java.lang.NullPointerException

当 List 中有 null 值的时候,使用 Collectors.toMap() 转为 Map 时,会报 java.lang.NullPointerException,如下:

List<SdsTest> sdsTests = new ArrayList<>();
SdsTest sds1 = new SdsTest("aaa","aaa");
SdsTest sds2 = new SdsTest("bbb",null); sdsTests.add(sds1);
sdsTests.add(sds2); Map<String, String> map = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, SdsTest::getAge));
System.out.println(map.toString()); ---------
运行错误:
Exception in thread "main" java.lang.NullPointerException
at java.util.HashMap.merge(HashMap.java:1216)
at java.util.stream.Collectors.lambda$toMap$150(Collectors.java:1320)
.....

  原因是 toMap() 方法中使用 Map.merge() 方法合并时,merge 不允许 value 为 null 导致的,源码如下:

default V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
// 在这里判断了value不可为null
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value : remappingFunction.apply(oldValue, value);
...

解决方法

  1. 业务控制不要出现 Null 值【有 Null 的地方,可以赋值默认值】
  2. 在转换时加判断,如果为 null,则给一个默认值
Map<String, String> map = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, sdsTest -> sdsTest.getAge() == null ? "0" : sdsTest.getAge()));
  1. 使用 collect(..) 构建,允许空值
Map<String, String> nmap = sdsTests.stream().collect(HashMap::new,(k, v) -> k.put(v.getName(), v.getAge()), HashMap::putAll);
// TODO 下游业务从Map取值要做NPE判断
  1. 使用 Optional 对值进行包装
Map<String, Optional<String>> opmap = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, sdsTest -> Optional.ofNullable(sdsTest.getAge())));
System.out.println("bbb.age=" + opmap.get("bbb").orElse("0"));
------------
输出:
bbb.age=0

建议

  1. 优先业务控制,尽量避免 List 中存在 Null
  2. 其次推荐第 4 种方法【使用 Optional 对值进行包装】,能很好的避免 NPE 问题

key重复风险

java.lang.IllegalStateException: Duplicate key xx

当 List 中有重复值的时候,使用 Collectors.toMap() 转为 Map 时,会报:java.lang.IllegalStateException: Duplicate key xx,例如

List<SdsTest> sdsTests = new ArrayList<>();
SdsTest sds1 = new SdsTest("aaa","aaa");
SdsTest sds2 = new SdsTest("aaa","ccc"); sdsTests.add(sds1);
sdsTests.add(sds2); Map<String, String> map = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, SdsTest::getAge));
System.out.println(map.toString()); ---------
运行错误:
Exception in thread "main" java.lang.IllegalStateException: Duplicate key aaa
at java.util.stream.Collectors.lambda$throwingMerger$92(Collectors.java:133)
at java.util.stream.Collectors$$Lambda$6/1177096266.apply(Unknown Source)
at java.util.HashMap.merge(HashMap.java:1245)
.....

原因是两个参数的toMap(xx, xx)方法, 当出现重复key触发merge时,直接抛出异常。源码如下:

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
// 注意这里的throwingMerger()
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

接下来我们看throwingMerger() 方法:【注意方法注释

/**
* Returns a merge function, suitable for use in
* {@link Map#merge(Object, Object, BiFunction) Map.merge()} or
* {@link #toMap(Function, Function, BinaryOperator) toMap()}, which always
* throws {@code IllegalStateException}. This can be used to enforce the
* assumption that the elements being collected are distinct.
*
* @param <T> the type of input arguments to the merge function
* @return a merge function which always throw {@code IllegalStateException}
*/
private static <T> BinaryOperator<T> throwingMerger() {
return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}
...

解决方法

  1. 业务控制尽量不要出现重复值
  2. 出现重复 key 时,使用后面的 value 覆盖前面的 value
SdsTest sds1 = new SdsTest("aaa","aaa");
SdsTest sds2 = new SdsTest("bbb","bbb");
SdsTest sds3 = new SdsTest("aaa","ccc"); sdsTests.add(sds1);
sdsTests.add(sds2);
sdsTests.add(sds3); // 写法一
Map<String, String> nmap = sdsTests.stream().collect(HashMap::new,(k, v) -> k.put(v.getName(), v.getAge()), HashMap::putAll);
System.out.println("nmap->:" + nmap.toString()); // 写法二
Map<String, String> nmap1 = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, SdsTest::getAge, (k1, k2) -> k2));
System.out.println("nmap1->:" + nmap1.toString());
...
----------------------
输出:
nmap->:{aaa=ccc, bbb=bbb}
nmap1->:{aaa=ccc, bbb=bbb}
  1. 出现重复 key 时,把对应的 value 拼接起来
...
Map<String, String> nmap1 = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName, SdsTest::getAge, (k1, k2) -> k1 + "," + k2));
System.out.println("nmap1->:" + nmap1.toString());
...
----------------
输出:
nmap1->:{aaa=aaa,ccc, bbb=bbb}
  1. 把重复 key 的值拼成一个集合
......
Map<String, List<String>> map = sdsTests.stream().collect(Collectors.toMap(SdsTest::getName,
s -> {
List<String> ages = new ArrayList<>();
ages.add(s.getAge());
return ages;
},
(List<String> v1, List<String> v2) -> {
v1.addAll(v2);
return v1;
}));
System.out.println("map->"+map.toString());
------------
输出:
map->{aaa=\[aaa, ccc\], bbb=\[bbb\]}

建议:

  1. 优先业务控制,尽量避免 List 中出现重复
  2. 若存在重复场景,则根据实际业务场景选择具体方法【覆盖、拼接、搞成集合】

Java8 中使用Stream 让List 转 Map使用总结的更多相关文章

  1. 【Java8新特性】面试官:谈谈Java8中的Stream API有哪些终止操作?

    写在前面 如果你出去面试,面试官问了你关于Java8 Stream API的一些问题,比如:Java8中创建Stream流有哪几种方式?(可以参见:<[Java8新特性]面试官问我:Java8中 ...

  2. Java8中的Stream流式操作 - 入门篇

    作者:汤圆 个人博客:javalover.cc 前言 之前总是朋友朋友的叫,感觉有套近乎的嫌疑,所以后面还是给大家改个称呼吧 因为大家是来看东西的,所以暂且叫做官人吧(灵感来自于民间流传的四大名著之一 ...

  3. 【Java8新特性】面试官问我:Java8中创建Stream流有哪几种方式?

    写在前面 先说点题外话:不少读者工作几年后,仍然在使用Java7之前版本的方法,对于Java8版本的新特性,甚至是Java7的新特性几乎没有接触过.真心想对这些读者说:你真的需要了解下Java8甚至以 ...

  4. java8中的stream().filter()的使用和Optional()

    转: https://www.cnblogs.com/yimiyan/p/5992440.html Optional: https://www.cnblogs.com/zhangboyu/p/7580 ...

  5. Java8中的Stream API

    本篇文章继续介绍Java 8的另一个新特性——Stream API.新增的Stream API与InputStream和OutputStream是完全不同的概念,Stream API是对Java中集合 ...

  6. Java8中那些方便又实用的Map函数

    原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介 java8之后,常用的Map接口中添加了一些非常实用的函数,可以大大简化一些特定场景的代码编写,提升代码可读性,一 ...

  7. java8中的stream流遍历

    比较for循环.迭代器.java8Stream流遍历的不同 package cnom.test.testUtils; import java.io.Serializable; import java. ...

  8. java8中的Stream

    Collection.stream() / parallelStream() 1. Stream 1)Filter    stringCollection .stream().filter((s) - ...

  9. 【转】Java8中list转map方法总结

    https://blog.csdn.net/zlj1217/article/details/81611834 背景在最近的工作开发之中,慢慢习惯了很多Java8中的Stream的用法,很方便而且也可以 ...

随机推荐

  1. Python练习3-XML-RPC实现简单的P2P文件共享

    XML-RPC实现简单的P2P文件共享 先来个百度百科: XML-RPC的全称是XML Remote Procedure Call,即XML(标准通用标记语言下的一个子集)远程过程调用.它是一套允许运 ...

  2. Day006 方法的定义和调用

    方法的定义 Java的方法类似于其他语言的函数,是一段用来完成特定功能的代码片段,一般情况下,定义一个方法包含以下语法: 方法包含一个方法头和一个方法体.下面是一个方法的所有部分: 修饰符:修饰符,这 ...

  3. CCNA 第四章 轻松划分子网

    1:划分子网的的好处: (1):减少网络流量 (2):优化网络性能 (3):简化管理 (4):有助于覆盖大型地理区域 2:CIDR和ISP的概念 (1):CIDR:Classless Inter-Do ...

  4. Windows进程间通讯(IPC)----消息队列

    消息队列 windows系统是通过消息驱动的,每移动一下鼠标,点击一下屏幕都会产生一个消息.这些消息会先被放在windows的一个系统消息队列(先进先出)中,windows系统会为每一个GUI线程创建 ...

  5. utf8字符集下的比较规则

    前言: 在MySQL中,比较常用的字符集是utf8和utf8mb4.这两个字符集是类似的,utf8是utf8mb3的别名,所以之后在MySQL中提到utf8就意味着使用1~3个字节来表示一个字符,如果 ...

  6. volatile 的使用

    ① 编译器很聪明,会帮我们做些优化,比如: int a; a = 0; // 这句话可以优化掉,不影响 a 的结果 a = 1; ② 有时候编译器会自作聪明,比如: int *p = ioremap( ...

  7. prometheus node-exporter增加新的自定义监控项

    项目中collector中新增加自己所需监控项即可 定义启动node-exporter是传入的参数 var ( phpEndPoint = kingpin.Flag("collector.p ...

  8. [bug] Python:“TabError: inconsistent use of tabs and spaces in indentation”

    原因 代码中混用了Tab和4个空格 参考 https://blog.csdn.net/dongdong9223/article/details/82745068

  9. [c++] 文件包含

    当一个类用到另一个类时,有两种包含方式,在.h中包含和在.cpp中包含 用到公共类库时,在.h文件中包含(公共类库可视为不变的) 用到项目开发过程中自己或同事写的类时,在.cpp文件中包含(可能根据需 ...

  10. [刷题] 77 Combinations

    要求 给出两个整数n和k,在n个数字中选出k个数字的所有组合 示例 n=4 , k=2 [ [ 1, 2 ] , [ 1, 3 ] , [ 1, 4 ] , [ 2, 3 ] , [ 2, 4 ] , ...