Collectors.toMap使用的坑点

Ref

for循环完成List->Map

我们经常会用到 List 转 Map 操作,在过去我们可能使用的是 for 循环遍历的方式,如下所示。

// 简单对象 
@Accessors(chain = true) // 链式方法 
@lombok.Data
class User {
    private String id;
    private String name;
}
复制代码
List<User> userList = Lists.newArrayList(
        new User().setId("A").setName("张三"),
        new User().setId("B").setName("李四"),
        new User().setId("C").setName("王五")
);
复制代码

希望转成 Map 的格式为

A-> 张三 
B-> 李四 
C-> 王五 
复制代码

过去的做法(循环)

Map<String, String> map = new HashMap<>();
for (User user : userList) {
    map.put(user.getId(), user.getName());
}
复制代码

使用 Java8 特性

Java8 中新增了 Stream 特性,使得我们在处理集合操作时更方便了。

以上述例子为例,我们可以一句话搞定

userList.stream().collect(Collectors.toMap(User::getId, User::getName));
复制代码

当然,如果希望得到 Map 的 value 为对象本身时,可以这样写

userList.stream().collect(Collectors.toMap(User::getId, t -> t));

//或
userList.stream().collect(Collectors.toMap(User::getId, Function.identity()));
复制代码

Collectors.toMap 方法的参数

Collectors.toMap 有三个重载方法

// 1
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper);

// 2
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,
        BinaryOperator<U> mergeFunction);

// 3
toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper,
        BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier);
复制代码

参数含义分别是

  • keyMapperKey 的映射函数
  • valueMapperValue 的映射函数
  • mergeFunction:当 Key 冲突时,调用的合并方法
  • mapSupplier:Map 构造器,在需要返回特定的 Map 时使用

还是用上面的例子,如果 List 中 userId 有相同的,使用上面的写法会抛异常

List<User> userList = Lists.newArrayList(
        new User().setId("A").setName("张三"),
        new User().setId("A").setName("李四"), // Key 相同 
        new User().setId("C").setName("王五")
);
userList.stream().collect(Collectors.toMap(User::getId, User::getName));

// 异常:
java.lang.IllegalStateException: Duplicate key 张三 
    at java.util.stream.Collectors.lambda$throwingMerger$114(Collectors.java:133)
    at java.util.HashMap.merge(HashMap.java:1245)
    at java.util.stream.Collectors.lambda$toMap$172(Collectors.java:1320)
    at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
    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 Test.toMap(Test.java:17)
    ...
复制代码

这时就需要调用第二个重载方法,传入合并函数,如

userList.stream().collect(Collectors.toMap(User::getId, User::getName, (n1, n2) -> n1 + n2));

// 输出结果:
A-> 张三李四 
C-> 王五
复制代码

第四个参数(mapSupplier)用于自定义返回 Map 类型,比如我们希望返回的 Map 是根据 Key 排序的,可以使用如下写法

TreeMap是一个有序的 key-value 集合,它是通过红黑树实现的,会根据其键(key)进行自然排序。

List<User> userList = Lists.newArrayList(
        new User().setId("B").setName("张三"),
        new User().setId("A").setName("李四"),
        new User().setId("C").setName("王五")
);
userList.stream().collect(
    Collectors.toMap(User::getId, User::getName, (n1, n2) -> n1, TreeMap::new)
);

// 输出结果:
A-> 李四 
B-> 张三 
C-> 王五 
复制代码

坑点1-Map中的key不能重复

在使用 Collectors.toMap 时候,Map中的 key 不能重复。如下示例,当有重复的 key,会抛出 IllegalStateException 状态异常。

List<User> userList = Lists.newArrayList(
        new User().setId("A").setName("张三"),
        new User().setId("A").setName("李四"), // Key 相同 
        new User().setId("C").setName("王五")
);
userList.stream().collect(Collectors.toMap(User::getId, User::getName));

// 异常:
java.lang.IllegalStateException: Duplicate key 张三 
    at java.util.stream.Collectors.lambda$throwingMerger$114(Collectors.java:133)
    at java.util.HashMap.merge(HashMap.java:1245)
    at java.util.stream.Collectors.lambda$toMap$172(Collectors.java:1320)
    at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
    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 Test.toMap(Test.java:17)
    ...
复制代码

查看其源码,可以发现

public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
复制代码

If the mapped keys contains duplicates (according to Object#equals(Object)), an IllegalStateException is thrown when the collection operation is performed. If the mapped keys may have duplicates, use toMap(Function, Function, BinaryOperator) instead.

因此,若 Map 中有重复的 key,建议使用 toMap(Function, Function, BinaryOperator) 方法进行替换。

坑点2-Map中的value不能为null

在使用 Collectors.toMap 时候,Map中的 value 不能为null,否则会抛出 NullPointerException 异常,如下示例。

User user1 = new User("A","张三");
User user2 = new User("D","李四");
User user3 = new User("C",null);  //value 为null

List<User> list = new ArrayList<>();
list.add(user1);
list.add(user2);
list.add(user3);

Map<String, String> map = list.stream().collect(Collectors.toMap(User::getId, User::getName, (n1, n2) -> n1, TreeMap::new));
System.out.println(map.keySet());
System.out.println(map.values());
复制代码
Exception in thread "main" java.lang.NullPointerException
	at java.util.Objects.requireNonNull(Objects.java:203)
	at java.util.Map.merge(Map.java:1172)
	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 com.lbs0912.java.demo.Solution.main(Solution.java:30)
复制代码

查看源码可以发现,Collectors.toMap 底层是基于 Map.merge 方法来实现的,而 mergevalue 是不能为 null 的。如果为 null,就会抛出空指针异常。

Collectors.toMap() internally uses Map.merge() to add mappings to the map. Map.merge() is spec’d not to allow null values, regardless of whether the underlying Map supports null values. This could probably use some clarification in the Collectors.toMap() specifications.

其解决方案为

  1. 方案1:使用for循环或forEach
Map<String, String> map1 = null;
list.forEach(user -> {
    map1.put(user.getId(),user.getName());
});
复制代码
  1. 使用 stream 的 collect 的重载方法
Map<String, String> map1 = list.stream().collect(HashMap::new,(m,v)-> m.put(v.getId(),v.getName()),HashMap::putAll);   
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享