• Collectors.toMap的暗坑与避免方式


    使用Java的stream中的Collectors可以很方便地做容器间的转换,可以少写很多代码。但是其中有暗含的坑需要注意和避免,本文探讨Collectors.toMap(JDK8版本)。

    Collectors.toMap可以将一个流转化成Map,常见于需要将List转换成Map以便于进一步操作的场景,比如在通过RPC接口获取一个返回结果、从DB中查询到匹配的多条数据后,对其按某个字段(经常是主键id)做分组。这里先定义一个简单的类:

    public class User {
        private Long id;
    
        private String name;
    }
    

    后续所有代码的目的均为将这个User类组成的List转换为id作为key、name作为value的Map。

    1. 不使用stream

    不使用stream时,需要先new一个map,然后手动把list的每一项放入map

        public void test0() {
            User user1 = new User();
            user1.setId(1L);
            user1.setName("1");
    
            User user2 = new User();
            user2.setId(2L);
            user2.setName("2");
    
            List list = Lists.newArrayList();
            list.add(user1);
            list.add(user2);
            Map map = new HashMap<>();
            for(User user : list) {
                map.put(user.getId(), user.getName());
            }
        }
    

    2. 使用stream的Collectors.toMap

    构造list的代码不变,转化map的代码可以简化为

    Map map = list.stream().collect(Collectors.toMap(User::getId, User::getName));
    

    相较第一种,简洁多了。
    但是,如果User1和User2的id是一样的,会发生什么情况?将代码中user2.setId(2L)修改为user2.setId(1L),再次执行,发现抛异常java.lang.IllegalStateException: Duplicate key 1,说明在merge时报错了:key不允许重复。

    但是有些情况下key确实是可以重复的,比如我调用上游的数据,上游没做校验和控制;再或者这个字段本身不是惟一的,多个数据可能重复。那么如何改进呢?

    3. Collectors.toMap指定merge函数

    可以自定义一个merge函数来确定key重复时,如何取value。比如下面这种写法,是保留第一个value。你也可以保留第二个,或者是做一些更复杂的处理。

    Map map = list.stream().collect(Collectors.toMap(User::getId, User::getName, (x1,x2)->x1));
    

    4. value为null的场景

    传入merge方法以后,看似万事大吉了,没想到还有坑。将user2.setName(null),会发现抛了NullPointerException异常,异常栈信息为:

    可以看到对应的源码里,value是不允许为null的。

    虽然说Map的value是支持null值的,但是map自己的merge方法天生不支持,此时仅靠自定义merge方法也已经无能为力了。如果仍然想使用Collectors.toMap,需要手动处理null的值,比如:

    Map map = list.stream().collect(Collectors.toMap(User::getId, value -> Optional.ofNullable(value.getName()).orElse("")));
    

    当然,这样处理后的map的value并不是实际的值,并不适用于所有场景。
    这样看来,Collectors.toMap的局限性无法避免了,使用的时候要确认不会发生预期的问题。

    当然你也可以做一些预处理,比如使用filter过滤掉value=null的数据,来规避这个问题。

    5. key=null时会怎么样?

    不会怎么样,一切正常。

    public void test5() {
        User user1 = new User();
        user1.setId(null);
        user1.setName("1");
    
        User user2 = new User();
        user2.setId(2L);
        user2.setName("2");
    
        List list = Lists.newArrayList();
        list.add(user1);
        list.add(user2);
        Map map = list.stream().collect(Collectors.toMap(User::getId, User::getName));
        System.out.print(map);
    }
    

    6. 还有什么要注意的?

    上述的例子,均是建立在list不为空的前提下进行的。如果list本身为null,在调用stream()时自然也会抛NullPointerException;如果list是空的(内容为空但是容器本身初始化过,如list = new ArrayList<>()),则不会报错。

    小结

    • 调用stream()前先确定容器本身是否为null
    • 如果不确定要通过Collectors.toMap转换为map的源容器的数据
      • 对应key是否会重复,可以在toMap()传入merge方法
      • 对应value是否为null,可以做过滤或指定默认值
    • 如果不想思考和求证,还是继续用for循环吧
  • 相关阅读:
    docker运维之自定义网络配置
    gradio的基础教程
    计算机毕业设计ssm高校教师科研能力评定系统40n60系统+程序+源码+lw+远程部署
    基于单片机GSM的防火防盗系统的设计
    MySQL8.0优化 - SQL执行流程
    敲桌子案例
    虚拟机构建部署单体项目及前后端分离项目
    JAVA并发编程--7 在编程过程中怎么避免死锁
    [Spring] @Bean 修饰方法时如何注入参数
    亿级万物互联新时代的物联网消息中间件EMQX调研
  • 原文地址:https://www.cnblogs.com/wuyuegb2312/p/18005914