春招修仙系列 —— hashMap

时间:2019-01-18
本文章向大家介绍春招修仙系列 —— hashMap,主要包括春招修仙系列 —— hashMap使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

参考:https://zhuanlan.zhihu.com/p/21673805 Java 8系列之重新认识HashMap — 美团技术团队,建议直接去看该文章,这里只是一些摘要

存储结构:

HashMap = 数组+ 链表+红黑树(JDK1.8)

关键变量:

/** 哈希表中的默认初始容量(table.length) **/
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 
 /** 哈希表的最大容量 **/
 static final int MAXIMUM_CAPACITY = 1 << 30;

 /** 负载因子的默认值 **/
 static final float DEFAULT_LOAD_FACTOR = 0.75f;

 /** 将链表树化的阀值,即当链表存储量达到多大时,将其转化为树 **/
 static final int TREEIFY_THRESHOLD = 8;

 /** 将树转化为链表的阀值 **/
 static final int UNTREEIFY_THRESHOLD = 6;
 /** 这个字段决定了当hash表的至少大小为多少时,链表才能进行树化。**/
 /**当存储的节点过多时,最好的办法是调整表的大小,使其增大,而不是将链表树化。 **/
 static final int MIN_TREEIFY_CAPACITY = 64;

哈希冲突的解决策略

HashMap采用哈希表存储,解决冲突的策略是链地址法。

注:还有一些其他的解决冲突的办法,比如ThreadLoaclMap中使用的就是开发寻址法中的线性探测法

核心代码:

1 确定哈希桶数组索引的位置(方法一 + 方法二):

方法一:

static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 为第一步 取hashCode值
     // h ^ (h >>> 16)  为第二步 高位参与运算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

方法二:

  static int indexFor(int h, int length) {  //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的
     return h & (length-1);  //第三步 取模运算
}

总结:取Key的hashCode值,高位运算、取模运算

分析:

  • 用 h & (lengh - 1)来得到该对象得保存位,等价于取模运算。这也是为什么哈系桶table的长度大小必须是2的n次方的原因,避免产生0位,0位与任何数做&都为0,会使哈希表中的某些位置一直得不到使用。
  • h^(h>>>16)的目的是让数据的高位和低位都能参与到Hash的计算中,使散列更加的均匀

2 HashMap中的put方法:

1 判断键值对数组table[i] 是否位空或为null,否则执行resize()进行扩容
2 根据键值Key计算hash值得到数组索引 I ,如果table[i] == null ,则直接添加节点,转向第6步,如果table[i]不为空,转到3步
3 判断table[i]的首个元素是否和key一样,如果相同则直接覆盖value,否则转向4,这里的相同指的是 hashCode 以及equals
4 判断table[i]是否是红黑树,如果是,则在树中直接插入数据,否则转向5
5 遍历table[i],判断链表长度是否大于8,大于的话就把链表转换为红黑树,在红黑树里执行插入操作,否则进行链表的插入操作,如果遍历的过程中,发现key已经存在了,直接覆盖value即可
6 插入成功后,该哈希表的复杂率是否大于负载因子,如果超过,进行扩容

3 扩容机制:

Jdk1.7:创建新的数组,哈希表中全部的值重新哈希
Jdk1.8 : 使用2次幂的扩展,所以元素的位置要么在原位置,要么在原位置再移动2次幂的位置,

元素再重新计算hash之后,由于n变为原来的2倍,那么n-1的mask范围再高位多1bit,因此新的index就会发生这样的变化

因此在扩充HashMap的时候,不需要像JDK1.7实现那样,重新计算hash,只要看看原来的hash值新增的那个bit是1还是0就可以了,是0的话,索引没变,是1的话索引变成了 原索引+ “oldCap”,这个设计十分的巧妙:
1 省去了重新计算hash值的时间
2 由于新增的1bit可以认为是随机的,所以它会均匀的将之前的冲突节点分散到新的bucket中
3 JDK1.7在 rehash的时候,旧得链表迁移到新表的时候,如果在新表中的数组索引位置相同,则会出现链表元素的倒置。