点赞再看,养成习惯,微信搜索『程序通事』,关注就完事了!

点击查看更多历史文章

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

今天我就来总结这些常见的坑,再捞自己一手,防止后续同学再继续踩坑。

本文设计知识点如下:

不是所有的 Map 都能包含 null

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

代码示例如下:

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

于是改动了一下这段代码,将 HashMap 修改成了 ConcurrentHashMap

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

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

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

这不科学啊! 之前 HashMap 都没问题,都可以存在 null,为什么它老弟 ConcurrentHashMap 就不可以?

翻阅了一下 ConcurrentHashMap#put 方法的源码,开头就看到了对 KV 的判空校验。

看到这里,不知道你有没有疑惑,为什么 ConcurrentHashMapHashMap 设计的判断逻辑不一样?

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

总结一下:

  • null 会引起歧义,如果 value 为 null,我们无法得知是值为 null,还是 key 未映射具体值?
  • Doug Lea 并不喜欢 null,认为 null 就是个隐藏的炸弹。

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

也许正是因为这些原因,从而导致 ConcurrentHashMapHashMap 对于 null 处理并不一样。

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

上面的实现类约束,都太不一样,有点不好记忆。其实只要我们在加入元素之前,主动去做空指针判断,不要在 Map 中存入 null,就可以从容避免上面问题。

自定义对象为 key

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

示例代码如下:

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

翻看一下 HashMap#put 的源码:

以下代码基于 JDK1.7

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

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

所以如果需要使用自定义对象做为 Map 集合的 key,那么一定记得重写hashCodeequals 方法。

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

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

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

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

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

通过上面两种情况,可以看到使用自定义对象作为 Map 集合 key,还是挺容易踩坑的。

所以尽量避免使用自定义对象作为 Map 集合 key,如果一定要使用,记得重写 hashCodeequals 方法。另外还要保证这是一个不可变对象,即对象创建之后,无法再修改里面字段值。

错用 ConcurrentHashMap 导致线程不安全

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

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

上面示例代码,我们原本期望输出 1001,但是运行几次,得到结果都是小于 1001

深入分析这个问题原因,实际上是因为第一步与第二步是一个组合逻辑,不是一个原子操作。

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

这个问题同样也发生在其他线程安全的容器,比如 Vector等。

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

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

List 集合这些坑,Map 中也有

上一篇文章中我们提过,Arrays#asListList#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 各个实现类对于 null 的约束都不太一样,这里建议在 Map 中加入元素之前,主动进行空指针判断,提前发现问题。

第三,慎用自定义对象作为 Map 中的 key,如果需要使用,一定要重写 hashCodeequals 方法,并且还要保证这是个不可变对象。

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

最后(关注,点赞,转发三连)

你在使用 Map 的过程还踩过什么坑,欢迎留言讨论。

我是楼下小黑哥,我们下篇文章再见~

记住我们的约定,微信搜索『程序通事』,快来关注哦!

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn

