Java——HashMap
HashMap
从java.util.HashMap入手。
1. 类注释大意
(1)HashMap基于接口Map所实现,实现了Map接口的所有操作,并且允许 null 值和 null 键,其实与HashTable的区别于同步和是否允许空值两个问题,再就是无法保证HashMap中存储的键的顺序,顺序不会随着时间的推移保持恒定不变。
(2)HashMap的实现保证了:在hash函数正确将元素分散于桶中的时候,那么可以基本操作(put,get)会使恒定的操作时间(O(1)级别的操作时间)。另外就是在操作过程中的时间性能与HashMap的桶的大小有关,如果需要性能好的话,不要把初始容量设置过高,负载因子也不要设置的过小。
初始容量:哈希表中存储桶的数量,初始容量只是哈希表创建时的容量。
负载因子:衡量散列表的容量在自动增加之前允许达到的满度。
当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将被重新映射(即内部数据结构将被重建),哈希表新的容量为原来容量的2倍。
(3)通常,默认负载因子(0.75)在时间和空间成本之间提供了一个很好的折衷方案。较高的值会减少空间开销,但会增加查找成本(在HashMap类的大多数操作中都得到体现,包括get和put)。 设置映射表的初始容量时,应考虑映射中的预期条目数及其负载因子,以最大程度地减少重新哈希操作的数量。如果初始容量大于最大条目数除以负载因子,则将不会进行任何哈希操作。
(4)如果将许多映射存储在HashMap实例中,则创建具有足够大容量的映射将比让其根据需要增长表的自动重新哈希处理更有效地存储映射。请注意,使用许多具有相同hashCode()的键对于任何哈希表来说,是肯定会降低性能的。
(5)请注意,此实现未同步。如果多个线程同时访问一个哈希映射,并且至少有一个线程在结构上修改该映射,则它必须在外部进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已经包含的键相关联的值不是结构修改。)通常通过在自然封装了Map的某个对象上进行同步来实现。如果不存在这样的对象,则应使用Collections.synchronizedMap方法“包装”Map。 最好在创建时完成此操作,以防止意外地不同步访问Map:Map m = Collections.synchronizedMap(new HashMap(...))。
(6)HashMap类如果在创建迭代器后的任何时间对结构进行修改,则通过迭代器自己的remove方法,迭代器将抛出ConcurrentModificationException。因此,面对并发修改,迭代器会直接失败,而不会做出存在任何可能存在风险的操作。
请注意,迭代器的快速失败行为无法得到保证,因为通常来说,在存在不同步的并发修改的情况下,不可能做出任何严格的保证。快速失败的迭代器会尽最大努力抛出ConcurrentModificationException。 因此,编写依赖于此异常的程序的正确性是错误的:迭代器的快速失败行为应仅用于检测错误。
2. 源码
源码就是对注释中的功能的实现。
2.1 HashMap的父类与它的接口
1 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{}
继承了一个抽象类AbstractMap,实现了三个接口Map,Cloneable,Serializable。
2.2 扩容(resize)
对扩容代码的分步逐步的理解。
1 /** 2 * Initializes or doubles table size. If null, allocates in 3 * accord with initial capacity target held in field threshold. 4 * Otherwise, because we are using power-of-two expansion, the 5 * elements from each bin must either stay at same index, or move 6 * with a power of two offset in the new table. 7 * 8 * 初始化或对哈希表进行扩容。 9 * 如果为null,则根据存放在threshold变量中的初始化capacity的值来分配table内存。 10 * 如果不为空,因为我们使用2的幂扩容,所以每个bin中的元素要么保持与原来相同的索引,要么在新表中以2的幂偏移。 11 * @return the table 12 */ 13 final Node<K,V>[] resize() { 14 Node<K,V>[] oldTab = table;//将原来的哈希表赋值给新的哈希表,用oldTab记录,对oldTab进行操作 15 int oldCap = (oldTab == null) ? 0 : oldTab.length;//扩容前的哈希表的容量 16 int oldThr = threshold;//oldTab的阈值 17 int newCap, newThr = 0;//新哈希表的容量和阈值 18 if (oldCap > 0) { 19 // 如果此时oldCap>=MAXIMUM_CAPACITY(1 << 30),表示已经到了最大容量, 20 // 这时还要往map中放数据,则阈值设置为整数的最大值 Integer.MAX_VALUE, 21 // 直接返回这个oldTab的内存地址。 22 if (oldCap >= MAXIMUM_CAPACITY) { 23 threshold = Integer.MAX_VALUE; 24 return oldTab; 25 } 26 // 如果(当前容量*2<最大容量&&当前容量>=默认初始化容量(16)) 27 // 并将将原容量值<<1(相当于*2)赋值给 newCap 28 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 29 oldCap >= DEFAULT_INITIAL_CAPACITY) 30 //如果能进来证明此map是扩容而不是初始化 31 //操作:将原扩容阀界值<<1(相当于*2)赋值给 newThr 32 newThr = oldThr << 1; // double threshold 33 } 34 else if (oldThr > 0) // initial capacity was placed in threshold 35 //进入此if证明创建map时用的带参构造:public HashMap(int initialCapacity)或 36 // public HashMap(int initialCapacity, float loadFactor) 37 //注:带参的构造中initialCapacity(初始容量值)不管是输入几都会通过 “this.threshold = tableSizeFor(initialCapacity);” 38 // 此方法计算出接近initialCapacity参数的2^n来作为初始化容量(初始化容量==oldThr) 39 newCap = oldThr; 40 else { // zero initial threshold signifies using defaults 41 //此处代表采用的是无参构造 42 //然后将参数newCap(新的容量)、newThr(新的扩容阀界值)进行初始化 43 newCap = DEFAULT_INITIAL_CAPACITY; 44 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 45 } 46 if (newThr == 0) { 47 //进入此if有两种可能 48 // 第一种:进入此“if (oldCap > 0)”中且不满足该if中的两个if 49 // 第二种:进入这个“else if (oldThr > 0)” 50 51 //分析:进入此if证明该map在创建时用的带参构造, 52 // 如果是第一种情况就说明是进行扩容且oldCap(旧容量)小于16,如果是第二种说明是第一次put 53 float ft = (float)newCap * loadFactor; 54 //计算扩容阀界值 55 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 56 (int)ft : Integer.MAX_VALUE); 57 } 58 threshold = newThr; 59 @SuppressWarnings({"rawtypes","unchecked"}) 60 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 61 table = newTab; 62 //如果“oldTab != null”说明是扩容,否则直接返回newTab 63 if (oldTab != null) { 64 for (int j = 0; j < oldCap; ++j) { 65 Node<K,V> e; 66 if ((e = oldTab[j]) != null) { 67 oldTab[j] = null; 68 if (e.next == null)//该桶位置只是一个单一的桶,桶上没有链表或是红黑树 69 newTab[e.hash & (newCap - 1)] = e; 70 //e是红黑树的一个实例,此种情况对红黑树进行处理 71 else if (e instanceof TreeNode) 72 //那么我们去将树上的节点rehash之后根据hash值放到新地方 73 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 74 // e是链表的情况 75 // 进行链表复制 76 // 方法比较特殊: 它并没有重新计算元素在数组中的位置 77 // 而是采用了 原始位置加原数组长度的方法计算得到位置 78 else { // preserve order 79 //此对象接收会放在原来位置 80 Node<K,V> loHead = null, loTail = null; 81 //此对象接收会放在“j + oldCap”(当前位置索引+原容量的值) 82 Node<K,V> hiHead = null, hiTail = null; 83 Node<K,V> next; 84 do { 85 next = e.next; 86 // 注意:是(e.hash & oldCap);而不是(e.hash & (oldCap-1)) 87 88 // (e.hash & oldCap) 得到的是 元素的在数组中的位置是否需要移动,示例如下 89 // 示例1: 90 // e.hash=10 0000 1010 91 // oldCap=16 0001 0000 92 // & =0 0000 0000 比较高位的第一位 0 93 //结论:元素位置在扩容后数组中的位置没有发生改变 94 95 // 示例2: 96 // e.hash=17 0001 0001 97 // oldCap=16 0001 0000 98 // & =1 0001 0000 比较高位的第一位 1 99 //结论:元素位置在扩容后数组中的位置发生了改变,新的下标位置是原下标位置+原数组长度 100 101 102 // (e.hash & (oldCap-1)) 得到的是下标位置,示例如下 103 // e.hash=10 0000 1010 104 // oldCap-1=15 0000 1111 105 // & =10 0000 1010 106 // e.hash=17 0001 0001 107 // oldCap-1=15 0000 1111 108 // & =1 0000 0001 109 //新下标位置 110 // e.hash=17 0001 0001 111 // newCap-1=31 0001 1111 newCap=32 112 // & =17 0001 0001 1+oldCap = 1+16 113 //元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化: 114 // 0000 0001->0001 0001 115 if ((e.hash & oldCap) == 0) { 116 // 如果原元素位置没有发生变化 117 if (loTail == null) 118 loHead = e; 119 // 第一次进入时 e -> aa ; loHead -> aa 120 else 121 loTail.next = e; 122 //第二次进入时 loTail-> aa; e -> bb; loTail.next -> bb;而loHead和loTail是指向同一块内存的,所以loHead.next 地址为 bb 123 //第三次进入时 loTail-> bb; e -> cc; loTail.next 地址为 cc;loHead.next.next = cc 124 loTail = e; 125 // 第一次进入时 e -> aa; loTail -> aa loTail指向了和 loHead相同的内存空间 126 // 第二次进入时 e -> bb; loTail -> bb loTail指向了和 loTail.next(loHead.next)相同的内存空间 loTail=loTail.next 127 // 第三次进入时 e -> cc; loTail -> cc loTail指向了和 loTail.next(loHead.next.next)相同的内存 128 } 129 else { 130 if (hiTail == null) 131 hiHead = e; 132 else 133 hiTail.next = e; 134 hiTail = e; 135 } 136 } while ((e = next) != null);//这一块就是 旧链表迁移新链表 137 //总结: 138 // 1.8中 旧链表迁移新链表 链表元素相对位置没有变化; 实际是对对象的内存地址进行操作 139 // 1.7中 旧链表迁移新链表 如果在新表的数组索引位置相同,则链表元素会倒置 140 if (loTail != null) { 141 loTail.next = null;// 将链表的尾节点 的next 设置为空 142 newTab[j] = loHead; 143 } 144 if (hiTail != null) { 145 hiTail.next = null;// 将链表的尾节点 的next 设置为空 146 newTab[j + oldCap] = hiHead; 147 } 148 } 149 } 150 } 151 } 152 return newTab; 153 }
从代码中可以知道,再对哈希表进行扩容的时候,分为两部解决该问题,第一步首先是通过(e.hash&oldCap)来确定元素e在扩容后是否需要移动位置,第二步通过(e.hash&(oldCap-1))来确定元素e的新位置。但是在理解上可以理解为只有一步操作,即(e.hash&(newCap-1)),因为newCap-1后与原来的哈希值进行&运算,那么结果一定是原来的结果,最后只是新的地址对于原来的哈希地址只是相对结果的变化。通过这样的方式,减少了在扩容的时候元素位置的变化,更多的只是内存地址指向的一个变化。
https://www.cnblogs.com/shianliang/p/9233199.html
https://www.cnblogs.com/pzx-java/p/9135341.html
2.3 转换
当桶节点的链表长度>=8的时候,那么就将链表转为红黑树;如果红黑树的大小<=6的时候,那么将红黑树转为链表。
待续

浙公网安备 33010602011771号