作者|楼下hldnm
头图| CSDN从图虫下载
作为List聚集的兄弟Map,我们每天都在用。 一不小心就会踩到坑里。
今天把这些常见的坑总结起来,牵起自己的手,防止后续同学继续踩坑。
本文的设计知识点如下
并不是所有的Map都包括在内
这个踩坑的经历还是在实习的时候发生的。 那时有这样的业务代码。 功能很简单,从XML中读取相关配置并存储在Map中。
代码示例如下:
那时正好有个小需求,需要更改这个业务代码。 的更改过程中,突然想到了HashMap并发进程可能会导致死锁的问题。
因此,我们更改了此代码,并将HashMap更改为ConcurrentHashMap。
滋滋提交了代码。 然后当天上线的时候,我意识到破裂了。
在APP启动过程中发生了NPE问题,导致APP启动失败。
根据异常日志,很快就查明了问题的原因。 由于XML配置问题,读取元素为,元素位于ConcurrentHashMap中,并抛出了空指针异常。
这不科学啊。 一直以来HashMap都可以正常存在,为什么弟弟的ConcurrentHashMap不行呢?
如果翻阅ConcurrentHashMap#put方法的源代码,会在开头看到KV的空判定检查。
看到这里,你有没有怀疑过为什么ConcurrentHashMap和HashMap的设计判断逻辑不同呢?
向万能的谷歌求助,找到了Doug Lea爷爷的答案:
总结:
引起歧义。 如果value为,则不知道是值还是key没有映射具体值。
我不喜欢Doug Lea,以为是隐藏的炸弹。
上面说的Josh Bloch正是HashMap的作者,他和Doug Lea在问题上意见不一致。
因此,ConcurrentHashMap和HashMap的处理可能不同。
最后,粘贴常用的Map子类集合以查看存储情况。
上面的实现系制约,都太不同了,我有点记不住。 其实,在我们加入要素之前,如果自主清空指针进行判断,不存入Map,就可以冷静地避免上述问题。
自定义对象为key
让我们先来看一个简单的例子。 定制Goods商品类,并将其作为密钥存在于Map中。
示例代码如下:
在上面的代码中,第二次添加了同样的商品。 本来,我们期待新添加的值会被原来的旧值代替。 但是,实际上这里没有成功替换,而是添加了一对键值。
让我们看看HashMap#put的源代码。
以下代码基于JDK1.7
在此,首先判断通过混列计算的混列,如果相等则判断equals的结果。 但是,由于没有重写Goods对象的hashCode和equals方法,缺省情况下
hashCode 将会使用父类对象 Object 方法逻辑。而 Object#hashCode 是一个 native 方法,默认将会为每一个对象生成不同 hashcode(与内存地址有关),这就导致上面的情况。
所以如果需要使用自定义对象做为 Map 集合的 key,那么一定记得重写hashCode 与 equals 方法。
然后超帅的网络为自定义对象重写上面两个方法,接下去又可能踩坑另外一个坑。
使用 lombok 的 EqualsAndHashCode 自动重写 hashCode 与 equals 方法。
上面的代码中,当 Map 中置入自定义对象后,接着修改了商品金额。然后当我们想根据同一个对象取出 Map 中存的值时,却发现取不出来了。
上面的问题主要是因为 get 方法是根据对象 的 hashcode 计算产生的 hash 值取定位内部存储位置。
当我们修改了金额字段后,导致 Goods 对象 hashcode 产生的了变化,从而导致 get 方法无法获取到值。
通过上面两种情况,可以看到使用自定义对象作为 Map 集合 key,还是挺容易踩坑的。
所以尽量避免使用自定义对象作为 Map 集合 key,如果一定要使用,记得重写 hashCode 与 equals 方法。另外还要保证这是一个不可变对象,即对象创建之后,无法再修改里面字段值。
错用 ConcurrentHashMap 导致线程不安全
之前的文章『每天都在用 Map,这些核心技术你知道吗?』我们说过 HashMap 是一个线程不安全的容器,多线程环境为了线程安全,我们需要使用 ConcurrentHashMap代替。
但是不要认为使用了 ConcurrentHashMap 一定就能保证线程安全,在某些错误的使用场景下,依然会造成线程不安全。
上面示例代码,我们原本期望输出 1001,但是运行几次,得到结果都是小于 1001。
深入分析这个问题原因,实际上是因为第一步与第二步是一个组合逻辑,不是一个原子操作。
ConcurrentHashMap 只能保证这两步单的操作是个原子操作,线程安全。但是并不能保证两个组合逻辑线程安全,很有可能 A 线程刚通过 get 方法取到值,还未来得及加 1,线程发生了切换,B 线程也进来取到同样的值。
这个问题同样也发生在其他线程安全的容器,比如 Vector等。
上面的问题解决办法也很简单,加锁就可以解决,不过这样就会使性能大打折扣,所以不太推荐。
我们可以使用 AtomicInteger 解决以上的问题。
List 集合这些坑,Map 中也有
上一篇文章中我们提过,Arrays#asList 与 List#subList 返回 List 将会与原集合互相影响,且可能并不支持 add 等方法。同样的,这些坑爹的特性在 Map 中也存在,一不小心,将会再次掉坑。
Map 接口除了支持增删改查功能以外,还有三个特有的方法,能返回所有 key,返回所有的 value,返回所有 kv 键值对。
// 返回 key 的 set 视图Set<K> keySet;// 返回所有 value Collection 视图Collection<V> values;// 返回 key-value 的 set 视图Set<Map.Entry<K, V>> entrySet;这三个方法创建返回新集合,底层其实都依赖的原有 Map 中数据,所以一旦 Map 中元素变动,就会同步影响返回的集合。
另外这三个方法返回新集合,是不支持的新增以及修改操作的,但是却支持 clear、remove 等操作。
示例代码如下:
所以如果需要对外返回 Map 这三个方法产生的集合,建议再来个套娃。
new ArrayList<>(map.values);最后再简单提一下,使用 foreach 方式遍历新增/删除 Map 中元素,也将会和 List 集合一样,抛出 ConcurrentModificationException。
总结
从上面文章可以看到不管是 List 提供的方法返回集合,还是 Map 中方法返回集合,底层实际还是使用原有集合的元素,这就导致两者将会被互相影响。所以如果需要对外返回,请使用套娃大法,这样让别人用的也安心。
第二, Map 各个实现类对于 的约束都不太一样,这里建议在 Map 中加入元素之前,主动进行空指针判断,提前发现问题。
第三,慎用自定义对象作为 Map 中的 key,如果需要使用,一定要重写 hashCode 与 equals 方法,并且还要保证这是个不可变对象。
第三,ConcurrentHashMap 是线程安全的容器,但是不要思维定势,不要片面认为使用 ConcurrentHashMap 就会线程安全。
版权声明:本文为CSDN博主「楼下hldnm」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014634309/java/article/details/105964392