编程坑太多,Map 集合怎么也有这么多坑?一不小心又踩了好几个!的更多相关文章

  1. 微信支付.net官方坑太多,我们来精简

    原文:微信支付.net官方坑太多,我们来精简 微信支付官方坑太多,我们来精简 我把官方的代码,打包成了 an.wxapi.dll. 里面主要替换了下注释.呵呵.然后修改了几个地方. 修改一.Confi ...

  2. (1)Map集合 (2)异常机制 (3)File类 (4)I/O流

    1.Map集合(重点)1.1 常用的方法 Set<Map.Entry<K,V>> entrySet() - 用于将Map集合转换为Set集合. 其中Map.Entry<K ...

  3. Java版本:识别Json字符串并分隔成Map集合

    前言: 最近又看了点Java的知识,于是想着把CYQ.Data V5迁移到Java版本. 过程发现坑很多,理论上看大部分很相似,实践上代码写起来发现大部分都要重新思考方案. 遇到的C#转Java的一些 ...

  4. 识别Json字符串并分隔成Map集合

    识别Json字符串并分隔成Map集合 前言: 最近又看了点Java的知识,于是想着把CYQ.Data V5迁移到Java版本. 过程发现坑很多,理论上看大部分很相似,实践上代码写起来发现大部分都要重新 ...

  5. ES6中的Set和Map集合

    前面的话 在ES6标准制定以前,由于可选的集合类型有限,数组使用的又是数值型索引,因而经常被用于创建队列和栈.如果需要使用非数值型索引,就会用非数组对象创建所需的数据结构,而这就是Set集合与Map集 ...

  6. Map集合、散列表、红黑树介绍

    前言 声明,本文用得是jdk1.8 前面已经讲了Collection的总览和剖析List集合: Collection总览 List集合就这么简单[源码剖析] 原本我是打算继续将Collection下的 ...

  7. 遍历Map集合的几种方式

    import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entr ...

  8. Java容器 | 基于源码分析Map集合体系

    一.容器之Map集合 集合体系的源码中,Map中的HashMap的设计堪称最经典,涉及数据结构.编程思想.哈希计算等等,在日常开发中对于一些源码的思想进行参考借鉴还是很有必要的. 基础:元素增查删.容 ...

  9. Java常用的几种集合, Map集合,Set集合,List集合

    Java中  Object是所有类的根 Java集合常用的集合List集合.Set集合.Map集合 Map接口常用的一些方法 size() 获取集合中名值对的数量 put(key k, value v ...

随机推荐

  1. "字母全变小写"组件:<lowercase> —— 快应用组件库H-UI

     <import name="lowercase" src="../Common/ui/h-ui/text/c_text_lowercase">& ...

  2. Elasticsearch系列---聚合查询原理

    概要 本篇主要介绍聚合查询的内部原理,正排索引是如何建立的和优化的,fielddata的使用,最后简单介绍了聚合分析时如何选用深度优先和广度优先. 正排索引 聚合查询的内部原理是什么,Elastich ...

  3. 宏定义#define和内联函数inline的区别

    1 宏定义在预编译的时候进行字符串替换.内联函数在编译的时候进行函数展开. 2 宏定义没有类型检查.内联函数会进行参数列表.返回值等类型检查.

  4. 理解JSON:3分钟课程

    理解JSON:3分钟课程 博客分类: Java综合 jsonAjaxJavaScriptXMLLISP 本文是从 Understanding JSON: the 3 minute lesson 这篇文 ...

  5. 如何配置多个Spring的xml配置文件(多模块配置)

    如何使用多个Spring的xml配置文件(多模块配置) (2009-08-22 13:42:43)   如何使用多个Spring的xml配置文件(多模块配置) 在用Struts Spring Hibe ...

  6. 【论文笔记】张航和李沐等提出:ResNeSt: Split-Attention Networks(ResNet改进版本)

    github地址:https://github.com/zhanghang1989/ResNeSt 论文地址:https://hangzhang.org/files/resnest.pdf 核心就是: ...

  7. android学习笔记——利用BaseAdapter生成40个列表项

    RT: main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns ...

  8. 用Python绘制全球疫情变化地图

    目前全球疫情仍然比较严重,为了能清晰地看到疫情爆发以来至现在全球疫情的变化趋势,我绘制了一张疫情变化地图,完整代码共 230 行,需要的朋友在公众号回复关键字 疫情地图 即可. 废话不多说,先上图 下 ...

  9. Python爬取养眼图片

    1.准备 各位绅士们,你可能会觉得疫情在家无聊,那么现在我们的Python语言可以满足你们的需求.项目需要的工具(1)Python3(2)requests库requests库可以通过代码pip ins ...

  10. vue2.x学习笔记(十二)

    接着前面的内容:https://www.cnblogs.com/yanggb/p/12592256.html. 组件基础 组件化是vue的一个重要特性,也是vue学习中非常重要的一个知识点. 基础示例 ...