HashMap
是一个基于哈希表实现的无序的key-value
容器,它键和值容许设置为 null
,同时它是线程不安全的。java
jdk 1.7
中HashMap
是以数组+链表的实现的jdk1.8
开始引入红黑树,HashMap
底层变成了数组+链表+红黑树实现红黑树是一种特殊的平衡二叉树,它有以下的特征:数组
NULL
节点)因此红黑树的时间复杂度为: O(lgn)
。安全
HashMap
的底层首先是一个数组,元素存放的数组索引值就是由该元素的哈希值(key-value
中key
的哈希值)肯定的,这就可能产生一种特殊状况——不一样的key
哈希值相同。数据结构
在这样的状况下,因而引入链表,若是key
的哈希值相同,在数组的该索引中存放一个链表,这个链表就包含了全部key
的哈希值相同的value
值,这就解决了哈希冲突的问题。多线程
可是若是发生大量哈希值相同的特殊状况,致使链表很长,就会严重影响HashMap
的性能,由于链表的查询效率须要遍历全部节点。因而在jdk1.8
引入了红黑树,当链表的长度大于8,且HashMap
的容量大于64的时候,就会将链表转化为红黑树。ide
// jdk1.8 // HashMap#putVal // binCount 是该链表的长度计数器,当链表长度大于等于8时,执行树化方法 // TREEIFY_THRESHOLD = 8 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); // HashMap#treeifyBin final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // MIN_TREEIFY_CAPACITY=64 // 若 HashMap 的大小小于64,仅扩容,不树化 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 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); } }
所谓的加载因子,也叫扩容因子或者负载因子,它是用来进行扩容判断的。函数
假设加载因子是0.5,HashMap
初始化容量是16,当HashMap
中有16 * 0.5=8
个元素时,HashMap
就会进行扩容操做。性能
而HashMap
中加载因子为0.75,是考虑到了性能和容量的平衡。this
由加载因子的定义,能够知道它的取值范围是(0, 1]。spa
HashMap
的容量(size
属性,构造函数中的initialCapacity
变量)有一个要求:它必定是2的幂。因此加载因子选择了0.75就能够保证它与容量的乘积为整数。// 构造函数 public HashMap(int initialCapacity, float loadFactor) { // …… this.loadFactor = loadFactor;// 加载因子 this.threshold = tableSizeFor(initialCapacity); } /** * Returns a power of two size for the given target capacity.返回2的幂 * MAXIMUM_CAPACITY = 1 << 30 */ 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; }
HashMap
的默认初始容量是16,而每次扩容是扩容为原来的2倍。这里的16和2倍就保证了HashMap
的容量是2的n次幂,那么这样设计的缘由是什么呢?
与运算&
,基于二进制数值,同时为1结果为1,不然就是0。如1&1=1,1&0=0,0&0=0。使用与运算的缘由就是对于计算机来讲,与运算十分高效。
在给HashMap
添加元素的putVal
函数中,有这样一段代码:
// n为容量,hash为该元素的hash值 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
它会在添加元素时,经过i = (n - 1) & hash
计算该元素在HashMap
中的位置。
当 HashMap 的容量为 2 的 n 次幂时,他的二进制值是100000……(n个0),因此n-1的值就是011111……(n个1),这样的话(n - 1) & hash
的值才可以充分散列。
举个例子,假设容量为16,如今有哈希值为1111,1110,1011,1001四种将被添加,它们与n-1(15的二进制=01111)的哈希值分别为11十一、11十、11十、1011,都不相同。
而假设容量不为2的n次幂,假设为10,那么它与上述四个哈希值进行与运算的结果分别是:010一、0100、000一、0001。
能够看到后两个值发生了碰撞,从中能够看出,非2的n次幂会加大哈希碰撞的几率。因此 HashMap 的容量设置为2的n次幂有利于元素的充分散列。
参考:HashMap初始容量为何是2的n次幂及扩容为何是2倍的形式
HashMap
会致使死循环是在jdk1.7
中,因为扩容时的操做是使用头插法,在多线程的环境下可能产生循环链表,由此致使了死循环。在jdk1.8
中改成使用尾插法,避免了该死循环的状况。
在网上找到了比较详细的解释分析博客与视频: