HashMap底层原理
5.4 HashMap底层原理
5.4.1初始长度
// 0000 0001 << 4 = 0001 0000 = 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap的初始长度为16,当不够用时,再扩展,但始终是2的幂次。
5.4.2 put操作原理
/**
** put(K,V) -----> putVal(hash,k,v)
**/
-
计算key对应的hash值;
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
备注:
hash()为什么要有^(h >>> 16)? 答:为了将hashCode的高位和低位一起异或运算,这样算出来的结果分散率较高,避免hash碰撞问题。 h:是int类型的,四个字节,32bit,是通过key(Object类型,Object有一个hashcode()方法获取key的hashCode值)的值计算出来的hash值。 假如 h = 00011010 11000110 11010001 01010111 h>>>16 = 00000000 00000000 00011010 11000110 (按位右移补零操作符) h ^ (h>>>16) = 00011010 11000110 11001011 10010001 (如果相对应位值相同,则结果为0,否则为1)
& 如果相对应位都是1,则结果为1,否则为0 (A&B),得到12,即0000 1100 | 如果相对应位都是 0,则结果为 0,否则为 1 (A | B)得到61,即 0011 1101 ^ 如果相对应位值相同,则结果为0,否则为1 (A ^ B)得到49,即 0011 0001 〜 按位取反运算符翻转操作数的每一位,即0变成1,1变成0。 (〜A)得到-61,即1100 0011 << 按位左移运算符。左操作数按位左移右操作数指定的位数。 A << 2得到240,即 1111 0000 >> 按位右移运算符。左操作数按位右移右操作数指定的位数。 A >> 2得到15即 1111 >>> 按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。 A>>>2得到15即0000 1111
-
初始化一个长度为16的Node<K,V> []数组;
if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
-
根据hash值计算数组下标,判断当前数组是否存有值
(1)当前位置没有值:则new一个node<k,v>存入。
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
备注:
//*********************注*************************// // 数组下标i可以直接取余n(数组长度),如n==16,i = hash%16 = 0~15 但是为什么要有(n-1) & hash ? 如 n-1 = 15 : 0000 1111 hash: 0101 0101 (随着key的不同而不同) & 0000 0101 (1)无论hash值如何变,最终算的i都是低位,即0~(n-1)之间。 (2)由于hash值随着key的不同而不同,所以这样算出来的i分布比较规律。(若使用hash%n算,可能算出的结果是某个i存了很多数据,其他i又没有存任何数据)
(3)规定数组的大小为2的幂次,如32,64....,为了方便计算i的值,使其规律分布。 如 n-1 = 31: 0001 1111 hash: 0110 1010 & 0000 1010 (4)当初始化一个HashMap的长度n时,会返回一个大于等于n的2的幂次。 // 如 Map<String,String> map = new HashMap<>(10); // 实际上内部数组的大小为16 = 2^4
(2)当前位置有值:判断key是否相同
else { Node<K,V> e; K k; //判断当前位置与要put进去得hash值、key值是否相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;//相同直接等于e }
当前位置hash值和key都相同,但是value不同:覆盖新的值,返回旧的值
if (e != null) { // existing mapping for key V oldValue = e.value; //如果onlyIfAbsent为true,或者旧的值为null,则覆盖新的值 if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; }
//key不同
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//(1)
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// (3)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash); //转为红黑树
break;
}
// (2)if() 判断key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
备注:
(1)for循环?
遍历table数组某个位置上的链表,进行尾插法,遍历个数,当链表个数大于8,则需要将链表改为红黑树。
(2)又有 if() 判断key值是否相等?
第一次是判断链表第一个节点的key值是否与put进来的相等,而for循环里面的if是判断链表里面的key值是否与put进来的相等。
(3)TREEIFY_THRESHOLD = 8,当binCount>7时,表示当前链表上已经有8个节点了,此时需要将链表转为红黑树,而新put进来的node节点是先加到链表上,再去转为红黑树(此时已有9个节点)。(jdk1.8)
-
扩容
//每进行修改(put、modify)一次,都会去判断当前容量是否充足,threshhold为阈值。 ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict);
resize()扩容
1.jdk1.7 直接新建一个数组,将老数组中的每一个节点直接转移到新数组。
2.jdk1.8 final Node<K,V>[] resize() { Node<K,V>[] 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; } //原数组的容量翻一倍(<<1)小于最大容量,且原数组容量已超过默认值 //新数组容量翻倍,新阈值也翻倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //自己指定HashMap大小的时候 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { //遍历整个数组(tab) for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //发现数组某个位置存储有值 if ((e = oldTab[j]) != null) { oldTab[j] = null; //这个位置只存了一个节点(既不是链表也不是红黑树) if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //这个位置上存储的是红黑树 看注3 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //这个位置上存储的是链表 else { //高位与低位看注1 Node<K,V> loHead = null, loTail = null;//低位头尾节点 Node<K,V> hiHead = null, hiTail = null;//高位头尾节点 Node<K,V> next; //遍历这个位置上的链表 do { next = e.next; //此处看注2 //如果这个节点是低位的,则给低位链表(尾插法) 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; }
注1:
高位和低位的出现: 由于存储的数组索引是根据(n-1)& hash计算出来的,n是数组的容量大小,由于扩容之后数组的大小改变,所以某数据存储在原数组的索引与扩容之后存储的不同。 前面分析知道扩容后大小变大一倍。假如原数组大小为n=16,则扩容后变为32. 例1:hash=0110 1010 15: 0000 1111 31: 0001 1111 hash: 0110 1010 hash: 0110 1010 & 索引: 0000 1010 0000 1010 可以看出扩容之后所得到索引与原数组的相同。 例2:hash=0111 1010 (改变了第四个数为1,称“特殊位”) 15:0000 1111 31: 0001 1111 hash: 0111 1010 hash: 0111 1010 & 索引: 0000 1010 0001 1010 可以看出扩容之后所得到的索引与原数组存储的索引相差16,即相差原数组大小的距离。 结论:扩容之后,原来某个数据所存储的位置索引i可能与原来的位置相同(称为低位),也可能与原位置相差n大小(称为高位)。 即:相等 / 原+原数组大小 即:看扩容后节点存储在高位还是低位,关键在hash值的特殊位是0还是1,若是0,则低位,否则,为高位。
注2:
看扩容后节点存储在高位还是低位,关键在hash值的”特殊位“是0还是1,若是0,则低位,否则,为高位。 (1)判断此节点存储在低位 (e.hash & oldCap) == 0 //oldCap:是原数组的大小 例1: 假设oldCap = 16 16: 0001 0000 hash: 0110 1010 & 值: 0000 0000 = 0 存低位 (2)判断此节点存储高位 例2: 假设oldCap = 16 16: 0001 0000 hash: 0111 1010 & 值: 0001 0000 != 0 存高位
总结:链表
如果当前位置存储的是链表,在扩容时,我们首先得遍历链表得每一个节点,然后判断当前节点是存储在低位还是高位,低位的生成一个新的链表,然后存储到新数组的低位,高位的生成一个新的链表,然后存储到高位。 低位是指当前节点的hash值与原数组大小进行&运算,若为0则为低位,否则为高位。
注3:红黑树
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
TreeNode:
变化后如上图,一个几点有:next、pre、left、right
//思路和此位置存的是链表的代码相同(看链表)
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
//遍历红黑树(循环链表)
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) { //低位
if ((e.prev = loTail) == null) //第一个
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else { //高位
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
////UNTREEIFY_THRESHOLD=6
if (lc <= UNTREEIFY_THRESHOLD)
//低位个数小于6时,转化为链表(退化)
tab[index] = loHead.untreeify(map);
else {
//不满足条件时,先把头节点存在新数组位置,在判断是否要重新转化为一个新的红黑树
//当hiHead!=null时,表示有节点存在高位,而存在低位的节点需要重新生成一个红黑树
//当hiHead==null时,表示原数组这个位置的所有节点都存在新数组的低位,这样就不用重新生成一个新的红黑树,直接让头节点等于就行了,它自己本来就是一个红黑树了。
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
总结:红黑树
如果当前位置存储的是红黑树,那么扩容时,我需要去遍历当前位置的红黑树的每个TreeNode,并统计存储在低位和高位的个数。如果存在低位的个数 <= 6,就需要将存在低位的TreeNode转化为Node,存在新数组低位的链表中。
如果存在低位的个数 > 6,我们先要判断高位是否有数据,如果没有的话,直接将红黑树存在新数组低位就可以了。
如果有的话,我们就得将存在低位的节点重新生成一个新的红黑树。
5.红黑树(TreeNode)
(1)红黑树的特点:
1.每个节点是红色或黑色的。
2.根节点必须是黑色的。
3.叶子节点是黑色的并且是空节点。
4.节点是红色的,那么他的两个儿子都是黑色。(不能连续出现两个红色节点)
5.对于每个节点,从该节点到其子孙节点的路径上包含的黑节点数目相同。
(2)新增一个节点:
1.新节点是黑色的,不需要调整。
2.新节点是红色的:需要变换(变色+旋转)
(3)变换规则:
1.变色:新增节点的父亲节点和叔叔节点是红色。
(1)把父亲节点、叔叔节点变为黑色;
(2)把祖父(父亲的父亲)节点变为红色;
2.旋转:当前节点的父亲节点是红色,而叔叔节点是黑色。
(1)左旋:当前节点是右子树
以父节点进行左旋
(2)右旋:当前节点是左子树
以祖父节点进行右旋
将父节点变为黑色,祖父节点变为红色。
例子:
①第一步,是否符合红黑树特点:新增节点2,为红色的,树5和2节点不符合红黑树特点4,需要变换。
②第二步,是否需要变色:父节点5和叔叔节点9都是红色,需要变色。
把父节点和叔叔节点变为黑色,祖父节点变为黑色。
③第三步,是否符合红黑树特点:8不符合。变色的条件也不满足,此时就要旋转了。
④第四步,左旋:以父节点左旋。(父节点以及其左子树整个往下,右节点往上充当父节点,把右节点多余的左子树给原父节点,过程自己琢磨)
⑤第五步,是否符合红黑树特点:5和8不符合,且红节点5位于左子树,要进行右旋。
⑥第六步,右旋:以祖父节点右旋。(祖父节点以及其右子树整个往下,祖父节点的左节点此时往上充当新的祖父节点,并将多余的分支给原祖父节点)