为了账号安全,请及时绑定邮箱和手机立即绑定

HashMap源码分析

标签:
Java

HashMap源码分析

0、绪论

hashMap是一个很重要的数据结构首次出现在JDK1.2中 。简单的来说就是将数据的hash码按照相近的程度分别装入一些桶。每一个桶中的对象的hash值相似。这样可以按照需要查找的对象的hash值,来快速逼近所需要查询的对象的本体。

1、初始容量

初始容量要求是2的n次幂:即为16,32,64,128 etc

源码中:1 >> 4 = 2^4 = 16 ,所以缺省的初始容量是16

2、负载因子

负载因子表示扩展的比例,已经存入的KV对的数量占capacity的最大比例。

0.75 这个缺省的负载因子,在时间成本和空间成本上做到了最好的折衷。过大省空间但是浪费了时间,过小省时间但是浪费了空间。

3、阈值

阈值:threshold  = initcapacity * loadFactory

阈值表示这个hashMap所能容纳的最大的KV对数。(一个K和一个对应的V叫做一个键)超过了这个数量,容量就需要翻倍,即 capaciry >> 1。

为2的n次幂的原因是:对每一个桶进行重新排列的时候,hash码是二进制的形式,每一位上只有0 和 1 ,当容量增大的时候,对应的位数上为0 的保持在原来的位置 n 上,为1 的变到 n + oldLength 上

 

1、原理

构建一个数组,这个数组成为桶数组(bucket-array)。这个数组中存放的是一个链表的头节点或者是一个红黑树的根结点。

属于桶数组的数据结构是链表还是红黑树,要取决于属于这个桶KV对的数量。

如果在8个以下,那么这个数据结构一定会是一个链表。

如果链表长度到达了8个,那么有两种情况

1、桶数组的长度在64以下,那么就会进行扩容操作,将桶的数量翻倍。

2、桶数组的长度到达了64,就会将这个链表变成一个红黑树。

 

如果某个桶中装的红黑树,经过删除操作之后又回到了7个,就会降级,变成一个链表。

 

以上的操作,解决了hash冲突。

 

还有一种导致扩容的条件:

那就是table中的 容量 * 负载因子 都填充了对象,那么就会导致扩容。这就是构造方法里面的初始设置的变量的意义。

2、HashMap的构造方法

HashMap 有四个构造方法:

0、无参构造方法:

1 public HashMap() {
2 
3     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
4 
5 }

 

 

1、初始桶数量构造方法:

1 public HashMap(int initialCapacity) {
2 
3     this(initialCapacity, DEFAULT_LOAD_FACTOR);
4 
5 }

 

 

2、初始桶数量和负载因子构造方法:

 

复制代码

 1 public HashMap(int initialCapacity, float loadFactor) {
 2 
 3     if (initialCapacity < 0)
 4 
 5         throw new IllegalArgumentException("Illegal initial capacity: " +
 6 
 7                                            initialCapacity);
 8 
 9     if (initialCapacity > MAXIMUM_CAPACITY)
10 
11         initialCapacity = MAXIMUM_CAPACITY;
12 
13     if (loadFactor <= 0 || Float.isNaN(loadFactor))
14 
15         throw new IllegalArgumentException("Illegal load factor: " +
16 
17                                            loadFactor);
18 
19     this.loadFactor = loadFactor;
20 
21     this.threshold = tableSizeFor(initialCapacity);
22 
23 }

复制代码

 

 

3、从map导入生成构造方法

复制代码

1 public HashMap(Map<? extends K, ? extends V> m) {
2 
3     this.loadFactor = DEFAULT_LOAD_FACTOR;
4 
5     putMapEntries(m, false);
6 
7 }

复制代码

 

 

方法0 :

以无参的方式构造一个hashmap,使用缺省的桶的数量16,使用缺省的负载因子0.75。

 

方法1:

自行设置初始的桶的数量。并且这个值只能为2的n次方。

 

方法2:

自行设置初始的桶的数量,自行设置初始的桶负载因子(0~1.0f)。

 

方法3:

通过一个map对象,将这个map对象中的所有KV对全部倒入这个hashmap中,这个hashMap使用缺省的初始桶数量和缺省的负载因子。

 

 

