HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。
1.HashMap继承了AbstractMap类,实现了Map,Cloneavle,Serializable接口。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
2.HashMap的初始容量为16。HashMap数组每一个元素的初始值都是Null,第一次调用put方法时,则会开始第一次初始化扩容,长度为16。。
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
3.HashMap的最大容量为1<<30。
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
4.HashMap的装载因子为0.75。
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
( H a s h 表 的 装 载 因 子 = 填 入 表 中 的 元 素 个 数 / H a s h 表 的 长 度 ) (Hash表的装载因子 = 填入表中的元素个数 / Hash表的长度) (Hash表的装载因子=填入表中的元素个数/Hash表的长度)
当HashMap的装载因子大于该值,则进行扩容。
5.动态扩容机制
如下图中的散列表,当装载因子达到0.8时进行扩容,装载因子变为0.4,原来的数据就会存储在新的hash表中。
当数据插入到HashMap时,如果装载因子还未达到临界值,此时还不需要扩容,插入的数据非常快,但如果装载因子达到了临界值,这是就需要先进行扩容,然后再插入数据,这个时候就会变得很慢。
当数据需要从HashMap中删除时,如果HashMap已经经历过扩容,随着数据的删除,空闲空间会越来越多。当程序对内存空间非常敏感时,可以设置当装载因子小于某个临界值时,启动动态缩容,让内容空间得到充分利用;当程序对内存空间不太敏感时,就不需要进行动态缩容处理。
为了减少动态扩容耗时,可以将扩容的操作穿插在插入操作过程中。具体如下图所示:
这样每次插入时迁移一个数据,没有集中一次性迁移数据那样耗时,不会形成明显的阻塞。
由于迁移过程中,有新旧两个HashMap,查找数据时,先在新的HashMap中进行查找,如果没有,再去旧的HashMap中进行查找。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
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;
}
-
在resize()方法中,定义了oldCap参数,记录了原table的长度,定义了newCap参数,记录新table长度,newCap是oldCap长度的2倍(注释1),同时扩展点也乘2。
-
注释2是循环原table,把原table中的每个链表中的每个元素放入新table。
-
注释3,e.next==null,指的是链表中只有一个元素,所以直接把e放入新table,其中的e.hash & (newCap - 1)就是计算e在新table中的位置,和JDK1.7中的indexFor()方法是一回事。
-
注释// preserve order,这个注释是源码自带的,这里定义了4个变量:loHead,loTail,hiHead,hiTail,看起来可能有点眼晕,其实这里体现了JDK1.8对于计算节点在table中下标的新思路:
正常情况下,计算节点在table中的下标的方法是:hash&(oldTable.length-1),扩容之后,table长度翻倍,计算table下标的方法是hash&(newTable.length-1),也就是hash&(oldTable.length*2-1),于是我们有了这样的结论:这新旧两次计算下标的结果,要不然就相同,要不然就是新下标等于旧下标加上旧数组的长度。
6.一个桶中bin(箱子)的存储方式由链表转换成树的阈值。即当桶中bin的数量超过TREEIFY_THRESHOLD时使用树来代替链表。默认值是8。
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
7.当执行resize操作时,当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树。默认值是6。
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
8.当桶中的bin被树化时最小的hash表容量。(如果没有达到这个阈值,即hash表容量小于MIN_TREEIFY_CAPACITY,当桶中bin的数量太多时会执行resize扩容操作)这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
9.hash(Object key)原理
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
在jdk1.7中有indexFor(int h, int length)方法。jdk1.8里没有,但原理没变。1.7源码在1.8中用tab[(n - 1) & hash]代替。
hashcode为int类型,4个字节32位,为了确保散列性,肯定是32位都能进行散列算法计算是最好的。首先,为什么用异或计算?二进制位计算,a只可能为0,1,b只可能为0,1。a中0出现几率为1/2,1也是1/2,b同理。 位运算符有三种,|,&。a,b进行位运算,有4种可能 00,01,10,11 a或b计算结果为1的几率为3/4,0的几率为1/4,a与b计算结果为0的几率为3/4,1的几率为1/4,a异或b计算结果为1的几率为1/2,0的几率为1/2,所以,进行异或计算,得到的结果肯定更为平均,不会偏向0或者偏向1,更为散列。右移16位进行异或计算,将其拆分为两部分,前16位的异或运算,和后16位的异或运算,后16位的异或运算,即原hashcode后16位与原hashcode前16位进行异或计算,得出的结果,前16位和后16位都有参与其中,保证了32位全部进行计算。前16位的异或运算,即原hasecode前16位与0000 0000 0000 0000进行异或计算,结果只与前16位hashcode有关,同时异或计算,保证结果为0的几率为1/2,1的几率为1/2,也是平均的。所以为什么是右移16位,(1)结果的后16位保证了hashcode32位全部参与计算,也保证了0,1平均,散列性。(2)结果的前16位保证hashcode前16位了0,1平均散列性,附带hashcode前16位参与计算。(3) 16与16位数相同,利于计算,不需要补齐,移去位数数据更多情况,hashmap只会用到前16位(临时数据一般不会这么大),所以(1)占主因。