在实际项目中我们经常会有 List 转 Map 操作,在过去(JAVA8以前)我们可能使用的是 for 循环遍历的方式,这种方式就不做过多赘述。这里主要讲解使用 Collectors.toMap 方式及使用过程需注意的地方,避免踩坑。

先看看 Collectors.toMap 方法,有三个重载方法:

//方法一:2个参数
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);
}
//方法二:3个参数
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
//方法三:4个参数
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);
}

参数含义分别是:

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

下面分别对三个重载方法进行演示:

目录

方法一:2个参数,keyMapper,valueMapper。

方法二:3个参数,keyMapper,valueMapper,mergeFunction。

方法三:4个参数,keyMapper,valueMapper,mergeFunction,mapSupplier。

总结​​​​​​​

方法一:2个参数,keyMapper,valueMapper。

示例:user id没重复的情况下。

public class ToMapTest {

    @Data
    @Accessors(chain = true)
    private static class User {
        private String id;
        private String name;
    }

    public static void main(String[] args) {
        List<User> list = Arrays.asList(
                new User().setId("1").setName("张三"),
                new User().setId("2").setName("李四"),
                new User().setId("3").setName("王五")
        );
        //2个参数的toMap方法
        Map<String, String> map = list.stream()
                .collect(Collectors.toMap(User::getId, User::getName));
        System.out.println(map);
    }
}

结果:

示例:user id有重复的情况下。 

...//其他代码相同
List<User> list = Arrays.asList(
                new User().setId("1").setName("张三"),
                new User().setId("2").setName("李四"),
                //id重复
                new User().setId("2").setName("小李四"),
                new User().setId("3").setName("王五")
        );
...//其他代码相同

结果:直接异常,重复key。

这就是在项目中使用需要注意的地方,当要作为key的值有重复的时候,使用2个参数的toMap方法,会直接抛出异常。

顺便提一下,不要认为这只是个小问题,如果没处理好,在线上出的问题,先不说影响大不大,光说排查修复重新发布所需的时间成本,就已经不少了,这也可能是有的人加班少,有的人要加班多的原因。

再看一下源码:

走到这里,就可以看到,如果oldValue有值,那么就会执行抛异常的方法。

那么,如何避免这种情况呢?JAVA早有考虑:可以使用3个参数的toMap方法。

方法二:3个参数,keyMapper,valueMapper,mergeFunction。

//3个参数的toMap方法
Map<String, String> map = list.stream().collect(
        Collectors.toMap(
                User::getId,
                User::getName,
                //v1、v2表示两个重复key的value,下面是对value的处理,可根据自己业务需求来
                (v1, v2) -> v1 + "、" + v2
        )
);

 结果:可以看到没有出现异常,结果对value进行了处理。 

这里还需要注意一点:

HashMap对象的key、value值均可为null。

但是,toMap的话,value为null,也是会抛异常-空指针异常!

看下示例:

List<User> list = Arrays.asList(
                new User().setId("1").setName("张三"),
                new User().setId("2").setName("李四"),
                new User().setId("3").setName("王五"),
                //没有设置name,name为null。
                new User().setId("9")
        );

结果: 

看下源码: 

 还是调用map的merge方法这里:

不言而喻了。

 如果value可能出现null,则需要在toMap()的第二个参数进行空(null)值的判断逻辑;例如:

Map<String, String> map = list.stream().collect(
        Collectors.toMap(
                User::getId,
                //value的空值判断
                u->u.getName() == null? "未知" : u.getName(),
                (v1, v2) -> v1 + "、" + v2
        )
);

最后,再来看看4个参数的。

方法三:4个参数,keyMapper,valueMapper,mergeFunction,mapSupplier。

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

//4个参数的toMap方法
Map<String, String> map = list.stream().collect(
        Collectors.toMap(
                User::getId,
                u->u.getName() == null? "未知" : u.getName(),
                (v1, v2) -> v1 + "、" + v2,
                TreeMap::new
        )
);

结果:

总结

Collectors.toMap 确实带来方便,但是,与此同时,也需要注意两点(你也可以认为是2个坑):

  1. 需考虑是否有key重复情况;
  2. 需考虑是否有value为null情况。

以上两点在阿里的Java开发手册中也明确指出:

 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