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

点赞再看,养成习惯,微信搜索『程序通事』,关注就完事了!
点击查看更多历史文章
上一篇 List 踩坑文章中,我们提到几个比较容易踩坑的点。作为 List 集合好兄弟 Map,我们也是天天都在使用,一不小心也会踩坑。
今天我就来总结这些常见的坑,再捞自己一手,防止后续同学再继续踩坑。
本文设计知识点如下:

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

那时候正好有个小需求,需要改动一下这段业务代码。改动的过程中,突然想到 HashMap 并发过程可能导致死锁的问题。
于是改动了一下这段代码,将 HashMap 修改成了 ConcurrentHashMap。
美滋滋提交了代码,然后当天上线的时候,就发现炸了。。。
应用启动过程发生 NPE 问题,导致应用启动失败。

根据异常日志,很快就定位到了问题原因。由于 XML 某一项配置问题,导致读取元素为 null,然后元素置入到 ConcurrentHashMap 中,抛出了空指针异常。
这不科学啊! 之前 HashMap 都没问题,都可以存在 null,为什么它老弟 ConcurrentHashMap 就不可以?

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

看到这里,不知道你有没有疑惑,为什么 ConcurrentHashMap 与 HashMap 设计的判断逻辑不一样?
求助了下万能的 Google,找到 Doug Lea 老爷子的回答:

总结一下:
- null 会引起歧义,如果 value 为 null,我们无法得知是值为 null,还是 key 未映射具体值?
- Doug Lea 并不喜欢 null,认为 null 就是个隐藏的炸弹。
上面提到 Josh Bloch 正是 HashMap 作者,他与 Doug Lea 在 null 问题意见并不一致。
也许正是因为这些原因,从而导致 ConcurrentHashMap 与 HashMap 对于 null 处理并不一样。
最后贴一下常用 Map 子类集合对于 null 存储情况:

上面的实现类约束,都太不一样,有点不好记忆。其实只要我们在加入元素之前,主动去做空指针判断,不要在 Map 中存入 null,就可以从容避免上面问题。
自定义对象为 key
先来看个简单的例子,我们自定义一个 Goods 商品类,将其作为 Key 存在 Map 中。
示例代码如下:

上面代码中,第二次我们加入一个相同的商品,原本我们期望新加入的值将会替换原来旧值。但是实际上这里并没有替换成功,反而又加入一对键值。
翻看一下 HashMap#put 的源码:
以下代码基于 JDK1.7

