HashMap1.8剖析

182 阅读7分钟

hashMap在升级至1.8后做了重大改变,这篇文章通过几个主要方法带大家看下haspmap是如何工作的。

基础结构

1.8的hashmap底层是由数组、链表、红黑树组成。简单来说,通过key计算hashcode找到数组中的位置再插入,如果位置有值就用链表存放,如果链表长度超过8就转成红黑树。

// 基础数据结构是数组
transient Node<K,V>[] table;

// 链表
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

// 红黑树
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
   TreeNode<K,V> parent;  // red-black tree links
   TreeNode<K,V> left;
   TreeNode<K,V> right;
   TreeNode<K,V> prev;    // needed to unlink next upon deletion
   boolean red;
   TreeNode(int hash, K key, V val, Node<K,V> next) {
       super(hash, key, val, next);
   }
}

new

构造一个空的hashmap,默认的长度是16,还会给一个默认的负载因子0.75。

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}


/**
 * The load factor for the hash table.(当前对象的负载因子)
 *
 * @serial
 */
final float loadFactor;


/**
 * The load factor used when none specified in constructor.(默认负载因子)
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

put

put方法时,先把key进行hash运行,算出应在数组中哪个位置。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

我们逐行来看是如何put数据的。

// hashmap的基础结构就是数组,长度总是2次幂
transient Node<K,V>[] table;

/**
 *  onlyIfAbsent:如果为true,不更改原值。
 *  evict:如果为false,说明是创建模式
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // map中的值。
    Node<K,V>[] tab; 
    // key的hash所对应的已存在对象
    Node<K,V> p; 
    // n=map长度,i=数组下标
    int n, i;
    // 如果此时table中没有值,会先进行扩容,resize方法就是扩容方法。
   if ((tab = table) == null || (n = tab.length) == 0)
       n = (tab = resize()).length;
    // 拿数组长度和hash进行与运算,找到数组下标
   if ((p = tab[i = (n - 1) & hash]) == null)
       // 如果这个位置没有值,直接存入数据
       tab[i] = newNode(hash, key, value, null);
   else {
       // e = 存入的对象
        Node<K,V> e; K k;
       // 如果存入的key相同,就覆盖
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
       // 接下来就是hash值不同的情况了,这时就会加入链表或者红黑树了
       // 这个if先处理红黑树的情况
        else if (p instanceof TreeNode)
            // 如果能正常插入不冲突,返回值将为null
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
       // 那这里就是链表的处理,如果长度过长,还会转成红黑树 
       else {
            for (int binCount = 0; ; ++binCount) {
                // 进入循环后,e=p.next,就相当于用e来找链表的尾部
                // 在最后一次循环中,e就是最后插入的数据了
                if ((e = p.next) == null) {
                    // 尾插,通过binCount的循环,找到链表的尾部,插入链表的尾部
                    p.next = newNode(hash, key, value, null);
                    // 如果数量>8就会转成红黑树
                    // static final int TREEIFY_THRESHOLD = 8;
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 在链表中找到相同的值,则记录当前p.next
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 相当于p=p.next
                p = e;
            }
        }
       // 用e来判断是否存在相同的Key
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent 可以用来控制发生冲突是保留原值,还是使用新值覆盖
            if (!onlyIfAbsent || oldValue == null)
                // 新值的覆盖
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 记录结构变更的次数
    ++modCount;
    // 判断是否需要扩容
    // threshold = 初始长度*负载因子,初始值为16*0.75 = 12
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}


默认初始长度
/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

get

get方法就简单很多,能过计算key的hash值来找位置。

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        // first = 数组的位置
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            // 从数组中找到
            return first;
        if ((e = first.next) != null) {
            // 已经升级为红黑树
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                // 链表的情况,就是next往后找
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 找不到就是null
    return null;
}

resize

通过上面的put方法就能知道,在第一次put值时就会先执行扩容,在长度超过16后,也会扩容,现在来看下扩容具体是怎么执行的。

  final Node<K,V>[] resize() {
      // 原map中的数组
       Node<K,V>[] oldTab = table;
      // 原数组容量
       int oldCap = (oldTab == null) ? 0 : oldTab.length;
      // 原数组的扩容阈值,容量*负载因子 16*0.75=12
       int oldThr = threshold;
      // 新数组的长度与扩容阈值
       int newCap, newThr = 0;
      // 原数组已满,需要扩容
       if (oldCap > 0) {
           // MAXIMUM_CAPACITY 最大容量1<<30
           if (oldCap >= MAXIMUM_CAPACITY) {
               // 无法扩容,返回
               threshold = Integer.MAX_VALUE;
               return oldTab;
           }
           // 扩容后的容量在范围内 且 原容量>=16(DEFAULT_INITIAL_CAPACITY = 默认长度=16)
           // 扩容后的容量 = 原容量左移1位,相当于翻倍,所以初始16,扩容后就是32
           else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
               // 新扩容阈值是旧的两倍
               newThr = oldThr << 1; // double threshold
       }
      // 如果原数组未设置容量,但已有阈值
      // 原阈值 给 新长度
       else if (oldThr > 0) // initial capacity was placed in threshold
           newCap = oldThr;
       else {               // zero initial threshold signifies using defaults
           // 初始map 扩容
           // 新容量 = 默认容量16,新阈值 = 16*0.75=12
           newCap = DEFAULT_INITIAL_CAPACITY;
           newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
       }
      
       if (newThr == 0) {
           // 新阈值兜底计算,最大不超过integer范围
           float ft = (float)newCap * loadFactor;
           newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                     (int)ft : Integer.MAX_VALUE);
       }
      // 赋值给map中的属性
       threshold = newThr;
      
      // 以下就是用新长度生成新的map
       @SuppressWarnings({"rawtypes","unchecked"})
       Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
       table = newTab;
       if (oldTab != null) {
           // 循环原map中的数组,挨个重新计算hash,再放入新数组
           for (int j = 0; j < oldCap; ++j) {
               // e 就代表当前数组中的链表对象
               Node<K,V> e;
               if ((e = oldTab[j]) != null) {
                   // 置为null可以方便gc回收
                   oldTab[j] = null;
                   // 当前对象不是链表,就直接根据新长度计算hash,放入新数组
                   if (e.next == null)
                       newTab[e.hash & (newCap - 1)] = e;
                   else if (e instanceof TreeNode)
                       // 原对象是红黑树,通过split方法重新生成新的红黑树或者链表,下面单开方法讲
                       ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                   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;
                       }
                   }
               }
           }
       }
       return newTab;
   }

扩容时会遇到原数组是链表或者是红黑树的情况,会有另外的处理方式,先以红黑树为例

// 红黑树的处理方式
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // Relink into lo and hi lists, preserving order
    // 定义了两类节点,lo表示新位置就在原下标,hi表示新位置=原下标+原数组长度
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        // 拿hash和原数组长度做与运算
        // 两种情况,要么与运算为0,则说明还是在原位置
        // 要么不为0,说明新位置是原位置+原数组长度。
        // 原因会在总结中详细说明。
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            // 并且记录长度
            ++lc;
        }
        else {
            // 同上,这里是新位置的情况。
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    if (loHead != null) {
        // 常量=6,如果保留在原位置上的长度<=6,就会拆成链表
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            // 生成新的红黑树
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
                loHead.treeify(tab);
        }
    }
    if (hiHead != null) {
        // 常量=6,同上。
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);
        }
    }
}

再回头来看链表的处理步骤

// 同样lo表示保持原位置  hi表示新下标=原位置+原数组长度
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;
}

总结

流程图

\

数组长度为什么是2次幂

因为2的次幂换成二进制一定是高位第1位1,后面都是0,比如16=10000,32=100000。

在计算下标时,用hash&(n-1),保留的是hash的低位值。16-1 = 10000 -1 = 01111,与运算就是保留了低4位,当从长度16扩容到32时,拿hash再与n-1做与运算,其实就是看这低位第5位是0还是1,如果是0,则是在原位置,如果是1,则是在新位置,而新位置就是最高位1+原hash的低4位,所以就是原hash的低4位+10000,也就等同于原下标位+原数组长度。举个例子如图。

在扩容遇到红黑树或者链表需要计算新下标时,也只有两种情况,所以是拿if ((e.hash & 原数组长度) == 0) 的判断,来区分新下标是保持原位,还是+原数组长度。也是得益于“数组长度一定是2次幂”这一规定,所以也是只需要看低位第n位是0还是1,就能判断。

\

为什么size超过阈值就扩容

考虑到hash碰撞严重,即使数组没有占满,但是链表或者红黑树元素就会越压越多,就会影响到整体性能。