3、HashMap的查找

使用get方法和getNode方法实现查找。

1、get方法。

 

复制代码

1 public V get(Object key) {
2 
3     Node<K,V> e;
4 
5     return (e = getNode(hash(key), key)) == null ? null : e.value;
6 
7 }

复制代码

 

 

在get方法里面包含着一个getNode方法,get方法返回的是getNode方法返回值的一个V。

 

2、getNode方法。

 

复制代码

 1 final Node<K,V> getNode(int hash, Object key) {
 2 
 3     Node<K,V>[] tab; 
 4 
 5    Node<K,V> first, e; 
 6 
 7    int n; 
 8 
 9    K k;      
10 
11     if ((tab = table) != null && (n = tab.length) > 0 &&
12 
13         (first = tab[(n - 1) & hash]) != null) {
14 
15  
16 
17         if (first.hash == hash && // always check first node
18 
19             ((k = first.key) == key || (key != null && key.equals(k))))
20 
21             return first;
22 
23  
24 
25         if ((e = first.next) != null) {
26 
27             if (first instanceof TreeNode)
28 
29                 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
30 
31             do {
32 
33                 if (e.hash == hash &&
34 
35                     ((k = e.key) == key || (key != null && key.equals(k))))
36 
37                     return e;
38 
39             } while ((e = e.next) != null);
40 
41         }
42 
43  
44 
45  
46 
47     }
48 
49     return null;
50 
51 }

复制代码

 

 

getNode方法是hashmap主要的查找方法。

1、桶数组table(tab)不能为空,桶的长度(n)不能为0,key对应的桶(first)不能为空。

2、如果桶的hash和传入的hash相同,并且key对应的桶的key和传入的key时第一个key是同一个key,那么这个时候,桶中的root或者head节点就是我们需哦需要的结果,返回它即可。

3、在第二个不成立的基础上,那么就进入循环。

4、在循环的第一次,判断是不是一个二叉红黑树,如果是,那么就直接调用树的查询方法,并返回这个结果。

5、如果不是一个树,那么就要通过一个循环来完成这个,查询的结果。

6、在链表中判断的结果和第二条的要求一致。

 

3、HashMap的遍历

有两种便利方法:

1、K (key) 遍历

可以使用foreach方法:

1 for (Object key : map.keySet()) {
2 
3     System.out.println((String) key);
4 
5 }

 

 

map.keySet的作用是返回一个map的Set对象,这样的情况,对hashMap的遍历,就转化成了对这个Set对象的遍历

中keySet的源码:

复制代码

 1 public Set<K> keySet() {
 2 
 3     Set<K> ks = keySet;
 4 
 5     if (ks == null) {
 6 
 7         ks = new KeySet();
 8 
 9         keySet = ks;
10 
11     }
12 
13     return ks;
14 
15 }

复制代码

 

1、如果不为空,直接的返回内部用来存放key的容器对象 :keySet(来自于抽象类AbstractMap)。

2、如果为空,new 一个空keySet,将其返回。

 

2、Entry 遍历

1 for (HashMap.Entry entry : map.entrySet()) {
2 
3     System.out.print(entry.getKey()+" -> "+entry.getValue() + " \n");
4 
5 }

 

1、使用了EntrySet方法,返回一个用来存放Entry的容器EntrySet(来自于HashMap)。

2、Entry是和K、V操作相关的一个接口。

#、K、V 存放在Entry中

#、Entry的集合是EntrySet

3、获取entrySet,对其遍历,可以获得所有的Entry。

4、提取每一个Entry的K、V,可以获得所有的键对。

 

其中entrySet的源码:

复制代码

1 public Set<Map.Entry<K,V>> entrySet() {
2 
3     Set<Map.Entry<K,V>> es;
4 
5     return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
6 
7 }

复制代码

 

1、如果不为空,直接的返回内部key和value的容器 :EntrySet。

2、为空,new 一个空EntrySet,将其返回。

 

 

 

4、HashMap的添加

1、添加方法

 

复制代码

1 public V put(K key, V value) {
2 
3 //这里的hash方法在这段代码的最后,不同于一般的hashcode。
4 
5     return putVal(hash(key), key, value, false, true);
6 
7 }

