HashMap详解(JDK1.7和1.8)

105 阅读3分钟

​​本文已参与「新人创作礼」活动,一起开启掘金创作之路。

HashMap

HashMap JDK1.7 数据结构图

​编辑

HashMap JDK1.8 数据结构图 

​编辑

HashMap JDK1.8 put 数据流程图 (泳道图)

​编辑

HashMap源码解释

HashMap类

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

}

1.单继承自AbstractMap,多实现Map,Cloneable..

HashMap成员变量

//默认初始容量为 16= 位运算2的4次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认扩容加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转红黑树的阈值 等于9的时候才考虑是否转红黑树
static final int TREEIFY_THRESHOLD = 8;
//红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//可以树形化的容器的最小表容量。JDK1.8链表转红黑树时候的容量前提条件
static final int MIN_TREEIFY_CAPACITY = 64;
  •  默认初始容量为 16= 位运算2的4次方,不直接写16是因为位运算的效率更高。

  •  默认扩容加载因子是0.75,是因为考虑空间时间折中数字,有网友用牛顿二项式计算出适合  扩容的因子是0.6913

  •  链表转红黑树的阈值设置为8是因为波尔分布概率,当链表长度为8时的概率为

    0.00000006极低的概率,是考虑海量数据或人为干扰,变换数据结构为红黑树,此时提升性能的体现是海量数据。
    

  • JDK1.8链表转红黑树时候的容量前提条件是数组的容量大于64

HashMap放入元素时计算数组下标(不变)

//返回hash
//(h = key.hashCode()) ^ (h >>> 16) 把object方法中取出的hashcode进行位或运算返回
static final int hash(Object key) {
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

    //如果数组的(n - 1) & hash 位置是空的 :
    //n = (tab = resize()).length; n是数组长度-1
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);

    //(n - 1) & hash hash值与(数组长度-1)进行与运算才是数组下标的值
  1. hashcode与16进行位或运算作为hash值
  2. hash值再与(数组长度-1)进行与运算 作为最终数组下标值。
  •  为什么与16进行位或运算?

    • 因为效率更高
  • 为什么(数组长度-1)?

    • 因为数组长度是2的幂次方值,任何二进制数与其或运算后都会是在数组的头或尾部。如果减一,与运算结果可以散列在0-15之间

HashMap放入重复key怎么办?(不变)

if (e != null) { // existing mapping for key
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
         e.value = value;
         afterNodeAccess(e);
     return oldValue;
}

HashMap扩容时候怎么办?(重点变化)

JDK1.7

  • 当前容量>总容量 * 0.75

    • 为什么是0.75?源码中注释有介绍:是考虑到时间和空间的折中数字。
  • 扩容到原容量的2倍,保证2的指数次幂。

  • 移动原来元素去新数组(扩容后的新容量)。源码见transfer方法如下,会导致多种问题。

        void transfer(Entry[] newTable, boolean rehash){
            int newCapacity = newTable.length;
            //遍历原数组,
            for (Entry<K, V> e : table) {
                //链表遍历
                while (null != e) {
                    // 第一行
                    Entry<K, V> next = e.next;
                    if (rehash) {
                        //再次hash运算--->影响效率
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }

                    int i = indexFor(e.hash, newCapacity);
                    // 第二行
                    e.next = newTable[i];
                    // 第三行
                    newTable[i] = e;
                    // 第四行
                    e = next;
                }
            }
        }

问题点:

  • 再次hash的效率问题。
  • 移动过程中链表的先后顺序会颠倒,因为采用头插法导致。(第一行、第二行..第四行)的指针移动导致。
  • 多线程情况下:某一线程执行后链表的先后顺序会颠倒,另一线程并发移动时重新构建链表后链表呈循环状态(即链表末尾元素的next指针会指向头节点而不是null),导致再头插法进新元素时遍历链表会进入死循环。

JDK1.8

  • 当前容量>总容量 * 0.75 。 扩容到原容量的2倍,保证2的指数次幂。移动原来元素去新数组(扩容后的新容量)(没有改变思路)
  • 【变化的】是扩容时候移动的代码,如下
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }

改进点:

  • 去掉再次hash定位,效率提升。取而代之的是高地位指针【前提:数组的长度保持2的幂次方】。

    • 高位Node<K,V> hiHead = null, hiTail = null;

      • 如果(e.hash & 旧容量) == 0 用低位指针
    • 低位Node<K,V> loHead = null, loTail = null;

      • 如果(e.hash & 旧容量) != 0 用高位指针
    • 经过循环后会产生两个链表,

      • 低位链表移到新数组的与旧数组【同样位置】

      • 高位链表移到【同样位置+旧的容量 = 新位置】

        • ​编辑
  • 尾插的, 此思路有效避免循环链表问题。

  • 此思路适用于分库分表在线扩容。