当Map的值为NULL

最近在开发过程中遇到了一个非常令人费解的情况,排查了将近一早上还没找到头绪,还是在师兄的帮助下,才发现了问题。

问题复现

先看一段代码:

// 调用远程RPC方法,获取map
Map<String, Object> map = remoteMethod.queryMap();
// 如果包含对应key,则进行业务处理
if(map.contains(KEY)) {
    String value = (String)map.get(KEY);
    System.out.println(value);
}

这段代码乍一看,逻辑没有什么问题,如果查询到的map包含这个key,就拿到这个key的值,并转为String类型,并打印这个value。
但是事实上,程序会偶发的打印出 null ,所以我就开始排查是否是因为map.get(KEY) = null,排查方式如下:

// 调用远程RPC方法,获取map
Map<String, Object> map = remoteMethod.queryMap();
// 如果包含对应key,则进行业务处理
System.out.println(JSON.toJsonString(map));

结果对应的输出也只是一个非常简单的{}字符串,这个时候我就非常懵逼了,明明map中没有对应的key,为啥还会执行map.contains(KEY)的逻辑呢?

问题原因

最后,经过排查,发现了两个问题

  1. HashMap的value可以为null,这就意味着虽然hashMap.get(KEY) == null,但是hashMap.contains(KEY) == true也是完全可以存在的。这就是第一段代码出现问题的原因
  2. Fastjson和Gson默认不会打印value为null的键值对,即当hashMap.get(KEY) == null && hash.contains(KEY)时,常见的json框架如没有特殊制定的情况下,是不会将{KEY:null}打印出来的,这也是第二段代码没有排查出来问题的原因

结合这两个问题来看,原因就十分明显了,远程的RPC服务返回的Map中的value包含null值,导致仍然可以进入正常的业务逻辑中;同时,又因为没有json默认不会打印value=null的键值对,所以导致了问题排查的困难

解决方案

在开发过程中,尤其是调用外部接口或者从数据库获取数据的时候,由于Java是强类型,我们都喜欢把扩展字段的类型封装成Map<String, Object>,然后再根据具体的场景将value强转为不同的类型(如String),这个时候,无论是#get还是#put都需要相当注意:

  1. #put方法在使用的时候,如果value=null,就尽量不要put进去
  2. 在要对value处理的情况下,尽量不要使用#contains判断key的存在,要使用(V = (map.get(KEY))) != null会更好一点

感悟总结

Map实现类的区别,是面试很常见的一个问题,其中的一个不同点就是key=null和value=null的情况,但是在看书的时候只是匆匆略过,知道有这么一个事情,有点不以为意,只有真正踩坑的时候,才明白有多重要~

HashMap: key和value均可以为null
HashTable:key和value均不可以为null
ConcurrentHashMap:key和value均不可以为null

其实,仔细想了一下,常见的json框架默认不打印{KEY:null}也是有原因的,因为如果很多json2Str方法的作用都是打印日志或者持久化到数据库中的,如果把大量的value=null打印出来,尤其是在数据库持久化的时候,会产生大量无用的数据,多少有点浪费的。不过跟资金相关的日志,建议还是把null值打印出来比较好,更方便排查问题了:)

### 解决 Java Stream 将 List 转换为 Mapnull 的问题 当使用 `Stream` API 将 `List` 转换为 `Map` 时,如果存在键冲突,默认情况下会保留第一个遇到的并丢弃后续具有相同键的。这可能导致某些条目的丢失或被覆盖,从而使得最终映射中的部分可能为 `null` 或不符合预期。 为了防止这种情况发生,在收集器中可以定义合并函数来处理键冲突的情况。具体来说,可以通过提供自定义逻辑决定如何组合两个不同对象以应对重复键的情形: ```java private static void solutionWithoutNullValues() { List<ClazzDuplicateKeyInCollectMap> list = Arrays.asList( new ClazzDuplicateKeyInCollectMap(1, 10), new ClazzDuplicateKeyInCollectMap(1, 20), new ClazzDuplicateKeyInCollectMap(2, 30)); // 使用 mergeFunction 来避免为空的问题 Map<Integer, ClazzDuplicateKeyInCollectMap> map = list.stream().collect(Collectors.toMap( ClazzDuplicateKeyInCollectMap::getKey, Function.identity(), (existing, replacement) -> existing != null ? existing : replacement)); System.out.println(map); } ``` 上述代码片段展示了通过指定 `(existing, replacement)` 合并策略的方式确保即使遇到相同的键也不会导致目标 `Map` 中存储 `null` [^1]。 另外一种方法是在构建 `Map` 之前先过滤掉那些可能会引起冲突的数据项,或者提前设定好默认以便更好地控制结果集的内容。对于更复杂的场景,则可以根据业务需求调整合并规则,比如累加数、选取最大/最小等操作。 #### 处理潜在的 Null 键或 除了处理因键冲突而产生的 `null` 外,还需要考虑输入列表本身是否含有带有 `null` 键或的对象实例。为了避免这些情况影响到最终的结果,可以在流管道中加入额外的筛选条件: ```java // 排除 key 或者 valuenull 的元素后再进行转换 map = list.stream() .filter(e -> e.getKey() != null && e.getValue() != null) .collect(Collectors.toMap(ClazzDuplicateKeyInCollectMap::getKey, Function.identity())); ``` 这样就可以有效减少由于数据质量问题带来的不确定性因素。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值