复制代码

 

 

 

 

复制代码

  1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
  2 
  3     Node<K,V>[] tab; 
  4 
  5   Node<K,V> p; //p是桶中的第一个KV对
  6 
  7   int n, i;  //n是table的长度,i是桶数组的index。
  8 
  9  
 10 
 11   //如果在hashmap中table是空的,就创建一个。
 12 
 13     if ((tab = table) == null || (n = tab.length) == 0)
 14 
 15         n = (tab = resize()).length;
 16 
 17  
 18 
 19   //如果这个桶是空的,那么在这个桶中创建第一个节点。
 20 
 21     if ((p = tab[i = (n - 1) & hash]) == null)
 22 
 23         tab[i] = newNode(hash, key, value, null);
 24 
 25  
 26 
 27     else {
 28 
 29   //要插入的KV对
 30 
 31         Node<K,V> e; 
 32 
 33       K k;
 34 
 35  
 36 
 37   //如果桶中的第一个KV对的key的hash和要插入的hash相同,那么就把它替代掉
 38 
 39         if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
 40 
 41             e = p;
 42 
 43  
 44 
 45         //如果桶中的第一个节点下面挂的是一棵树
 46 
 47         else if (p instanceof TreeNode)
 48 
 49             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
 50 
 51  
 52 
 53       //如果桶中的第一个节点装的是一个链表
 54 
 55         else {
 56 
 57         //查出尾节点,然后挂上节点就ok了
 58 
 59             for (int binCount = 0; ; ++binCount) {
 60 
 61                 if ((e = p.next) == null) {
 62 
 63                     p.next = newNode(hash, key, value, null);
 64 
 65                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
 66 
 67                         treeifyBin(tab, hash);
 68 
 69                     break;
 70 
 71                 }
 72 
 73                 if (e.hash == hash &&
 74 
 75                     ((k = e.key) == key || (key != null && key.equals(k))))
 76 
 77                     break;
 78 
 79                 p = e;
 80 
 81             }
 82 
 83         }
 84 
 85  
 86 
 87       //复符合了上面的一个,然后返回V
 88 
 89         if (e != null) { // existing mapping for key
 90 
 91             V oldValue = e.value;
 92 
 93             if (!onlyIfAbsent || oldValue == null)
 94 
 95                 e.value = value;
 96 
 97             afterNodeAccess(e);
 98 
 99             return oldValue;
100 
101         }
102 
103     }
104 
105     ++modCount;
106 
107     if (++size > threshold)
108 
109         resize();
110 
111     afterNodeInsertion(evict);
112 
113     return null;
114 
115 }

复制代码

 

1、添加方法实际上是在内部调用了一个更加详细的添加方法,真正的添加的操作都是在这了详细的操作里面完成的。

2、并且这里所使用的hash方法不是直接的hashCode。

 

2、hash()方法

复制代码

1 static final int hash(Object key) {
2 
3     int h;
4 
5     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
6 
7 }

复制代码

 

 

1、>>> :忽略符号位右移

2、<<< :忽略符号位左移

3、A^B :以二进制的形式按位取反

4、这样重新计算一次hash的值,增加了它的复杂度。让它们(KV键对)可以更好的在桶数组上均匀分布。

 

5、HashMap的resize(扩容)

resize方法在权衡之后,判断是否需要对node的数组table的长度翻倍处理。如果需要,将原来的元素分配到新的数组里。table的长度必须为2的幂。(16、32、64、128、256 etc.)

 

复制代码

  1 final Node<K,V>[] resize() {
  2 
  3     Node<K,V>[] oldTab = table;
  4 
  5     int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6 
  7     int oldThr = threshold;
  8 
  9     int newCap, newThr = 0;
 10 
 11  
 12 
 13   //说明初始化过了,将新的容量赋给newCap
 14 
 15     if (oldCap > 0) {
 16 
 17         if (oldCap >= MAXIMUM_CAPACITY) {
 18 
 19             threshold = Integer.MAX_VALUE;
 20 
 21             return oldTab;
 22 
 23         }
 24 
 25     //容量翻倍
 26 
 27         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 28 
 29                  oldCap >= DEFAULT_INITIAL_CAPACITY)
 30 
 31             newThr = oldThr << 1; // 阈值翻倍
 32 
 33     }
 34 
 35   //下面的2个条件分支都是说明未被初始化
 36 
 37     else if (oldThr > 0) // initial capacity was placed in threshold
 38 
 39         newCap = oldThr;
 40 
 41     else {               // zero initial threshold signifies using defaults
 42 
 43         newCap = DEFAULT_INITIAL_CAPACITY;
 44 
 45         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 46 
 47     }
 48 
 49  
 50 
 51   //在上面的选项里面执行的是未初始化过的那第二个条件分支,那么这个判断就会为真,然后将newThr赋值
 52 
 53     if (newThr == 0) {
 54 
 55         float ft = (float)newCap * loadFactor;
 56 
 57         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
 58 
 59                   (int)ft : Integer.MAX_VALUE);
 60 
 61     }
 62 
 63  
 64 
 65     threshold = newThr;
 66 
 67  
 68 
 69     @SuppressWarnings({"rawtypes","unchecked"})
 70 
 71     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 72 
 73     table = newTab;
 74 
 75  
 76 
 77   //利用新的容量和阈值,构建新的table,并将oldTable转移到新的Table中
 78 
 79     if (oldTab != null) {
 80 
 81   //遍历oldTable
 82 
 83         for (int j = 0; j < oldCap; ++j) {
 84 
 85       //oldTable中的每一个桶的第一个节点
 86 
 87             Node<K,V> e;
 88 
 89             if ((e = oldTab[j]) != null) {
 90 
 91                 oldTab[j] = null;
 92 
 93           //单节点
 94 
 95                 if (e.next == null)
 96 
 97                     newTab[e.hash & (newCap - 1)] = e;
 98 
 99           //下挂树
100 
101                 else if (e instanceof TreeNode)
102 
103                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
104 
105           //下挂链表
106 
107                 else { // preserve order
108 
109                     Node<K,V> loHead = null, loTail = null;
110 
111                     Node<K,V> hiHead = null, hiTail = null ;
112 
113                     Node<K,V> next;
114 
115                     do {
116 
117                         next = e.next;
118 
119                         if ((e.hash & oldCap) == 0) {
120 
121                             if (loTail == null)
122 
123                                 loHead = e;
124 
125                             else
126 
127                                 loTail.next = e;
128 
129                             loTail = e;
130 
131                         }
132 
133                         else {
134 
135                             if (hiTail == null)
136 
137                                 hiHead = e;
138 
139                             else
140 
141                                 hiTail.next = e;
142 
143                             hiTail = e;
144 
145                         }
146 
147                     } while ((e = next) != null);
148 
149                     if (loTail != null) {
150 
151                         loTail.next = null;
152 
153                         newTab[j] = loHead;
154 
155                     }
156 
157                     if (hiTail != null) {
158 
159                         hiTail.next = null;
160 
161                         newTab[j + oldCap] = hiHead;
162 
163                     }
164 
165                 }
166 
167             }
168 
169         }
170 
171     }
172 
173     return newTab;
174 
175 }

复制代码

 

 

 

6、链表的树化和树的链化

1、树化的几个阈值

复制代码

 1 /**
 2 
 3  * 树化必须要达到的节点数
 4 
 5  */
 6 
 7 static final int TREEIFY_THRESHOLD = 8;
 8 
 9  
10 
11 /**
12 
13  * 取消树化的节点数
14 
15  */
16 
17 static final int UNTREEIFY_THRESHOLD = 6;
18 
19  
20 
21 /**
22 
23  * 树化的最小table长度。(桶数量)
24 
25    低于这个数量说先进行table扩容
26 
27  */
28 
29 static final int MIN_TREEIFY_CAPACITY = 64;

复制代码

 

 

这几个常量规定了hashMap中用来存储节点的table数组中存储的节点形态变化的阈值。

具体可以为以下几点:

1、树化的条件有两点:

(1)这个桶中存储的节点数量已经达到了8个

(2)桶的数量(table的长度)已经到达了64

满足这个两个条件,桶中的链表就会变成一个红黑树。

2、树化之后,当树的的节点已经不足6个了,那么就会取消树化返回链表的形态。

 

树是一个红黑树。

 

