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 }
共同学习,写下你的评论
评论加载中...
作者其他优质文章