JDK1.8 hashMap源码分析
HashMap 1.8 最大的变化就是引入红黑树数据结构。
数据结构为数组+链表+红黑树。当链表的长度大于8,且总的元素大小大于等于64时,将链表修改为红黑树(时间复杂度为 O(logn)),将原来链表数据复制进去。
问题?
1、链表的长度大于8就转为红黑树吗?
不是,需要满足新增元素时链表的长度已经为8,且HashMap的长度大于64,否则只是进行扩容操作
2、为什么选择在链表长度大于8时转红黑树
理想情况下,在随机哈希码下,哈希表中节点的频率遵循泊松分布,而根据统计,忽略方差,列表长度为K的期望出现的次数是以上的结果,可以看到其实在为8的时候概率就已经很小了,再往后调整并没有很大意义。
3、为什么转成红黑树?
因为链表是取一个数需要遍历链表,复杂度为O(N),而红黑树为O(logN)
4、为什么不直接使用红黑树,而是要先使用链表再转红黑树呢?
在HashMap源码的175行有说明,“因为树节点的大小是链表节点大小的两倍,所以只有在容器中包含足够的节点保证使用才用它”。
显然尽管转为树使得查找的速度更快,但是在节点数比较小的时候,此时对于红黑树来说内存上的劣势会超过查找等操作的优势,自然使用链表更加好,但是在节点数比较多的时候,综合考虑,红黑树比链表要好。
5、HashMap为什么还是线程不安全
在JDK1.8中,在并发执行put操作时仍会发生数据覆盖的情况
源码:
默认初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
最大容量为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
默认负载因子为0.75f
static final float DEFAULT_LOAD_FACTOR = 0.75f;
THRESHOLD 阀值,临界值,hashmap实际容量达到阀值后进行扩容。
hashMap的构造函数
1.无参构造,使用默认的初始容量16,默认的负载因子0.75f
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
2.指定初始容量
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
3.指定初始容量和负载因子,如果指定的初始容量大于支持的最大容量2^30次方则重设初始容量为2^30次方。
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); // jdk1.7此处 threshold = initialCapacity;阀值直接等于初始容量,会在第一次put时重设阀值。 }
然后设置阀值
static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
对设置的初始容量加一后进行一系列的右移然后或运算,如设置初始容量为9,结果为16,设置初始容量为16,则结果为16。也就是说找到 小于等于(n-1)*2的最大的2的次方的值。
所以如果不指定初始容量,则初始容量和阀值都为16。
4.直接根据接收一个map的构造函数创建map
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
put方法,才初始化node数组
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; // transient Node<K,V>[] table; 如果node数组没有初始化则进行一个resize。 if ((tab = table) == null || (n = tab.length) == 0) //对数组执行扩容操作。 n = (tab = resize()).length; // 对数组长度减一和key的hash值进行与运算得到数组下标,查询此下标是否有值。 if ((p = tab[i = (n - 1) & hash]) == null) // 没值就新建一个node将key,value放在此下标上 tab[i] = newNode(hash, key, value, null); else { // 运算得到的数组对应的下标已经有值了,则判断已经存在的值的key和将要保存的key是否都相同 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 如果相同,则将旧值p赋给e e = p; else if (p instanceof TreeNode) // 如果旧值是一个树结点,则将新值放进这个红黑树中。 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 是链表结构 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 将新的key,value放在链接的新的节点上 p.next = newNode(hash, key, value, null); // TREEIFY_THRESHOLD =8,如果链表的长度大于8,且table的容量大于或等于64时(binCount=7时,链表长度为9了),则转为红黑树。 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; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
resize方法:在第一次put元素的时候就执行一次。final Node<K,V>[] resize() Node<K,V>[] oldTab = table;
// 第一次put时table为null int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧的node数组的长度 int oldThr = threshold; // 旧的阀值 int newCap, newThr = 0; if (oldCap > 0) { // 如果旧的数组的长度大于2^30次方,则旧的阀值为int型的最大值即为2^31次方 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 新的数组的长度为旧的数组长度的两倍,如果旧的数组的长度的两倍小于最大容量2^30,且旧的数组的长度大于等于默认初始容量16,则新的阀值为旧的阀值的两倍。 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } //如果旧的阀值大于0,且node数组没有初始化,即刚创建hashMap,且指定了初始容量。第一次put。则,新的容量等于旧的阀值 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 16 else { // zero initial threshold signifies using defaults // 即创建HashMap用的是无参构造,还未初始化数组,则设置数组长度为默认容量16,阀值为默认负载因子0.75f*16=12 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 只有在用hashmap的带参构造创建map且第一次put数组没有初始化时。此时新数组的长度为创建map时的阀值。 if (newThr == 0) { // 修改创建map时赋的阀值,为其自身的0.75.而这时创建的数组的长度为创建map时赋的阀值,如创建时指定了初始容量为9,则会创建一个初始容量为16的数组,后将阀值设为12. 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; }
创建红黑树替换之前的链表
/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY 64 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
1.7和1.8的区别
1、(无参构造不同)1.8的无参构造没有初始化阀值,在第一次put时如果阀值未初始化的话才设数组的容量为默认初始容量,阀值为容量的0.75。1.7的无参构造和带参构造都指定了了阀值等于初始容量。
2、(带参构造的初始阀值不同)用带参构造的话1.7的阀值等于初始容量,第一次put时设为f(n)*0.75。1.8用带参构造创建map时,阀值为f(n)即小于等于(n-1)*2的最大的2的次方的值,n为指定的初始容量或为默认初始容量
3、(数组长度的值来源不同,但结果相同)1.7和1.8都是在第一次put时初始化数组。不同的是1.7直接将数组的长度设为了f(n),将阀值设为了长度的0.75。1.8是在第一次put中的resize中初始化数组,如果是带参构造创建的map则将初始阀值设为数组的长度,再修改阀值为其自身的0.75
4、(数据结构不同)1.7的数据结构为数组+链表。1.8的数据结构为数组+链表+红黑树。当链表的长度大于8,且总的元素大小大于等于64时,将链表修改为红黑树(时间复杂度为 O(logn)),将原来链表数据复制进去
5、(resize方法不同)1.8数组未初始化时也是通过resize进行初始化的
6、1.8 扩容后链表元素不会出现倒置
7、1.7的扩容条件是(size >= threshold) && (null != table[bucketIndex]),即达到阀值,并且当前需要存放对象的slot上已经有值。从代码上看,是先扩容,然后进行新增元素操作,而1.8是增加元素之后扩容
未完待续...