梦想还是要有的,万一忘了咋办?

0%

jdk1.8的HashMap

Map
前面了解了哈希表,hashmap的原理就是哈希表。hashmap散列函数采用取余法,冲突解决采用的是链地址法。

概述

本文基于jdk1.8,HashMap中,键值对(key-value)在内部是以Entry(HashMap中的静态内部类)实例的方式存储,散列表table是一个Entry数组,保存Entry实例。如果若干个entry计算得到相同散列地址(具体是由(n-1)&h(key)求得),这些entry被组织成一个链表,并以table[i]为头指针。
hashMap结构

函数

  • HashMap的hash函数
1
2
3
4
5
6
static final int hash(Object key) {
// h = key.hashCode() 为第一步 取hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • HashMap的散列函数
1
int i=(n-1)&hash(key);

由于采用以上两个函数,哈系表的长度lenth大小必须是2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把表的长度设计为素数。目的在于 在取模和扩容时做优化,减少冲突,定位哈希表索引位置时也加入了高位参与运算的过程(非合数设计缺陷的补偿)。

  • 链表引入红黑树

Jdk1.8版本中,对数据结构做了进一步优化,引入红黑树。而当链表长度太长时(默认超过8)。链表就会转换为红黑树,利用红黑树快速增删改查的特点提高hashmap的性能;

分析HashMap的put方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
       //1、table不存在、或长度=0 ,resize。
       if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
       //2、获取对应位置node,为null时 直接将新node存入
       if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
           //3、对应位置的node不是null,且key相等 直接赋值;
           if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
           //4、key 不相等、但 node 是 treenode,直接封装为treenode存入链中
           else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
           //5、key不相等、node是普通的node,直接遍历存入链未。
               for (int binCount = 0; ; ++binCount) {
              //6、到队尾未找到相等元素,将新元素存入队尾。
                  if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
                       //8、存入后链中node数大于临界值(8),将普通node链转换为红黑树。
                       if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
                   //7、找到相等key值,直接将新node替换老node
                   if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
           //9、已经存在的key,返回之前的value
           if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
       //10、超过指定容量,进行resize。
       ++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

扩容

扩容(resize)就是,原有容量不够使用后将容量扩大。table长度变大以后,移动entry位置是很讲究的。最笨的方式就是遍历所有的node,重新计算hash,取模值,放入对应位置。但JDK1.8做了一些优化:


元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

因此,我们在扩充hashmap的时候不需要向JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是0还是1就好了,是0的话索引没有变化,是1的话索引变成了“元索引+oldCap"可以看看16扩充到32的resize示意图:

这个设计非常的巧妙,即省去了重新计算hash值得时间,而且可以把之前冲突的节点均匀的分散到新的bucket上。
备注:jdk7在rehash的时候如果在新链表的数组索引位置相同,则链表元素会倒置,JDK8是不会的。

安全

Hashmap是线程不安全的,涉及多线程时使用ConcurrentHashMap代替。

参考阅读