HashMap源码分析

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 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 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 getNode(int hash, Object key) {
 2 
 3     Node[] tab; 
 4 
 5    Node 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)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 keySet() {
 2 
 3     Set 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> entrySet() {
2 
3     Set> 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[] tab; 
  4 
  5   Node 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 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)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[] resize() {
  2 
  3     Node[] 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[] newTab = (Node[])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 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)e).split(this, newTab, j, oldCap); 104 105           //下挂链表 106 107 else { // preserve order 108 109 Node loHead = null, loTail = null; 110 111 Node hiHead = null, hiTail = null ; 112 113 Node 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 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 removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
  2 
  3     Node[] tab; //tab : table
  4 
  5 Node 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 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)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)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 }

 

你可能感兴趣的:(HashMap源码分析)