数据结构-HashMap总结

207 阅读4分钟

HashMap

简介

  • 什么是HashMap?

哈希表, kv

  • 为什么用HashMap?

数组的特点是:寻址容易,插入和删除困难;而链表的特点是:寻址困难,插入和删除容易。那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表

HashMap是哈希表的实现方式之一, 采用的是“拉链法”, 可以理解为数组链表,是链表的散列。允许Null值和Null键。

数据结构

链表的散列。数组,数组的每个元素是链表。

15236785532298.jpg

包含以下属性:

  • 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操作会发生死循环。

15236852646240.jpg

比如线程1已经完成了迁移,线程2从图中的循环位置继续开始迁移,导致循环链表出现,然后继续迁移到K7,k5,k11,循环链一直存在。

ConcurrentModificationException

关于这个异常,可以参考Javadoc,这里总结说明一下:

可以说这是一个jdk集合框架层提供的能够让开发人员debug用的一个异常,当一个线程在遍历集合,另外一个线程缺在修改集合,会立即抛出。这种会抛出该异常的遍历称作Fail Fast遍历。Fail Fast遍历不光在并发场景下出现,在不使用Itreator的for里删除元素也会抛出。框架层尽最大努力检测这种并发修改,提高debug效率。但是开发人员不能依赖这个异常。

使用时的优化

  • 预估元素个数,指定capacity,避免hash冲突带来的链表遍历的性能损耗,甚至过早rehash