HashMap源码分析
Posted milicool
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap源码分析相关的知识,希望对你有一定的参考价值。
一、要点
1. 如何减少哈希碰撞
1. 将哈希桶长度设置为2的倍数,这样在计算下标时(n-1)& hash 的(n-1)二进制最后一位也会参与运算,
2. 当Map中元素增加时,势必会造成碰撞的增加,这时候通过扩容来,来减少碰撞
2. 何时初始化HashMap
在put值时,初始化hashMap
3. 哈希桶的寻址方法
计算下标的算法 (n-1)& hash
4. 链表何时转红黑树
当哈希桶中链表长度大于7时,则链表转红黑树,因为红黑树的查找效率更高
5. 扩容时扩大几倍
扩大两倍,同时阈值也同样扩大两倍
6. 为什么hashMap容量都是2的倍数
1. 计算下标的算法是 (n-1)& hash
2. indexFor代码,正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问,
3. 以初始长度16为例,16-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。
7. 讲一下Put的过程
1. 哈希桶为空的话,调用扩容函数初始化哈希桶,默认长度16
2. (n-1)& hash计算下标,不发生hash碰撞的话,直接赋值
3. 发生hash碰撞,如果链头的key值就相同,直接替换,如果是红黑树就进入红黑树对比赋值
4. 否则遍历链表,比较赋值(比较方式hash+equals)
5. 最后判断是否需要扩容
8. 讲一下resize()过程
1. 判断是否需要初始化哈希桶的容量值、阈值
2. 遍历老数组,用hash值重新计算下标位置,要么将原来的链表放入低位、要么将要来的链表放入高位
9. 能否让HashMap同步
Map m = Collections.synchronizeMap(hashMap);
10. HashMap的长度为什么设置为2的n次方
1. 在寻址过中,一般用取余的方式来,这样的效率不高,当容量为2次方时,按位运算&上length-1时,效果和取余相同
2. 方便扩容时移位操作,效率高,同时扩容后还是2的次方
二、源码
hash()方法
1. 扰动函数就是为了解决hash碰撞的
key的hash值 异或 hash值的低16位
static final int hash(Object key) int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
put()方法
public V put(K key, V value) return putVal(hash(key), key, value, false, true); final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) Node<K,V>[] tab; Node<K,V> p; int n, i; // 判断数组是否为空 if ((tab = table) == null || (n = tab.length) == 0) // 数组为空则初始化数组, 并获取长度 n = (tab = resize()).length; // 判断数组中值是否为空,index是 哈希值 & 哈希桶长度-1, 代替模运算 if ((p = tab[i = (n - 1) & hash]) == null) // 数组中对应下标放入值 tab[i] = newNode(hash, key, value, null); else // 数组中对应下标不为空,哈希碰撞 Node<K,V> e; K k; // 判断数组取到的第一个节点的key值是否和要存入的相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 则把原来的值替换掉 e = p; else if (p instanceof TreeNode) // 如果p是红黑树, 则进入红黑树存值的流程 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else // 否则p是链表, 且第一个节点key值与要存入的不相同, 则对单项链表进行遍历 for (int binCount = 0; ; ++binCount) // 遍历到节点尾部 if ((e = p.next) == null) // 链表的下个节点为空时, 则放入要存的值 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 链表长度大于8,则将链表转成红黑树 treeifyBin(tab, hash); break; //如果e不是null,说明有需要覆盖的节点 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 遍历下一个节点 p = e; // 判断是否找到了与待插入元素的hash值与key值都相同的元素 if (e != null) // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; // 记录修改次数 ++modCount; //更新size,并判断是否需要扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null;
resize()扩容
final Node<K,V>[] resize() Node<K,V>[] oldTab = table; // 获取旧的数组的长度,和阈值 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 旧的容量大于0的情况 if (oldCap > 0) // 如果数组长度等于最大容量值,则不扩容直接返回 if (oldCap >= MAXIMUM_CAPACITY) // 设置阈值为2的31次方-1 threshold = Integer.MAX_VALUE; // 不扩容了,直接返回旧的数组 return oldTab; else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 如果容量变为2倍后小于最大容量,且大于等于默认容量16时 // 阈值也扩大一倍 newThr = oldThr << 1; // double threshold //如果当前表是空的,但是有阈值。代表是初始化时指定了容量、阈值的情况 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) // 如果新的阈值是0,对应的是 当前表是空的,但是有阈值的情况 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); threshold = newThr; // 扩容重新创建一个大小为2倍的数组 @SuppressWarnings("rawtypes","unchecked") Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 更新哈希桶引用 table = newTab; if (oldTab != null) // 循环将老数组中的放入新数组 for (int j = 0; j < oldCap; ++j) // 当前节点e Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap); // 如果发生了Hash碰撞, 节点小于8,要遍历节点,依次放入新的节点 else // preserve order //因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量 //低位链表的头结点、尾节点 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) 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. 扩容时,hiTail = e表示什么意思
以上是关于HashMap源码分析的主要内容,如果未能解决你的问题,请参考以下文章