7、HashMap的删除

 

复制代码

 1 /**
 2 
 3  * 删除这个KV对,如果K在hashMap中不存在,或输入的KV不对应,就返回false。
 4 
 5  * 如果KV对应,那么删除他们,返回true;如果不对应,返回false,其他的什么都不做。
 6 
 7  * 如果存在,删除这个KV对,并且返回true。
 8 
 9  */
10 
11  
12 
13 public boolean remove(Object key, Object value) {
14 
15     return removeNode(hash(key), key, value, true, true) != null;
16 
17 }
18 
19  
20 
21  
22 
23 /**
24 
25  * 删除这个K对应的V的值,并且返回这个V值,如果这个K不存在,那么返回null。
26 
27  * 这个方法和pop有些类似,删除了之后,返回一个这个对应的V值。
28 
29  */
30 
31 public V remove(Object key) {
32 
33     Node<K,V> e;
34 
35     return ( e = removeNode( hash(key), key, null, false, true) ) == null ? null : e.value;
36 
37 }
38 
39  
40 
41 /**
42 
43 * 方法为final,不可被重写。
44 
45 * 上面的两个删除的方法都是调用这个方法实现删除的。
46 
47 *
48 
49 * @param hash key的hash值,该值是通过hash(key)获取到的
50 
51 * @param key 要删除的键值对的key
52 
53 * @param value 输入的V是否有效取决于 matchValue 是否为真
54 
55 * @param matchValue 是否要求输入的KV对应,如果该值为真,但是KV不对应,就不删除。如果为假,则不关心输入的V的值
56 
57 * @param movable 删除后是否移动节点,如果为false,则不移动
58 
59 * @return 返回被删除的节点对象,如果没有删除任何节点则返回null
60 
61 */

复制代码

 

 

复制代码

  1 final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
  2 
  3     Node<K,V>[] tab; //tab : table
  4 
  5 Node<K,V> p; //p: 对应的桶中的第一个节点
  6 
  7 int n, index; //n:桶数组长度;index:对应的桶的index
  8 
  9  
 10 
 11 //桶数组不为空、对应的桶也不为空。
 12 
 13     if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
 14 
 15         Node<K,V> node = null, e;
 16 
 17   K k; 
 18 
 19   V v;
 20 
 21  
 22 
 23       //刚好是桶中的第一个节点,将 p 赋给 node
 24 
 25         if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
 26 
 27             node = p;
 28 
 29     //如果没有有那么幸运,对下挂的点检测。
 30 
 31         else if ((e = p.next) != null) {
 32 
 33        //如果是树,调用树的搜索方式,找到这个KV对。
 34 
 35             if (p instanceof TreeNode)
 36 
 37                 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
 38 
 39        //如果是一个链表,那么使用链表的查找方式,找到这个节点KV对,然后赋给node。
 40 
 41             else {
 42 
 43                 do {
 44 
 45                     if (e.hash == hash &&
 46 
 47                         ((k = e.key) == key ||
 48 
 49                          (key != null && key.equals(k)))) {
 50 
 51                         node = e;
 52 
 53                         break;
 54 
 55                     }
 56 
 57                     p = e;
 58 
 59                 } while ((e = e.next) != null);
 60 
 61             }
 62 
 63  
 64 
 65         }
 66 
 67  
 68 
 69     //如果在上面的步骤里面找到了node,并在逻辑上使用上 V 和 matchValue 判断找到的V是否有效。
 70 
 71         if (node != null && (!matchValue || (v = node.value) == value ||
 72 
 73                                          (value != null && value.equals(v)))) {
 74 
 75             if (node instanceof TreeNode)
 76 
 77                 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
 78 
 79             else if (node == p)
 80 
 81                 tab[index] = node.next;
 82 
 83             else
 84 
 85                 p.next = node.next;
 86 
 87             ++modCount;
 88 
 89             --size;
 90 
 91             afterNodeRemoval(node);
 92 
 93             return node;
 94 
 95         }
 96 
 97     }
 98 
 99     //没有找到node,下挂点为空或者是table为空,都会返回 null
100 
101     return null;
102 
103 }

复制代码

原文出处:https://www.cnblogs.com/zjxu97/p/10049493.html  

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消