Hashmap结构图

初始容量: 16 (1 << 4) 加载因子: 0.75f 采用懒加载模式put数据开始初始化数组
保证数组长度为2的次方
put方法
调用构造方法初始化容量和负载因子,懒加载的形式,put时候初始化table,通过key的hashcode算出hash值,(高十六位和低十六做异或计算),然后再与上数组的长度-1,算出在数组的位置。数组长度是2的幂次方,比如说16-1就是1111,hash值做与运算就是相当于取余,计算机底层运算快并且hash分布的更均匀。如果不是2的幂次方,肯定有些值取不到。


如果数组的位置上没有元素直接把新建节点放上去,如果是红黑树,则在红黑树中进行添加,如果是链表,会循环链表的节点,通过equal方法决定添加还是替换。当链表长度达到8并且数组长度达到64的话会转化成红黑树。
最后,判断节点数是否达到阈值,会进行map的扩容,它会判断原数组的长度,如果是大于1<<30,数组的最大长度,就不进行扩容。否则的话会新建一个数组,是原来的两倍大小。如果原来的桶位没有下一个元素,对新的长度重新计算索引值然后放入,如果是链表,循环元素
链表bin 的数量大于 TREEIFY_THRESHOLD(8)
容量小于 MIN_TREEIFY_CAPACITY(64),只会进行简单的扩容。
容量大于 MIN_TREEIFY_CAPACITY(64),则会进行树化改造。
源码解析
put()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)// 1 如果 table 为空或者长度为 0,即没有元素,则使用 resize()方法扩容n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)// 2 计算插入存储的数组索引 i,如果数组当前位置为空,则不存在 Hash 冲突,可以直 接插入元素。tab[i] = newNode(hash, key, value, null);else {// 3 插入新元素时,如果发生 Hash 冲突,则依次往下判断。Node<K,V> e; K k;// 3.1 判断 table[i]的元素的 key 是否与需要插入的 key 相同,// 若相同则直接用 新的 value 覆盖掉旧的 value,判断元素相等原则是 equals(),// 所以 key 对象需要重写该方法。if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 3.2 判断需要插入的数据结构是红黑树还是链表,// 如果是红黑树,则直接在树中 通过 putTreeVal()方法插入/更新键值对。else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);// 3.3 如果是链表,则在链表中插入/更新键值对else {// 3.3.1 遍历 table[i],判断 key 是否已经存在,采用 equals// 对比当前遍历结点的 key 与需要插入数据的 key,如果存在相同,则直接覆盖。// 3.3.2 遍历完毕后如果没有发现相同的 key,直接在链表尾部插入新的节点 元素。// 插入完成后判断链表长度是否>TREEIFY_THRESHOLD(8),若是,则把链 表转换为红黑树。for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 3.4 如果 e 不为空,证明 key 已经存在,直接用新的 value 覆盖旧的 value 即可。if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;// 4 插入成功后,判断实际存在的键值对数量 size>最大容量,如果大于则进行扩容。if (++size > threshold)resize();// 5 插入成功时会调用的方法(默认实现为空)afterNodeInsertion(evict);return null;}
resize
final Node<K,V>[] resize() {// 1 复制一份扩容前的数组(当前数组)Node<K,V>[] oldTab = table;// 2 保存旧的数组长度,阈值int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 3 判断扩容前的数组容量if (oldCap > 0) {// 3.1 若扩容前的数组容量超过最大值,则不再扩容。if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 3.2 若没有超过最大值,则扩容为原来二倍。else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}// 4 如果旧容量为 0,并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化 容量等于阈值。else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;// 5 如果旧容量、旧阈值都为 0,说明还没创建哈希表,容量为默认值,阈值为容量*加 载因子。else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 6 如果新的阈值为 0,则用新容量*加载因子 重新计算一次。if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 7 更新阈值。threshold = newThr;// 8 创建新链表数组,容量是原来两倍@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 9 接下来是遍历复制。table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {// 9.1 旧的桶置为空oldTab[j] = null;// 9.2 如果当前桶只有一个元素,则直接赋值给新 table 的对应位置。if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 9.3 如果当前桶是树形结构,则要把新 table 当前位置桶也变成树结 构。else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 9.4 保留旧哈希表桶中链表元素的顺序else { // preserve order//按原始链表顺序, 过滤出来扩容后位置不变的元素(低位=0),放在一起。Node<K,V> loHead = null, loTail = null;//按原始链表顺序, 过滤出来扩容后位置改变到(index+oldCap)的元素(高位=0),放在一起。Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;// 9.4.1 do-while 循环赋值给新哈希表do {next = e.next;// 9.4.2 通过计算(e.hash & oldCap) == 0 分成两条链表, 再将两条链表散列到新数组的不同位置。if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 9.4.3 放到原索引位置不变的桶中。if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 9.4.4 放到原索引+oldCap 位置的桶中。if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
HashMap默认加载因子为什么选择0.75
HashMap有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容、rehash操作(即重建内部数据结构),扩容后的哈希表将具有两倍的原容量。
通常,加载因子需要在时间和空间成本上寻求一种折衷。
负载因子的大小是主要就是决定了HashMap的数据密度的。
加载因子过高,例如为1,就会可能发生碰撞的几率越高,数组中的链表也会越容易长,虽然减少了空间开销,提高了空间利用率,但同时也增加了插入时的比较次数和查询时间成本,性能会下降。
加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数,扩容会影响性能,但是查询和插入时比较次数也会少一些,性能也会提高。
所以建议一般在使用HashMap时建议根据预估值设置初始容量,减少扩容操作,尽量给它大一点空间。
提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小;
如何解决Hash碰撞问题
— 首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法
HashMap 在并发时可能出现的问题
如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖。
如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失。
死循环问题
JDK 1.7 扩容采用的是“头插法”,会导致同一索引位置的节点在扩容后顺序反掉
JDK 1.8 之后采用的是“尾插法”,扩容后节点顺序不会反掉,不存在死循环问题。但是会因为1.8是在链表转换树或者对树进行操作的时候会出现线程安全的问题(涉及红黑树)
