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

点击查看更多历史文章

上一篇 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. 三、ARP协议和ICMP协议

    一.ARP协议 网络设备有数据要发送到另一台网络设备时,必须要知道对方的网络层地址(IP).IP地址由网络层来提供,但是仅有IP地址是不够的,IP数据报文必须封装成帧才能通过数据链路进行发送.数据帧必 ...

  2. spark本地开发环境搭建及打包配置

    在idea中新建工程 删除新项目的src,创建moudle 在父pom中添加spark和scala依赖,我们项目中用scala开发模型,建议scala,开发体验会更好(java.python也可以) ...

  3. Mac OS安装docker

    MacOS Docker 安装 使用 Homebrew 安装 macOS 我们可以使用 Homebrew 来安装 Docker. Homebrew 的 Cask 已经支持 Docker for Mac ...

  4. AJ学IOS(03)UI之纯代码实现UI——图片查看器

    AJ分享,必须精品 先看效果 主要实现类似看新闻的一个界面,不用拖拽,纯代码手工写. 首先分析app可以很容易知道他这里有两个UILabel一个UIImageView还有两个UIButton 定义UI ...

  5. Python数据可视化---pygal模块

    目录 pygal模块 安装pygal模块 pygal模块介绍 柱状图 单列柱状图 堆叠柱状图 横向柱状图 折线图 简单折线图 纵向折线图 堆叠折线图 饼状图 简单饼状图 多级饼状图 圆环图 半圆图 雷 ...

  6. 利用浏览器的console篡改cookie

    背景: 最近公司有个客户问题,是由于浏览器的cookie中多记录过期的session id导致重复登录,普通操作无法复现,因此尝试进行cookie篡改复现问题. 方法: 首先,要知道软件定义的sess ...

  7. 远程登录redis

    没想到吧,我居然已经摸到了redis. 远程登录redis redis-cli -h 127.0.0.1 -p 6379 ip地址和端口记得换成自己的

  8. 4. git log的常见用法

    git log ======见https://blog.csdn.net/daguanjia11/article/details/73823617 +++++++++++++++++++++++ 使用 ...

  9. 从零开始学AB测试:躲坑篇

    AB测试的原理很简单,只用到了最简单的统计假设检验,但表面的简单通常都隐藏着陷阱,这一点没有经过实践的摸爬滚打是不容易看到的,今天我就把前人已经踩过的坑,一共15个,给大家分享一下.在分享之前,大家脑 ...

  10. C# WCF的通信模式

    wcf 通信模式一般分为三种; 1,请求/响应模式 2,单工模式 3,双工模式 一,请求/响应模式 请求/响应通信是指客户端向服务端发送消息后,服务端会向客户端发送响应.这也意味着在接收到服务的响应以 ...