前情回顾:谈谈jdk1.8的HashMap 01
上次我们把put方法讲完了,有一个方法我没有细说,那就是resize()方法,这个方法我个人认为是HashMap最精华但是却最难理解的地方,接下来我们慢慢讲解。
transient Noderesize()方法[] table;//装Node的数组 transient int size; //HashMap的大小 int threshold; //容量 final float loadFactor;//负载因子
resize()方法的作用就是扩容还有初始化。
在该方法中,我们要着重注意old/newCap(旧/新数组长度)和old/newThr(旧/新容量),
看看代码吧,部分解析已经写在了注释里:
//当第一次调用这个方法的是时候,oldCap和oldThr都为0, final Node新旧数组数据迁移[] resize() { Node [] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //这一个判断就可以区别是初始化还是扩容了 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //把长度和阈值都扩大了一倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; } //若执行到这,则是初始化 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; //如果能执行到这里,也就是说调用的是有参构造,oldThr必定是2的整数次幂 else { //运行到这里就表示是调用了无参构造,进行初始化 //16 newCap = DEFAULT_INITIAL_CAPACITY; //12 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //从这里开始把旧数组的数据复制到新数组 Node [] newTab = (Node [])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { //这里我个人认为是方便GC oldTab[j] = null; if (e.next == null) //如果该结点没有后续结点,则直接重新计算在新数组的位置,插入 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode )e).split(this, newTab, j, oldCap); else { // preserve order //下文详细说明 Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
在jdk1.7中,每一个节点都要先经过一个扰动函数的计算后再重新计算下标,而在1.8中,是这么进行的…
如果没有hash冲突,也就是说某节点没有next节点,那就直接和新数组长度取余获得下标,如果有hash冲突,那么就开始一个循环:首先定义四个Node类型变量:
loHead,loTail,hiHead,HiTail(其实就是low和high的缩写),然后遍历该链表,把每一个结点的key的hash值与旧数组长度(假设是2的n次方)相与,目的是为了得到第n位的数是是否为0,如果是0,则把该结点放入以loHead为头,以loTail为尾的链表中,反之,则放入以hiHead为头,以hiHead为尾的链表。过程如下:
hash: 0000 0000 1110 1111
oldCap:0000 0000 0000 1000
相与的结果不是0,因此现在high链表中只有一个结点,接着遍历该结点的next结点,假设
hash: 0000 0000 1110 0111
oldCap:0000 0000 0000 1000
相与的结果是0,那么把该节点放入low链表中,接着遍历下一个结点,同样执行上述操作,如果结果是1,则放入high链表的尾部,重复上述过程直到next结点为null。
最后把low链表放到原位置,把high链表放到原位置+旧数组长度的位置,假设,Node数组长度为16,现在遍历到oldTable[2],发现table[2]发生了哈希冲突,有Node1<1,a>,Node2<2,b>,Node3<3,c>,经过上述操作后,low链表有Node2和Node3,而high链表有Node1,则最后新数组是这样放的,在newTable[2]的地方放入Node2(next结点就是Node3),在newTable[2+16] 的地方放入Node1,结束。
画个图加深理解吧
经过上述运算后
最后
其实HashMap体现了一种懒加载的思想,当我们调用完构造方法后,成员变量里的table是还没有被赋值的,也就是说这个时候table=null,直到第一次执行put方法时才会进行初始化。



