Hashmap的扩容机制及扩容后元素迁移·
一.HashMap基础
HashMap继承了AbstractMap抽象类,实现了Map,Cloneable,Serializable接口。
HashMap的源码属性:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //初始容量为16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //负载因子默认值 static final float DEFAULT_LOAD_FACTOR = 0.75f; //桶上的链表长度大于等于8时,链表转化成红黑树 static final int TREEIFY_THRESHOLD = 8; //桶上的红黑树大小小于等于6时,红黑树转化成链表 static final int UNTREEIFY_THRESHOLD = 6; //当数组容量大于64时,链表才会转化成红黑树 static final int MIN_TREEIFY_CAPACITY = 64;
- 初始容量:默认为16,在第一次put时生成
- 加载因子:默认为0.75
- 阈值:阈值=容量*加载因子。默认12,当元素数量超过阈值时便会触发扩容
- 最大容量:230 ,当容量超过230 ,容量不再扩大,阈值变为231 -1
- java1.8+,桶上的链表长度大于等于8时,链表转化成红黑树
- java1.8+,桶上的红黑树大小小于等于6时,红黑树转化成链表
- java1.8+,只有当数组容量大于64时,链表才会转化成红黑树
二.何时触发扩容
一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的2倍。
注意:
HashMap的容量必须为2的n次幂,进行扩容时其容量变为不小于指定容量的2的幂数(即初始化容量过程)。
例如:进行有参初始化时:
new HashMap<>(5)–>初次put时,容量扩为23 =8,数组长度达到阈值6,扩容为16
new HashMap<>(7)–>初次put时,容量扩为23 =8,数组长度达到阈值6,扩容为16
new HashMap<>(13)–>初次put时,容量扩为24 =16,数组长度达到阈值12,扩容为32
new HashMap<>(19)–>初次put时,容量扩为25 =32,数组长度达到阈值24,扩容为64
三.扩容机制
当HashMap决定扩容时,会调用HashMap类中的resize()方法进行扩容。
java1.7下扩容机制
HashMap的底层结构在java1.7版本之前是:数组+单链表。
resize()源码:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) {//当原有table长度已经达到了上限,不再扩容。 threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
源码可见,当原table容量没有到达最大,则会新建一个新的数组,并调用transfer()方法进行元素迁移(见下)。
- 第一次put时,容量扩容初始化:其容量变为不小于指定容量的2的幂数(默认为16)
- 不是第一次put时,扩容:新容量=旧容量 * 2 ,新阈值=新容量 * 加载因子
元素迁移
transfer()源码:
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } }
在准备好新的数组后,map会遍历数组的每个“桶”,然后遍历桶中的每个Entity,重新计算其hash值(也有可能不计算),找到新数组中的对应位置,以头插法插入新的链表。
注意:
因为是头插法,因此新旧链表的元素位置会发生转置现象。
元素迁移的过程中在多线程情境下有可能会触发死循环(无限进行链表反转),因此HashMap线程不安全。
java1.8+扩容机制
HashMap的底层结构在java1.8版本之后是:数组+单链表/红黑树。
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; 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; // 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) { 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) { 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; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 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) { 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; }
java1.8与java1.7扩容时容量的计算方法都为扩大为原来容量的二倍。
注意:
因为1.8版本之后HashMap的底层结构为:数组+单链表/红黑树。因此如果某个桶中的链表长度大于等于8了,则会判断当前的hashmap的容量是否大于64,如果小于64,则会进行扩容;如果大于64,则将链表转为红黑树。
元素迁移
java1.8+在扩容时,不需要重新计算元素的hash进行元素迁移。
而是用原先位置key的hash值与旧数组的长度(oldCap)进行"与"操作。
- 如果结果是0,那么当前元素的桶位置不变。
- 如果结果为1,那么桶的位置就是原位置+原数组 长度
值得注意的是:为了防止java1.7之前元素迁移头插法在多线程是会造成死循环,java1.8+后使用尾插法
注意:
java1.8 扩容的时候会判断当前的桶的位置有没有链表或者红黑树,如果没有链表或者红黑树,那么当前元素还是和JDK1.7中的求法一样,求新的桶的位置。如果有链表,那么链表的元素会按照上述方法求新的桶的位置。如果是红黑树,则会调用split()方法,将红黑树切分为两个链表,之后进行扩容操作。