HashMap
简介
- 什么是HashMap?
哈希表, kv
- 为什么用HashMap?
数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表
HashMap是哈希表的实现方式之一, 采用的是“拉链法”, 可以理解为数组链表,是链表的散列。允许Null值和Null键。
数据结构
链表的散列。数组,数组的每个元素是链表。
包含以下属性:
- table: Node[],是Node数组, Node是一个链表节点, 包含key,value,next指针
- modCount: 修改次数
- size: 包含的元素个数
- threshold: 阈值,当size大于该值,则开始rehash扩容,增大table数组的长度
- capacity: bucket的数量,上图是16个bucket,即table数组的长度,table为null时,取threshold或者默认值
- loadFactor: 加载因子(好抽象的翻译),threshold = capacity * loadFactor,一个百分数,当元素个数达到容量的一定比例,就需要扩容
<p>The iterators returned by all of this class's "collection view methods"
* are <i>fail-fast</i>: if the map is structurally modified at any time after
* the iterator is created, in any way except through the iterator's own
* <tt>remove</tt> method, the iterator will throw a
* {@link ConcurrentModificationException}. Thus, in the face of concurrent
* modification, the iterator fails quickly and cleanly, rather than risking
* arbitrary, non-deterministic behavior at an undetermined time in the
* future
迭代器里对集合的操作都是快速失败的。
当创建迭代器之后,集合发生了变化(除非是迭代器自己的方法如remove),
迭代器就会抛出一个ConcurrentModificationException异常。
这样的好处是迭代器干净而又快速的失败,而不是冒险在接下来的不确定的时间里发生任意的不确定的行为,主要是考虑并发问题。
因为HashMap是非并发方式实现的,所以这个策略能够快速发现对HashMap的不正常使用(也从而提高了JDK的上帝们的编码体验)
增加元素
Element <k,e>添加到HashMap中。put或者putVal方法。
- put: 存在,则覆盖,并返回老的值,不存在返回null
- putVal: 可以不覆盖
基本逻辑伪代码:
// 获得hash值
hash = hash(k);
/* 根据hash值找到e应该在table中的位置.
BTW:hashmap中的保证了capacity是2的幂,所以没有用%计算余数 */
index = hash % (capacity-1);
/*table[index]中是否存在和k一样的hash值(碰撞检测),如果有碰撞,且k相等(==或者equal),则覆盖,否则加到链表的头部;*/
// ...
为什么添加在表头? 因为是单向链表,所以插在链表末端的复杂度是O(n), 这一点很重要,因为在扩容过程中,新的链表顺序和原来会是相反的。
查找元素
参考添加元素, hash,获取数组的index,遍历链表,比较hash值
查找元素的效率和碰撞的比例有关,如果碰撞较多,在rehash之前,查询会有遍历链表的操作。
删除元素
先查找,再删除。这里有一个异常需要注意:ConcurrentModificationException.
- 如果没有使用迭代器遍历,在遍历过程中删除元素,会抛出该异常。
- 另外并发操作HashMap也会抛出该异常。
Rehash
当size到达阈值,再次put时候回触发rehash。 扩容2倍capacity。newTable大小为原来的2倍. 原来table的数据重新插入到新table中
单个链表(也就是单个数组元素)的迁移如下:
while(null != e){
Node next = e.next;
e.next = newTable[i];
newTable[i].next = e;
e = next;
}
这里会出现并发问题,导致出现循环链表,hashMap操作会发生死循环。
比如线程1已经完成了迁移,线程2从图中的循环位置继续开始迁移,导致循环链表出现,然后继续迁移到K7,k5,k11,循环链一直存在。
ConcurrentModificationException
关于这个异常,可以参考Javadoc,这里总结说明一下:
可以说这是一个jdk集合框架层提供的能够让开发人员debug用的一个异常,当一个线程在遍历集合,另外一个线程缺在修改集合,会立即抛出。这种会抛出该异常的遍历称作Fail Fast遍历。Fail Fast遍历不光在并发场景下出现,在不使用Itreator的for里删除元素也会抛出。框架层尽最大努力检测这种并发修改,提高debug效率。但是开发人员不能依赖这个异常。
使用时的优化
- 预估元素个数,指定capacity,避免hash冲突带来的链表遍历的性能损耗,甚至过早rehash