Java 8中的stream在项目开发中被同学们用的风生水起,当然大家也踩了不少坑。下面我就来说说Collections.toMap在项目使用中踩的坑,避免大家重复被坑。
一.介绍Collectors.toMap
Collectors.toMap 是 Java 8 中的一个收集器,它可以将流中的元素转换为 Map 对象,其中每个元素的 key 由指定的函数生成。
当我们使用 Collectors.toMap 方法时,可能会遇到重复的 key 问题,这是因为我们在将元素转化为 Map 对象时,如果两个元素具有相同的 key,则会发生冲突,抛出异常。
还可能会遇到value为null的问题,这是因为我们在将元素转化为 Map 对象时,toMap最终是调用了Map.merge方法,merge方法不允许value为null 导致的异常抛出。
二.问题复现与分析以及解决方案
1、Collectors.toMap的key重复问题
问题复现:
public static void main(String[] args) {List<BenefitModel> benefitModelList = new ArrayList<>();benefitModelList.add(new BenefitModel("123", "积分权益"));benefitModelList.add(new BenefitModel("123", "现金权益"));Map<String, String> benefitMap = benefitModelList.stream().collect(Collectors.toMap(BenefitModel::getBenefitId, BenefitModel::getBenefitName));System.out.println(JSON.toJSONString(benefitMap));}
运行结果:
原因分析:
查看Collectors.toMap源码如下,
toMap最终是调用了Map.merge方法,传入的mergeFunction是throwingMerger直接抛出异常,日志信息使用的是第一个参数u。传入的mapSupplier是HashMap对象(HashMap::new)。所以最终会调用到HashMap.merge。
而在HashMap.merge中,对于mergeFunction的应用如下:
在HashMap.merge的语义中,mergeFunction用于合并value,比如对于key的计数,可以使用map.merge(key, 1, Integer::sum)。若不存在则置1,存在则+1。这里的入参是oldValue和newValue。
所以最终传递给throwingMerger的两个参数就不是k-v了。所以报错的所谓Duplicate key其实是oldValue。
解决方案:
- 保证toMap的key不重复
- 调用重载方法,主动指定当key重复时,需要做的合并操作(合并规则可以根据业务需要,自定义)
于是上面重复key的代码优化后为:(合并规则:重复key出现时,取后面的,前面的丢弃)
public static void main(String[] args) {List<BenefitModel> benefitModelList = new ArrayList<>();benefitModelList.add(new BenefitModel("123", "积分权益"));benefitModelList.add(new BenefitModel("123", "现金权益"));Map<String, String> map = benefitModelList.stream().collect(Collectors.toMap(BenefitModel::getBenefitId, BenefitModel::getBenefitName,(k1, k2) -> k2));System.out.println(JSON.toJSONString(map));}
高版本JDK的修复措施:
重复key这个问题在后续版本中得到修复,比如在JDK 11中的处理。
2、Collectors.toMap的value值为null问题
问题复现:
public static void main(String[] args) {List<BenefitModel> benefitModelList = new ArrayList<>();benefitModelList.add(new BenefitModel("123", "积分权益"));benefitModelList.add(new BenefitModel("124", null));Map<String, String> benefitMap = benefitModelList.stream().collect(Collectors.toMap(BenefitModel::getBenefitId, BenefitModel::getBenefitName));System.out.println(JSON.toJSONString(benefitMap));}
运行结果:
原因分析:
有问题,看源码,查看Collectors.toMap源码如下,
toMap最终是调用了Map.merge方法,而在HashMap.merge中,对于value的应用如下:
在HashMap.merge的语义中,value使用前需要进行判空处理,null直接抛出异常NullPointerException。
解决方案:
方案1:先把value为null的数据过滤掉,再用Collectors.toMap。
Map<String, String> map2 = benefitModelList.stream().filter(m -> m.getBenefitName() != null).collect(Collectors.toMap(BenefitModel::getBenefitId, BenefitModel::getBenefitName));
方案2:查资料评价度最好的方案如下。其实跟你方案1中思路-手动foreach一毛一样。
Map<String, String> map2 = benefitModelList.stream().collect(HashMap::new, (m, v) -> m.put(v.getBenefitId(), v.getBenefitName()),HashMap::putAll);
高版本JDK的修复措施:
Collectors.toMap使用时,value值为null,这个问题在Java 11中仍然存在。可能value为null,这种数据很少见,促使解决过程比较缓慢。
三、Collectors.toMap使用总结
综上所以,在使用Collectors.toMap时需要记住几点:
1、key不能有重复,否则会报错IllegalStateException: Duplicate key,因为Map的key不能重复。
2、value不能为空,否则报错NullPointerException。
看完了本文,你可以去搜搜你的项目代码中使用Collectors.toMap的地方,有没有可能踩上面的坑。不要说你的业务数据不会出现重复key的数据,不会出现value值null的情况,上百万的业务数据,什么情况都会有的。
参考资料:java - Ignore duplicates when producing map using streams - Stack Overflow
java - NullPointerException in Collectors.toMap with null entry values - Stack Overflow