Map 集合怎么也有这么多坑?一不当心又踩了好几个!

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

上一篇 List 踩坑文章中,咱们提到几个比较容易踩坑的点。做为 List 集合好兄弟 Map,咱们也是每天都在使用,一不当心也会踩坑。html

今天我就来总结这些常见的坑,再捞本身一手,防止后续同窗再继续踩坑。安全

本文设计知识点以下:多线程

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

不是全部的 Map 都能包含 null

这个踩坑经历仍是发生在实习的时候,那时候有这样一段业务代码,功能很简单,从 XML 中读取相关配置,存入 Map 中。并发

代码示例以下:ide

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

那时候正好有个小需求,须要改动一下这段业务代码。改动的过程当中,忽然想到 HashMap 并发过程可能致使死锁的问题。性能

因而改动了一下这段代码,将 HashMap 修改为了 ConcurrentHashMap线程

美滋滋提交了代码,而后当天上线的时候,就发现炸了。。。设计

应用启动过程发生 NPE 问题,致使应用启动失败。3d

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

根据异常日志,很快就定位到了问题缘由。因为 XML 某一项配置问题,致使读取元素为 null,而后元素置入到 ConcurrentHashMap 中,抛出了空指针异常。指针

这不科学啊!以前 HashMap 都没问题,均可以存在 null,为何它老弟 ConcurrentHashMap 就不能够?

Map 集合怎么也有这么多坑?一不当心又踩了好几个!
翻阅了一下 ConcurrentHashMap#put 方法的源码,开头就看到了对 KV 的判空校验。

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

看到这里,不知道你有没有疑惑,为何 ConcurrentHashMapHashMap 设计的判断逻辑不同?

求助了下万能的 Google,找到 Doug Lea 老爷子的回答:

Map 集合怎么也有这么多坑?一不当心又踩了好几个!来源:http://cs.oswego.edu/pipermail/concurrency-interest/2006-May/002485.html

总结一下:

  • null 会引发歧义,若是 value 为 null,咱们没法得知是值为 null,仍是 key 未映射具体值?
  • Doug Lea 并不喜欢 null,认为 null 就是个隐藏的炸弹。

上面提到 Josh Bloch 正是 HashMap 做者,他与 Doug Lea 在 null 问题意见并不一致。

也许正是由于这些缘由,从而致使 ConcurrentHashMapHashMap 对于 null 处理并不同。

最后贴一下经常使用 Map 子类集合对于 null 存储状况:

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

上面的实现类约束,都太不同,有点很差记忆。其实只要咱们在加入元素以前,主动去作空指针判断,不要在 Map 中存入 null,就能够从容避免上面问题。

自定义对象为 key

先来看个简单的例子,咱们自定义一个 Goods 商品类,将其做为 Key 存在 Map 中。

示例代码以下:

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

上面代码中,第二次咱们加入一个相同的商品,本来咱们指望新加入的值将会替换原来旧值。可是实际上这里并无替换成功,反而又加入一对键值。

翻看一下 HashMap#put 的源码:

如下代码基于 JDK1.7

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

这里首先判断 hashCode 计算产生的 hash,若是相等,再判断 equals 的结果。可是因为 Goods对象未重写的hashCodeequals 方法,默认状况下 hashCode 将会使用父类对象 Object 方法逻辑。

Object#hashCode 是一个 native 方法,默认将会为每个对象生成不一样 hashcode与内存地址有关),这就致使上面的状况。

因此若是须要使用自定义对象作为 Map 集合的 key,那么必定记得重写hashCodeequals 方法。

而后当你为自定义对象重写上面两个方法,接下去又可能踩坑另一个坑。

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

使用 lombok 的 EqualsAndHashCode 自动重写 hashCodeequals 方法。

上面的代码中,当 Map 中置入自定义对象后,接着修改了商品金额。而后当咱们想根据同一个对象取出 Map 中存的值时,却发现取不出来了。

上面的问题主要是由于 get 方法是根据对象 的 hashcode 计算产生的 hash 值取定位内部存储位置。

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

当咱们修改了金额字段后,致使 Goods 对象 hashcode 产生的了变化,从而致使 get 方法没法获取到值。

经过上面两种状况,能够看到使用自定义对象做为 Map 集合 key,仍是挺容易踩坑的。

因此尽可能避免使用自定义对象做为 Map 集合 key,若是必定要使用,记得重写 hashCodeequals 方法。另外还要保证这是一个不可变对象,即对象建立以后,没法再修改里面字段值。

错用 ConcurrentHashMap 致使线程不安全

以前的文章『天天都在用 Map,这些核心技术你知道吗?』咱们说过 HashMap 是一个线程不安全的容器,多线程环境为了线程安全,咱们须要使用 ConcurrentHashMap代替。

可是不要认为使用了 ConcurrentHashMap 必定就能保证线程安全,在某些错误的使用场景下,依然会形成线程不安全。

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

上面示例代码,咱们本来指望输出 1001,可是运行几回,获得结果都是小于 1001

深刻分析这个问题缘由,其实是由于第一步与第二步是一个组合逻辑,不是一个原子操做。

ConcurrentHashMap 只能保证这两步单的操做是个原子操做,线程安全。可是并不能保证两个组合逻辑线程安全,颇有可能 A 线程刚经过 get 方法取到值,还将来得及加 1,线程发生了切换,B 线程也进来取到一样的值。

这个问题一样也发生在其余线程安全的容器,好比 Vector等。

上面的问题解决办法也很简单,加锁就能够解决,不过这样就会使性能大打折扣,因此不太推荐。

咱们可使用 AtomicInteger 解决以上的问题。

Map 集合怎么也有这么多坑?一不当心又踩了好几个!

List 集合这些坑,Map 中也有

上一篇文章中咱们提过,Arrays#asListList#subList 返回 List 将会与原集合互相影响,且可能并不支持 add 等方法。一样的,这些坑爹的特性在 Map 中也存在,一不当心,将会再次掉坑。

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 集合怎么也有这么多坑?一不当心又踩了好几个!

因此若是须要对外返回 Map 这三个方法产生的集合,建议再来个套娃。

new ArrayList<>(map.values());

最后再简单提一下,使用 foreach 方式遍历新增/删除 Map 中元素,也将会和 List 集合同样,抛出 ConcurrentModificationException

总结

从上面文章能够看到不论是 List 提供的方法返回集合,仍是 Map 中方法返回集合,底层实际仍是使用原有集合的元素,这就致使二者将会被互相影响。因此若是须要对外返回,请使用套娃大法,这样让别人用的也安心。

第二, Map 各个实现类对于 null 的约束都不太同样,这里建议在 Map 中加入元素以前,主动进行空指针判断,提早发现问题。

第三,慎用自定义对象做为 Map 中的 key,若是须要使用,必定要重写 hashCodeequals 方法,而且还要保证这是个不可变对象。

第三,ConcurrentHashMap 是线程安全的容器,可是不要思惟定势,不要片面认为使用 ConcurrentHashMap 就会线程安全。

相关文章
相关标签/搜索