HashMap是数组 + 链表 + 红黑树(JDK1.8新增红黑树部分)实现的
数据底层具体存储的是什么?这样存储方式的优点是什么?
1) 一个重要的字段 -- Node[] table 哈希桶数组,
Node 是HashMap的一个内部类,实现了Map.Entry 接口,本质上是一个键值对,上图中的每一个黑色原点就代表一个Node对象
2)HashMap就是使用哈希表来存储的,Java中采用链地址法(数组加链表的组合)来解决哈希冲突。
链地址法: 在每个数组元素上都有一个链表结构,调用key 的hashCode() 方法得到其hashCode值,然后再通过Hash算法的后两步运算(高位运算和取模运算)来确定该键值对的存储位置,key的hash值相同的话就发生了哈希冲突, 如果哈希桶数组很大,就算较差的hash算法也会比较分散,效率较高,反之亦然本之上就是空间成本和时间成本之间的权衡。
其实就是根据实际情况确定哈希桶数组的大小,并在此基础上设计好hash算法减少哈希碰撞,即 hash算法和扩容机制。
在HashMap的默认构造里有几个重要的变量
int threshold // 所能容纳键值对的极限
final float loadFactor = 0.75 // 负载因子
int modCount // 记录HashMap内部结构变化的次数,主要用于迭代的快速失败
int size // 实际存储的键值对数量
threshold = length * Load factor
Node[] table的初始化长度length(默认是16) Load factore 为负载因子(默认值为0.75) threshold = length * Load factor
length大小的值必须时2的n次方(合数),非常规的设计,一般把哈希桶的大小设计为素数,降低冲突,Hashtable初始化桶的大小是11 ,而HashMap采用这种非常规的设计,主要在于为了取模合扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
也就是说 HashMap 最初始容量可以16个,但当他存储到16*0.75 = 12个后,再存储就会进行扩容,之前容量的2倍 也就32
size 实际存储大小
modCount 内部结构发生变化指的时结构发生变化,例如put新键值对,但某个key的value值被覆盖不属于结构变化。
在JDK8 中当链表长度超过8时,链表就会转为红黑树,利用红黑树快速增删改查的特点提高性能,
1) 确定hash桶数组索引位置
Hash算法的本质就三步:
- 取key的hashCode值;
- 高位运算;
- 取模运算。
⽅法⼀:
static final int hash(Object key) { //jdk1.8 & jdk1.7
int h;
// h = key.hashCode() 为第⼀步 取hashCode值
// h ^ (h >>> 16) 为第⼆步 ⾼位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
⽅法⼆:
static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个⽅法 是实现原理⼀样的
return h & (length-1); //第三步 取模运算
}
2) put 方法
public V put(K key, V value) {
2 // 对key的hashCode()做hash
3 return putVal(hash(key), key, value, false, true);
4 }
5
6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
7 boolean evict) {
8 Node<K,V>[] tab; Node<K,V> p; int n, i;
// 步骤①:tab为空则创建
10 if ((tab = table) == null || (n = tab.length) == 0)
11 n = (tab = resize()).length;
12 // 步骤②:计算index,并对null做处理
13 if ((p = tab[i = (n - 1) & hash]) == null)
14 tab[i] = newNode(hash, key, value, null);
15 else {
16 Node<K,V> e; K k;
17 // 步骤③:节点key存在,直接覆盖value
18 if (p.hash == hash &&
19 ((k = p.key) == key || (key != null && key.equals(k))))
20 e = p;
21 // 步骤④:判断该链为红⿊树
22 else if (p instanceof TreeNode)
23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key,
value);
24 // 步骤⑤:该链为链表
25 else {
26 for (int binCount = 0; ; ++binCount) {
27 if ((e = p.next) == null) {
28 p.next = newNode(hash, key,value,null);
//链表⻓度⼤于8转换为红⿊树进⾏处理
29 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
30 treeifyBin(tab, hash);
31 break;
32 }
// key已经存在直接覆盖value
33 if (e.hash == hash &&
34 ((k = e.key) == key || (key != null &&
key.equals(k)))) break;
36 p = e;
37 }
38 }
39
40 if (e != null) { // existing mapping for key
41 V oldValue = e.value;
42 if (!onlyIfAbsent || oldValue == null)
43 e.value = value;
44 afterNodeAccess(e);
45 return oldValue;
46 }
47 }
48 ++modCount;
49 // 步骤⑥:超过最⼤容量 就扩容
50 if (++size > threshold)
51 resize();
52 afterNodeInsertion(evict);
53 return null;
54 }
3) resize() 控制机制
扩容就是重新计算容量,当然数组是无法自动扩容的,其实就是用要给新得数组代替小容量的数组 //jdk1.7的,1.8是基于链表的
// todo
1 void resize(int newCapacity) { //传⼊新的容量
2 Entry[] oldTable = table; //引⽤扩容前的Entry数组
3 int oldCapacity = oldTable.length;
4 if (oldCapacity == MAXIMUM_CAPACITY) { //扩容前的数组⼤⼩如果已经达到最⼤(2^30)了
5 threshold = Integer.MAX_VALUE; //修改阈值为int的最⼤值(2^31-1),这样以后就不会扩容了
6 return;
7 }
8
9 Entry[] newTable = new Entry[newCapacity]; //初始化⼀个新的Entry数组
10 transfer(newTable); //!!将数据转移到新的Entry数组⾥
11 table = newTable; //HashMap的table属性引⽤新的Entry数组
12 threshold = (int)(newCapacity * loadFactor);//修改阈值
13 }
1 void transfer(Entry[] newTable) {
2 Entry[] src = table; //src引⽤了旧的Entry数组
3 int newCapacity = newTable.length;
4 for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
5 Entry<K,V> e = src[j]; //取得旧Entry数组的每个元素
6 if (e != null) {
7 src[j] = null;//释放旧Entry数组的对象引⽤(for循环后,旧的Entry数组不再引⽤任何对象)
8 do {
9 Entry<K,V> next = e.next;
10 int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
11 e.next = newTable[i]; //标记[1]
12 newTable[i] = e; //将元素放在数组上
13 e = next; //访问下⼀个Entry链上的元素
14 } while (e != null);
15 }
16 }
17 }