在使用 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 stream 中利用 groupingBy 进行多字段分组求和

    Java8的groupingBy实现集合的分组,类似Mysql的group by分组功能,注意得到的是一个map 对集合按照单个属性分组 case1: List<String> items ...

  2. Java8 Stream新特性详解及实战

    Java8 Stream新特性详解及实战 背景介绍 在阅读Spring Boot源代码时,发现Java 8的新特性已经被广泛使用,如果再不学习Java8的新特性并灵活应用,你可能真的要out了.为此, ...

  3. JAVA8之lambda表达式具体解释,及stream中的lambda使用

    前言: 本人也是学习lambda不久,可能有些地方描写叙述有误,还请大家谅解及指正! lambda表达式具体解释 一.问题 1.什么是lambda表达式? 2.lambda表达式用来干什么的? 3.l ...

  4. Java8 Stream API

    Stream是Java8中,操作集合的一个重要特性. 从iteration到Stream操作 当你操作一个集合的时候,你通常的做法是迭代每一个元素,然后处理你想要的事情.举个例子: String co ...

  5. java代码之美(2)---Java8 Stream

    Stream 第一次看到Stream表达式就深深把我吸引,用它可以使你的代码更加整洁而且对集合的操作效率也会大大提高,如果你还没有用到java8的Stream特性,那就说明你确实out啦. 一.概述 ...

  6. JAVA8 Stream集合操作:中间方法和完结方法

    StreamLambda为java8带了闭包,这一特性在集合操作中尤为重要:java8中支持对集合对象的stream进行函数式操作,此外,stream api也被集成进了collection api, ...

  7. java List递归排序,传统方式和java8 Stream优化递归,无序的列表按照父级关系进行排序(两种排序类型)

    当有一个List列表是无序的,List中的数据有parentid进行关联,通过java排序成两种排序类型: 所用的测试列表最顶级无parentid,若为特殊值,修改下判断方法即可. 第一种排序:按照树 ...

  8. java8 Stream的实现原理 (从零开始实现一个stream流)

    1.Stream 流的介绍 1.1 java8 stream介绍 java8新增了stream流的特性,能够让用户以函数式的方式.更为简单的操纵集合等数据结构,并实现了用户无感知的并行计算. 1.2  ...

  9. Java8 Stream语法详解 2

    1. Stream初体验 我们先来看看Java里面是怎么定义Stream的: A sequence of elements supporting sequential and parallel agg ...

随机推荐

  1. MATLAB中将mat文件转为txt格式文件

    直接保存为txt文件: 可以用fprintf函数,来代替save函数 比如现在我有一个变量a=[0.1223   345.4544] 如果我想保存它的话,可以用下面的程序: fid = fopen(' ...

  2. JavaScript 词法 All In One

    JavaScript 词法 All In One JavaScript 词法 这部分描述了JavaScript 的词法(lexical grammar). ECMAScript 源码文本会被从左到右扫 ...

  3. NMAP 使用教程!,nmap [Scan Type(s)] [Options] {target specification} , nmap -sn 192.168.2.0/24 , raspberry pi 3

    NMAP 使用教程 https://nmap.org/man/zh/man-briefoptions.html 当Nmap不带选项运行时,该选项概要会被输出,最新的版本在这里 http://www.i ...

  4. Java REPL & JShell

    Java REPL & JShell Java 11 JShell Java Shell https://www.infoq.com/articles/jshell-java-repl/ Th ...

  5. GitHub Sponsors

    GitHub Sponsors https://github.com/sponsors https://github.com/sponsors/xgqfrms?preview=true https:/ ...

  6. Interview of Chinese IT companies Ratings and Reviews website/app

    Interview of Chinese IT companies Ratings and Reviews website/app // js hack const getShitRank = (st ...

  7. taro weapp

    taro weapp 开发指南 https://nervjs.github.io/taro/docs/GETTING-STARTED.html#微信小程序 taro # build $ taro bu ...

  8. js 截取固定长度字符串但不打断单词

    适用地方:英文网站中,比如面包屑导航处新闻标题过长.用css截取文本简介时将单词打断   // 使用方法 // 1.复制下面函数 // 2.调用 // 3.填参数  sliceString(targe ...

  9. Linux安装MySQL_全网最详细

    Linux安装MySQL 编辑于2021-02-19 本文中MySQL安装目录为/usr/local/mysql 上传MySQL安装包至/usr/local目录,执行命令解压 [root@JetXi ...

  10. List转String数组 collection.toArray(new String[0])中new String[0]的语法解释

    Collection的公有方法中,toArray()是比较重要的一个. 但是使用无参数的toArray()有一个缺点,就是转换后的数组类型是Object[]. 虽然Object数组也不是不能用,但当你 ...