Java HashMap 源码研究
标签:for cti ade mapping abstract 转换 散列 ott equals
Jdk 8 的hashmap ,内部使用Node 表示数组成员,Node 实现了Map.Entry接口。
put() 过程:
1 public class HashMapextends AbstractMapimplements Map, Cloneable, Serializable {
2 // map 的元素就是保存在这个table数组变量之中,也就是“数组+链表” 之中的数组
3 transient Node[] table;
4
5 public V put(K key, V value) {
6 return putVal(hash(key), key, value, false, true);
7 }
8 // hash 算法核心,减少碰撞
9 static final int hash(Object key) {
10 int h;
11 /** 如果key是空,hash值返回0
12 否则,用key的hashcode() h变量 和 h无符号右移16位【右移一半】后的值【h是int类型32位,右移使hash值的高位变成低 位,举例1010 右移两位变成0010】作异或运算。这么做的目的是为了是哈希值更加散列【因为求key的下标是tab[i = (n - 1) & hash],用数组长度减一的值和哈希值做与运算,大多数情况下,哈希值参与运算仅为低位部分值,
13 举例:数组长度16,转换二进制:1111,哈希值为78897121,转换二进制:100101100111101111111100001与1111 运算如 下,结果是0000 0000 0000 0000 0000 0000 0000 0001,可见决定结果的是低4位,所以为了让高位也能参与到运算,减少 冲突。最后哈希值高位没变动,低位值被重新计算。】
14 */
15 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
16 }
17 // 散列码(哈希值)的获取,依赖本地方法,通常是将对象的内存地址转换为整数。
18 public native int hashCode();
19 // onlyIfAbsent:true 存在则不更新。evict:供LinkedHashMap 回调方法afterNodeInsertion()使用
20 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
21 boolean evict) {
22 // tab 临时变量,p是桶位上的首个node,n添加元素后的数组长度,i是目标位置下标
23 Node[] tab; Node p; int n, i;
24 if ((tab = table) == null || (n = tab.length) == 0)
25 n = (tab = resize()).length;
26 // 如果之前桶位上没有值,那么构造一个node 放上去
27 if ((p = tab[i = (n - 1) & hash]) == null)
28 tab[i] = newNode(hash, key, value, null);
29 else {
30 // 到这里说明桶位上有值,要么添加自己到链表/红黑树末尾,要么找到和自己key一致的node,替换node的值
31 // e 是存在的那个节点,k 是存在的节点的key
32 Node e; K k;
33 // 如果p-原来的node的哈希值和新值的hash值一致,且内容也一样。则p就是新值要替代对象,赋给e
34 if (p.hash == hash &&
35 ((k = p.key) == key || (key != null && key.equals(k))))
36 e = p;
37 // 如果p已经不是node结构【链表结构】,而是红黑树
38 else if (p instanceof TreeNode)
39 // 调用红黑树的putTreeVal(),返回要替代的目标节点
40 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
41 else {
42 // 走到这里,说明目前还是数组+链表结构,且桶位上p并不是我们要替代的目标,所以开始遍历链表
43 for (int binCount = 0; ; ++binCount) {
44 // 如果p的下一个结点不存在,说明遍历链表完毕,那好,就构造一个新节点插入到链尾
45 if ((e = p.next) == null) {
46 p.next = newNode(hash, key, value, null);
47 // 如果目前桶位上链表数超过 树化阈值,那么就转为红黑树
48 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
49 treeifyBin(tab, hash);
50 break;
51 }
52 // 到这里说明e不为空,判断当前结点e和新值的哈希值和key内容是否相同
53 // 如果成立,好这就是要更新替代的目标节点,跳出循环,不再遍历后续链表
54 if (e.hash == hash &&
55 ((k = e.key) == key || (key != null && key.equals(k))))
56 break;
57 p = e;
58 }
59 }
60 // 如果e不为空,就返回老值,保存新值。
61 // 如果e为空,说明之前已经构造新值并插入链表末尾
62 if (e != null) { // existing mapping for key
63 V oldValue = e.value;
64 // onlyIfAbsent 方法形参,true:不能修改存在的值。控制是否可以修改值的一个开关
65 // 如果允许修改或者老值为空,则赋新值
66 if (!onlyIfAbsent || oldValue == null)
67 e.value = value;
68 afterNodeAccess(e);// 回调方法,空实现,模板模式
69 return oldValue;
70 }
71 }
72 // 修改计数器加一
73 ++modCount;
74 // 走到这一步,说明新元素已经被构造成一个Node,添加到了链尾,最后返回值为空,因为新元素是新加入,没有替代谁【本方法说的替代准确说是替代老节点的value的值,而不是老节点】。
75 // 如果hashmap添加新元素后,实际使用长度超过扩容阈值,完蛋,扩容吧。
76 if (++size > threshold)
77 resize();
78 afterNodeInsertion(evict);// 回调方法,空实现,模板模式
79 return null;
80 }
81 // Callbacks to allow LinkedHashMap post-actions
82 void afterNodeAccess(Node p) { }
83 void afterNodeInsertion(boolean evict) { }
84
85 // 树化阈值,如果链表结点数大于等于8,就会转为红黑树
86 static final int TREEIFY_THRESHOLD = 8;
87 // 链化阈值,如果红黑树结点数小于等于6,就会转为链表
88 static final int UNTREEIFY_THRESHOLD = 6;
89
90
91 }
扩容过程 resize(),是Java 8 HashMap更改最大的部分,Jdk7 是使用头插法,多线程情况下会导致循环引用
1 // 扩容方法,面试重点
2 final Node[] resize() {
3 Node[] oldTab = table;
4 int oldCap = (oldTab == null) ? 0 : oldTab.length;
5 int oldThr = threshold;
6 int newCap, newThr = 0;
7 if (oldCap > 0) {
8 // 如果之前的容量大于等于1
9 if (oldCap >= MAXIMUM_CAPACITY) {
10 threshold = Integer.MAX_VALUE;
11 return oldTab;
12 }
13 // 如果之前容量扩大一倍后赋给新容量 小于最大容量,且老容量大于等于默认初始化容量16,新扩容阈值也变大
14 else if ((newCap = oldCap 15 oldCap >= DEFAULT_INITIAL_CAPACITY)
16 newThr = oldThr // double threshold
17 }
18 // 如果oldCap 0,那么初始容量设置为阈值
19 else if (oldThr > 0) // initial capacity was placed in threshold
20 newCap = oldThr;
21 else { // zero initial threshold signifies using defaults
22 newCap = DEFAULT_INITIAL_CAPACITY;
23 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
24 }
25 // 如果新的扩容阈值等于0,重新计算扩容阅知
26 if (newThr == 0) {
27 float ft = (float)newCap * loadFactor;
28 newThr = (newCap float)MAXIMUM_CAPACITY ?
29 (int)ft : Integer.MAX_VALUE);
30 }
31 threshold = newThr;
32 // 以新的容量构造一个数组
33 @SuppressWarnings({"rawtypes","unchecked"})
34 Node[] newTab = (Node[])new Node[newCap];
35 // map的数组引用指向新数组【扩容后数组】
36 table = newTab;
37 if (oldTab != null) {
38 // 遍历原数组
39 for (int j = 0; j j) {
40 Node e;
41 // 如果原数组桶位上 存在节点node,不为空
42 if ((e = oldTab[j]) != null) {
43 // 桶位上的node已经赋值到e,清空桶位
44 oldTab[j] = null;
45 /** 如果e的下一个节点不存在,也就是说链表只有一个节点,直接将e这个放到新数组的桶位上,
46 桶位的下标计算公式不变,唯一区别是使用新数组的容量-1来与运算。newCap是oldCap 的2倍,
47 比如 oldCap为16,oldCap-1→1111,newCap为32,newCap-1→11111。此时e.hash值的第三位决定了,新桶 位的下标,如果e.hash的第三位是0,则计算出的下标和之前一样,可以说位置不变。如果是1,新下标就会变 成11111,可分解为 原下标1111【15】+10000【16】,原下标15,新下标31。总结就是下标要么在原址,
48 要么跑到原址+oldCap【原来容量】的地址
49 */
50 if (e.next == null)
51 newTab[e.hash & (newCap - 1)] = e;
52 else if (e instanceof TreeNode)
53 // 如果e 是红黑树节点,调用split()方法来处理
54 ((TreeNode)e).split(this, newTab, j, oldCap);
55 else { // preserve order
56 // 到这里说明,桶位上是链表,且链表结点不为1.
57 // loHead、loTail 是扩容后在落在原址的结点组成的链表的链表头尾,称之为低位链
58 // hiHead、hiTail 是扩容后到落在原址+oldCap的地址组成的链表的链表头尾,称之为高位链。
59 Node loHead = null, loTail = null;
60 Node hiHead = null, hiTail = null;
61 // next 临时变量,代表下一个被处理的节点
62 Node next;
63 // 开始遍历待分配位置链表
64 do {
65 // 当前结点e的下一个结点地址赋给next
66 // ? next变量 能否省略。我的感觉是可以,退出条件改为while((e=e.next) !=null)
67 next = e.next;
68 /* 判断当前结点的哈希值和扩容前容量做与运算是否等于0,目的是为了将链表上的节点均匀的分配到低位链 高位链。它是如何做到的呢?oldCap二进制一定是100~这种形式,做与运算只需判断e.hash二进制值和 前者1所对应的位置上的数是0还是1,是0当前结点e加入低位链。是1当前节点e加入高位链。
69 注意:一个链表上的不同结点的散列码是不同的,来自不同的内存地址。
70 */
71 if ((e.hash & oldCap) == 0) {
72 // 如果是首次向低位链添加,就把e赋值给loHead链表头
73 if (loTail == null)
74 loHead = e;
75 else
76 // 不是第一次向低位链添加结点,那么就赋值给loTail.next,链接到链表尾部【尾插法】
77 loTail.next = e;
78 // 更新低位链后,loTail重新指向链尾。【就像一个箭头下移了一位】
79 loTail = e;
80 }
81 // 执行else,说明结点e应该被添加到高位链上。
82 else {
83 // 如果是首次向高位链添加,就把e赋值给hiHead链表头
84 if (hiTail == null)
85 hiHead = e;
86 else
87 // 不是第一次向高位链添加结点,那么就赋值给hiTail.next,链接到链表尾部【尾插法】
88 hiTail.next = e;
89 // 更新高位链后,hiTail重新指向链尾。【就像一个箭头下移了一位】
90 hiTail = e;
91 }
92 // 遍历老链表,当next 为null时,退出循环
93 } while ((e = next) != null);
94 // 遍历完成后,待分配节点已经各自组成了高位链、低位链
95 if (loTail != null) {
96 // 低位链链尾指向null,把链表放置到新数组老位置
97 loTail.next = null;
98 newTab[j] = loHead;
99 }
100 if (hiTail != null) {
101 // 高位链链尾指向null,把链表放置到新数组新位置,相对老位置偏移oldCap位。
102 hiTail.next = null;
103 newTab[j + oldCap] = hiHead;
104 }
105 }
106 }
107 // 每次循环搞定一个桶位
108 }
109 }
110 // 最终返回扩容后数组
111 return newTab;
112 }
get()方法下回补充。。
Java HashMap 源码研究
标签:for cti ade mapping abstract 转换 散列 ott equals
原文地址:https://www.cnblogs.com/houj/p/14275177.html
评论