记录一下 Java8 Stream 流操作中 Collectors.toMap() 操作当 value 为 null 时报空指针的问题以及解决方案。


问题代码:

@Override
public Map<Long, String> getEmployeeIdCompanyLicenseNoMap(Collection<Long> employeeIds) {
    if (CollectionUtils.isEmpty(employeeIds)) {
        return Collections.emptyMap();
    }
    List<EmployeeCompanyLicenseNoDTO> relationList = baseMapper.getEmployeeCompanyLicenseNoList(employeeIds);
    if (CollectionUtils.isEmpty(relationList)) {
        return Collections.emptyMap();
    }
    return relationList
            .stream()
            .collect(Collectors.toMap( 
                    EmployeeCompanyLicenseNoDTO::getEmployeeId,
                    EmployeeCompanyLicenseNoDTO::getCompanyLicenseNo);
}

上面这段代码目的是查询员工 id 与其所属公司的营业执照编号的对应关系。其中有些员工是没有公司的,查询出来的营业执照编号将是 null。


运行这段代码后收到一个空指针异常,断点查看 relationList 的数据,其中有部分数据的 companyLicenseNo 字段值为 null。很是疑惑,之前用了那么久的 toMap 方法都没有问题呀?而且 Map 的 value 是允许有多个 null 值的呀?为什么这里就不行了。光是想肯定没有用,带着这些疑问点开源码,看看最终是哪一步调用导致的空指针异常。

Collectors.toMap() 方法
Collectors.toMap() 方法

从图中可以看出来,最终实际上是调用的图片右侧的 toMap 方法,可以看到右侧绿色框的代码,有一句 (map, element) -> map.merge(keyMapper.apply(element),,而 map 根据函数签名上的泛型定义 public static <T, K, U, M extends Map<K, U>> 可以知道,map 变量表示的是 Map 接口,并且调用了 Map 接口的 merge 方法。跟着点开对应源码:

Map 的 merge() 方法定义
Map 的 merge() 方法定义

可以看到,Map 接口中 merge 方法有个默认实现,其中对 value 进行了判空的操作,但是我们可以这里调用 toMap 操作的时候默认 new 的是 HashMap 的实例。而 HashMap 是 Map 的实现类之一,因此我们点开 HashMap 的 merge 方法。

HashMap 的 merge() 方法实现
HashMap 的 merge() 方法实现

可以看到 HashMapmerge 方法也是对 value 进行了判空操作的,因此问题的原因也就找到了。


在网上找了一圈,最终采用的解决方案就是放入 value 的时候判断一下,如果是 null 则放入一个空字符串或者是其他不具有业务含义的值来代替。


以下是其中一种写法:

@Override
public Map<Long, String> getEmployeeIdCompanyLicenseNoMap(Collection<Long> employeeIds) {
    if (CollectionUtils.isEmpty(employeeIds)) {
        return Collections.emptyMap();
    }
    List<EmployeeCompanyLicenseNoDTO> list = baseMapper.getEmployeeCompanyLicenseNoList(employeeIds);
    if (CollectionUtils.isEmpty(list)) {
        return Collections.emptyMap();
    }
    return list
            .stream()
            .collect(Collectors.toMap(
                    EmployeeCompanyLicenseNoDTO::getEmployeeId,
                    it -> Optional.ofNullable(it.getCompanyLicenseNo()).orElse(CommonConstant.EMPTY_STR)));
}