HashMap 源码分析
-
HashMap 的内部数据结构是怎样的?
答:数组+链表 具体结构如下图:
验证:数组、链表(单向)数组:
transient Node<K,V>[] table;
Node 结构如下(链表):
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; 指向下一个节点(单向链表) Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
2.hash 的作用?
map.put(1,“javck”)
-----Node(key=1,value="jack",....) 组装成功后。它该落到那边?数组上or 链表上?
答:1>.一定是要经过数组。数组的默认大小为16. 具体源码中是通过
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
使用位移算法的好处?位移运算效率高。
2>.一定要得到一个数组的下标位置,
为什么不使用 Random.next(16)? 随机得到一个值呢?
答:因为这样重复几率高,会出现链表节点上Node 节点分布不均匀
因此引进一个算法 hash这个算法是用于得到数组下标的前戏。
(1)根据hash 算法得到一个整型数
(2)控制在 0-15之间
hash 算法 :用 key.hashCode()高16位 和低16位 进行一个异或运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
-
请你描述下 put 的过程,流程是怎么的,在源码中是如何体现的?
a.先根据hash 算法得到一个 结果 :1484515
b.先判断当前 Node 节点的组是否为空,如果为空,先初始化这个数组,源码如下:
判断是否为空
对变量进行初始化赋值:
else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); }
初始化数组大小:
@SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
c.根据key,value 值组装成 Node 节点 然后计算初下标的位置 (根据hash 算法的值 “&”n-1 得到下标的位置 ),判断该位置是被占用
if ((p = tab[i = (n - 1) & hash]) == null) //计算下标i的位置 说明:n-1=15的二进制是01111表示 & 之前算的hash 值 得到的结果必然在 0~15之间,为什么不用前面计算出的hash来%16算出下标的位置?因为 二进制& 运算比% 运算效率高的多。 tab[i] = newNode(hash, key, value, null);
d.如果!=null 说明此时数组下标tab[i ]有值,则需要将该Node节点存储到链表上。
else { Node<K,V> e; K k; //e临时变量 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) //说明key相等 e = p; //将p 赋值到临时 e else if (p instanceof TreeNode)//如果当前节点属于红黑树Node 则用红黑树的存储方式 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //使用普通的Node链表进行存储。 p.next = newNode(hash, key, value, null); //如果链表的长度大于8,则转成红黑树的存储方式。 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果临时变量有值 证明 key相等,接下来需要将之前的value进行覆盖 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } }
e.判断数组的一个容量大小的改变,扩容机制
if (++size > threshold) resize(); //另外一个功能就是进行扩容 afterNodeInsertion(evict);
扩容 :扩容的前提, 当16*n <16进行扩容 比如说12
扩容的标准:数组的大小*0.75=某个值,当大于这个值的时候进行扩容。
2倍扩容,为什么是2的整数倍进行扩容呢?源码描述如下:
至于为什么是2的整数倍呢?
16->32对新数组进行使用,将原来的数组移动到新数组上
(1)数组有元素,下面为null的时候:
(2)数组位置有元素,下面不为null,链表,
else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //正常情况下 是计算在数组上的位置 if ((e.hash & oldCap) == 0) { // 判断001010101010 // 01111 & // 10000 & // ------------------------ // 00000 判断这个结果是否为0 ,如果到数第五位是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; //要么原来的位置+原来总容量大小。 } }
(3)数组位置有元素,下面不是null,红黑树
问题
-
为什么HashMap的长度是2的整数次幂?
1.计算 Noed节点下落数组中的位置方式是: hash(KEY) & (length - 1)
假设现在数组的长度 length 可能是偶数也可能是奇数
length 为偶数时,length-1 为奇数,奇数的二进制最后一位是 1,这样便保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 hash的值)即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性。
例如 length = 4,length - 1 = 3, 3 的 二进制是 11 若此时的 hash 是 2,也就是 10,那么 10 & 11 = 10(偶数位置) hash = 3,即 11 & 11 = 11 (奇数位置)
而如果 length 为奇数的话,很明显 length-1 为偶数,它的最后一位是 0,这样 hash & (length-1) 的最后一位肯定为 0,即只能为偶数,这样任何 hash 值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间
因此,length 取 2 的整数次幂,是为了使不同 hash 值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。