这里首先判断 hashCode 计算产生的 hash,如果相等,再判断 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 各个实现类对于 null 的约束都不太一样,这里建议在 Map 中加入元素之前,主动进行空指针判断,提前发现问题。
第三,慎用自定义对象作为 Map 中的 key,如果需要使用,一定要重写 hashCode 与 equals 方法,并且还要保证这是个不可变对象。
第三,ConcurrentHashMap 是线程安全的容器,但是不要思维定势,不要片面认为使用 ConcurrentHashMap 就会线程安全。
最后(关注,点赞,转发三连)
你在使用 Map 的过程还踩过什么坑,欢迎留言讨论。
我是楼下小黑哥,我们下篇文章再见~
记住我们的约定,微信搜索『程序通事』,快来关注哦!
欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn
编程坑太多,Map 集合怎么也有这么多坑?一不小心又踩了好几个!的更多相关文章
- 微信支付.net官方坑太多,我们来精简
原文:微信支付.net官方坑太多,我们来精简 微信支付官方坑太多,我们来精简 我把官方的代码,打包成了 an.wxapi.dll. 里面主要替换了下注释.呵呵.然后修改了几个地方. 修改一.Confi ...
- (1)Map集合 (2)异常机制 (3)File类 (4)I/O流
1.Map集合(重点)1.1 常用的方法 Set<Map.Entry<K,V>> entrySet() - 用于将Map集合转换为Set集合. 其中Map.Entry<K ...
- Java版本:识别Json字符串并分隔成Map集合
前言: 最近又看了点Java的知识,于是想着把CYQ.Data V5迁移到Java版本. 过程发现坑很多,理论上看大部分很相似,实践上代码写起来发现大部分都要重新思考方案. 遇到的C#转Java的一些 ...
- 识别Json字符串并分隔成Map集合
识别Json字符串并分隔成Map集合 前言: 最近又看了点Java的知识,于是想着把CYQ.Data V5迁移到Java版本. 过程发现坑很多,理论上看大部分很相似,实践上代码写起来发现大部分都要重新 ...
- ES6中的Set和Map集合
前面的话 在ES6标准制定以前,由于可选的集合类型有限,数组使用的又是数值型索引,因而经常被用于创建队列和栈.如果需要使用非数值型索引,就会用非数组对象创建所需的数据结构,而这就是Set集合与Map集 ...
- Map集合、散列表、红黑树介绍
前言 声明,本文用得是jdk1.8 前面已经讲了Collection的总览和剖析List集合: Collection总览 List集合就这么简单[源码剖析] 原本我是打算继续将Collection下的 ...
- 遍历Map集合的几种方式
import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entr ...
- Java容器 | 基于源码分析Map集合体系
一.容器之Map集合 集合体系的源码中,Map中的HashMap的设计堪称最经典,涉及数据结构.编程思想.哈希计算等等,在日常开发中对于一些源码的思想进行参考借鉴还是很有必要的. 基础:元素增查删.容 ...
- Java常用的几种集合, Map集合,Set集合,List集合
Java中 Object是所有类的根 Java集合常用的集合List集合.Set集合.Map集合 Map接口常用的一些方法 size() 获取集合中名值对的数量 put(key k, value v ...
随机推荐
- 三、ARP协议和ICMP协议
一.ARP协议 网络设备有数据要发送到另一台网络设备时,必须要知道对方的网络层地址(IP).IP地址由网络层来提供,但是仅有IP地址是不够的,IP数据报文必须封装成帧才能通过数据链路进行发送.数据帧必 ...
- spark本地开发环境搭建及打包配置
在idea中新建工程 删除新项目的src,创建moudle 在父pom中添加spark和scala依赖,我们项目中用scala开发模型,建议scala,开发体验会更好(java.python也可以) ...
- Mac OS安装docker
MacOS Docker 安装 使用 Homebrew 安装 macOS 我们可以使用 Homebrew 来安装 Docker. Homebrew 的 Cask 已经支持 Docker for Mac ...
- AJ学IOS(03)UI之纯代码实现UI——图片查看器
AJ分享,必须精品 先看效果 主要实现类似看新闻的一个界面,不用拖拽,纯代码手工写. 首先分析app可以很容易知道他这里有两个UILabel一个UIImageView还有两个UIButton 定义UI ...
- Python数据可视化---pygal模块
目录 pygal模块 安装pygal模块 pygal模块介绍 柱状图 单列柱状图 堆叠柱状图 横向柱状图 折线图 简单折线图 纵向折线图 堆叠折线图 饼状图 简单饼状图 多级饼状图 圆环图 半圆图 雷 ...
- 利用浏览器的console篡改cookie
背景: 最近公司有个客户问题,是由于浏览器的cookie中多记录过期的session id导致重复登录,普通操作无法复现,因此尝试进行cookie篡改复现问题. 方法: 首先,要知道软件定义的sess ...
- 远程登录redis
没想到吧,我居然已经摸到了redis. 远程登录redis redis-cli -h 127.0.0.1 -p 6379 ip地址和端口记得换成自己的
- 4. git log的常见用法
git log ======见https://blog.csdn.net/daguanjia11/article/details/73823617 +++++++++++++++++++++++ 使用 ...
- 从零开始学AB测试:躲坑篇
AB测试的原理很简单,只用到了最简单的统计假设检验,但表面的简单通常都隐藏着陷阱,这一点没有经过实践的摸爬滚打是不容易看到的,今天我就把前人已经踩过的坑,一共15个,给大家分享一下.在分享之前,大家脑 ...
- C# WCF的通信模式
wcf 通信模式一般分为三种; 1,请求/响应模式 2,单工模式 3,双工模式 一,请求/响应模式 请求/响应通信是指客户端向服务端发送消息后,服务端会向客户端发送响应.这也意味着在接收到服务的响应以 ...