JDK7时使用的主要步骤
当我们创建HashMap实例即new一个对象时,在底层HashMap会帮我们创建一个长度为16的一维数组,类型为Entry[],名为table。为什么长度是16?因为底层容量默认就是定义为16。
当我们逐个调用put方法将key-value(Entry对象)放入底层数组时,底层的方法首先会调用key(我们所插入的key-value中的key)所在类(不一定是自定义类,String,Object等都可,提到这三个类是因为自定义类你可以选择重写hashCode,而String则已经重写了hashCode,Object作为所有类的间接父类本身具有hashCode)的hashCode()计算当前key的哈希值(如果自定义类没有重写则调用其父类的hashCode)。
这个哈希值在经过某种算法计算后,能够得到该key-value将在数组中存放的位置。如果此时该位置没有其他元素,则该key-value直接添加成功。如果该位置上已经有元素了,则会比较key和该位置上已经存在的元素的哈希值。如果key的哈希值与其他数值的哈希值都不同,则该key-value直接添加成功。如果key的哈希值与某个元素的哈希值相同,则会调用key所在类的equals方法,若equals方法返回false,则该key-value直接添加成功,否则说明key与这个元素相同,则该key对应的value去覆盖这个元素的value值。
JDK8与JDK7使用的主要步骤区别:
- 在创建HashMap实例时,jdk7是直接创建数组,而jdk8是将负载因子赋给当前实例,没有创建数组,在第一次调用put时才会创建数组。
- jdk7底层数组为Entry类型,而jdk8底层数组是Node类型,
- 虽然本质上也是Entry类型。
- jdk7时底层结构为数组加链表,而jdk8时底层结构为数组加链表加红黑树。
- jdk7用的是头插法,而jdk8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为jdk7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在jdk8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
JDK8什么时候使用红黑树存储数据?
当数组的某一索引位置上的元素以链表形式存在的个数>=8并且当前数组长度>64时,此时该索引位置上的所有数据将会改用红黑树存储。
源码分析
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;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
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) {
p.next = newNode(hash, key, value, null);
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;
}
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;
}
这是put方法所调用的putVal方法,其中实现了添加链表节点,resize方法则实现了table的创建,扩容,其中两个变量需要我们记住:
负载因子:默认为0.75,它与底层数组的长度相关,与同一索引位置产生的链表长短也有一定关系,因为如果它太小,会导致底层数组长度太短而导致频繁扩容,如果它过大的话,则会导致同一索引位置产生的链表过长。
临界值:默认为12(16*0.75,由当前容量*负载因子求得),当数组长度>临界值时,则会进行扩容。
对于resize方法的部分解析:
从resize方法可以看到如果数组未创建,则
newCap = DEFAULT_INITIAL_CAPACITY;
这一句是将HashMap前边定义的常量赋给新的容量newCap。
在resize方法中threshold代表着负载因子,在上述操作同时,resize还将临界值求出:
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
newThr是中间变量,而后:
threshold = newThr;
上面两个大写字母的常量就是HashMap内定义的两个常量:负载因子,默认容量,两者相乘就是临界值了。
然后就创建了一个容量为16的数组:
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
在resize这个方法中还实现了扩容和将原数组复制到新数组的操作:
在判断原容量大于0的情况下,又与数组的最大容量进行比较,满足小于最大容量则会将原容量左移1位(*2)即原数组容量变为原来的两倍,并且负载因子变为原来的两倍,这也意味着临界值会变为原来的两倍:
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
然后
threshold = newThr; //将中间量赋值给负载因子
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建新的数组
table = newTab;
if (oldTab != null)
这一块内都是实现复制操作。
相信很多人都会疑惑扩容后新数组是如何重新存储元素的,那么请看到这一行代码:
newTab[e.hash & (newCap - 1)] = e;
这一句将一个位置只有一个元素时,这个元素的hash值与新容量-1这个值进行了与操作,得出新的位置,并把该元素进行搬家。这就可以实现数组扩容而不至于之前的元素还在原来位置导致的不均匀问题了。
后面的
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
是当以红黑树存储时进入,这里略。
当位置上有由多个元素构成的链表时,则会进入else,但是由于博主没有搞懂
(e.hash & oldCap) == 0
这一句到底意味着什么,所以很遗憾只能看到这,不能将整个resize方法吃透,若您有高见,麻烦评论区指导指导我这个菜鸟,谢谢谢谢!
对于前面的putVal,由于篇幅有限,我就不详细解读了,但是我还是会提及重点:
关于JDK8为什么是在链表元素>8且当前数组长度>64时才使用红黑树进行存储:
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
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
其中TREEIFY_THRESHOLD=8,由于binCount是由0开始计数,0到TREEIFY_THRESHOLD-1刚好8个元素,又因为当个数 = 8 时,链表 和 红黑树 的效率一样。所以只有当 链表 中的元素个数 >= 8,并且 数组的长度 >= 64 时才会将链表转为红黑树。至于为什么是数组长度大于等于64,请点进treeifyBin方法看源码:
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
当数组元素小于MIN_TREEIFY_CAPACITY(64)时,它会调用resize方法继续扩容,而不是将链表转为红黑树。