HashMap源码分析(JDK1.8)

一、前言

HashMap是我们日常开发中处理键值对最常用的数据结构。JDK1.8对HashMap的底层实现进行了优化,如引入了红黑树、resize()调整、优化了高位运算的hash算法等。
由于JDK1.8中引入了红黑树,这也成为理解HashMap的重要一环,感兴趣的同学可以阅读我之前写的一篇文章:TreeMap源码分析(红黑树的实现过程)。本文不再分析红黑树的新增删除过程(比较复杂,尤其删除节点过程)


二、HashMap原理、特性

在JDK1.8之前,HashMap采用了数组+链表实现
HashMap源码分析(JDK1.8)_第1张图片
若hash碰撞较多,链表的长度可能过长,严重影响了HashMap的性能。JDK1.8之后,引入了红黑树处理链表过长的情况,将复杂度由O(N)优化为O(logN)
HashMap源码分析(JDK1.8)_第2张图片
特性: HashMap根据key的hashCode值存储数据,具有很快的访问速度,遍历HashMap的顺序是不确定的。HashMap最多允许一个null键,允许多个null值。HashMap非线程安全,在高并发情况下操作同一个HashMap可能会导致数据丢失等问题,建议采用ConcurrentHashMap


三、HashMap源码分析

1、数据结构

HashMap源码分析(JDK1.8)_第3张图片

核心结构:

桶数组:transient Node[] table; 一个hash表数组,用来存放Node或者TreeNode
Node:HashMap的内部类,单向链表,实现了Map.Entry接口,存放键、值、hash以及下一个节点的引用
TreeNode:JDK1.8中新引入的红黑树节点,不详述,具体参考TreeMap源码分析(红黑树的实现过程)

    static class Node<K,V> implements Map.Entry<K,V> {
    	//key的hashCode,用来定位桶的索引
        final int hash;
        final K key;
        V value;
        //指向下一个Node节点
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
    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;
        //上一个节点的引用,和next一起用于保留Node的顺序
        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);
        }
        ......

    }

通过以上3个数据结构,结合前面的原理图,我们就可以看出HashMap基本的实现过程了。
有一个桶数组,添加键值对元素的时候我们根据key计算hash值,通过和桶数组长度的模运算确定该元素在数组中添加的位置,但该位置可能已经存在相同hash的元素了,这时候就将其插入到该位置最后一个元素的后面,这样就形成了一条链表。若该链表长度超过一定值时,我们将该链表转化为红黑树,从而提升查询效率
这就是JDK1.8中HashMap的最基本原理,我们后面将详细分析。

2、重要属性

threshold:当前HashMap所能容纳的最大Node数量,超过该数量需要扩容(resize)。该值是由capacity * loadFactor计算出的
loadFactor:负载因子。相同的数组长度,负载因子越大,可容纳的Node越多。通过调节负载因子的大小我们可以调节时间与空间的利用效率。默认值0.75是比较平衡的一个选择,一般情况下不需要改变
modCount:用于记录HashMap内部结构发生变化的次数,用于迭代的快速失败(fail-fast)。在迭代开始时,会将modCount赋给expectedModCount,迭代过程中,会对2个值进行比较,若不相等(modCount变化了),则会抛出ConcurrentModificationException异常
size:HashMap中存储的Node数量
一些常量:

	//默认的桶数组初始容量16
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

	//最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

	//默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

	//当链表上Node数量超过8时转化为红黑树
    static final int TREEIFY_THRESHOLD = 8;

	//当红黑树上TreeNode数量小于6时转化为链表
    static final int UNTREEIFY_THRESHOLD = 6;

	//当桶数组大小大于64时才考虑转化为红黑树(即优先对table进行扩容)
    static final int MIN_TREEIFY_CAPACITY = 64;

3、构造方法

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

HashMap一共有4个构造方法,我们最常用的是无参构造方法,其中将loadFactor设为默认值。
第一个构造方法中我们会发现有几个奇怪的地方:①、参数中的initialCapacity初始容量只参与到了计算threshold的过程,而没有定义桶数组的初始容量。②、之前说过threshold=capacity * loadFactor,但这里并不是这样计算的,而是通过tableSizeFor计算得到的。 这两个问题我们在后面的扩容机制中会进行说明,我们先来看一下tableSizeFor这个方法是做什么的。

	//返回大于或等于cap的最小2的幂
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

直接看上面的计算过程,我想一般人都是很难理解的,我们可以自己手动计算一下。如我们指定cap=15,按照上面的步骤最终计算得到的结果就是16

4、hash算法

	//计算key的hash值
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    //定位元素在桶数组中的位置的算法,其实就是取模
    tab[(n - 1) & hash]

总的来说,JDK1.8中的hash算法分为以下三步:
①取key的hashCode值
②对hashCode做高位运算
③和桶数组长度取模确定元素位置

我们先来看下为什么 (n - 1) & hash 是取模运算:

由于HashMap中桶数组n的大小永远是2的幂,此时(n - 1) & hash=hash%n

举个例子,hash值为30,桶数组大小n为16,30%16=14,我们再用位运算计算一下(只标注了1个字节,省略左边的0)
HashMap源码分析(JDK1.8)_第4张图片这是一个很巧妙的设计,因为位运算的效率是大于模运算的
再看下为什么需要做高位运算:因为我们通常声明HashMap的时候不会指定桶数组大小或者说不会声明很大的值,这时候若直接取key的hashCode与n-1做位与运算(也就是取模,跳过第②步),由于n-1的数值较小,那么二进制高位都为0,而hashCode一般数值较大,二进制高位是有1的,位与后高位仍未0,最终实际上参与运算的只有低位。因此将key的hashCode向右移16位后再与自身做异或运算(int是4字节32位,前16位为高位,后16位为低位),使得高位也参与了运算,增加了hash的复杂度,减少了碰撞概率
举个例子整体看一下hash算法:对"test"这个String做hash算法
HashMap源码分析(JDK1.8)_第5张图片

5、核心方法探究

①查找
    public V get(Object key) {
        Node<K,V> e;
        //hash算法前文已详述
        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;
        //tab[(n - 1) & hash]定位出元素所在的位置,前文已详述。这里取出第一个Node节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //和首节点比较,若匹配则直接返回第一个Node节点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //若首节点有后续节点
            if ((e = first.next) != null) {
            	//若首节点是红黑树TreeNode节点,则调用getTreeNode查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //对该条链表循环查找,返回匹配节点
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

查找方法中的hash算法前文已详述,其它过程通过注释可以轻松理解,红黑树查找过程不做详述

②遍历

HashMap在遍历时的顺序和元素插入的顺序一般都是不一样的
我们以HashMap的keySet举例:

        for (Object key : map.keySet()) {
            //TODO
        }

这种遍历在编译时等价于通过迭代器遍历:

        Set keys = map.keySet();
        Iterator iterator = keys.iterator();
        while (iterator.hasNext()) {
            Object key = iterator.next();
            //TODO
        }

以下是遍历相关源码

    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

    final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator<K> iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }

    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            //index指向第一个包含节点的桶位置
            //next指向第一个桶中的第一个节点
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            //前面提到的modCount比对,用于fail-fast
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            //next指向下一个Node,若为null则寻找下一个包含Node的桶
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

对上面的HashMap遍历key做一个总结:首先获取HashMap的KeySet集合对象,再获取KeySet的迭代器KeyIterator。KeyIterator继承自HashIterator,在HashIterator初始化构造方法中会从桶数组中找到第一个包含Node的桶。在之后的nextNode()方法中会遍历该桶中的链表,遍历结束后会寻找下一个包含Node的桶,之后重复以上过程。
我们发现遍历桶中的Node只是不停的去寻找Node的next,并没有区分是链表还是红黑树,这是因为红黑树中仍然保留了next的引用

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

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //table为空则进行初始化,HashMap在第一次插入数据时才进行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(n - 1) & hash为上文说的取模运算计算位置,若桶中该位置不包含任何Node,则将新节点直接放入桶中即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //若该位置的桶中已经存放了Node节点
        else {
            Node<K,V> e; K k;
            //新元素和桶中第一个Node做比较,若key的值以及hash的值均相等,则将e指向第一个Node
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //若该Node为红黑树则调用红黑树的插入方法(红黑树插入过程不详细阐述)
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //若该链为链表
            else {
            	//对链表进行遍历
                for (int binCount = 0; ; ++binCount) {
                	//遍历到了该链表的尾部(最后一个Node的next肯定是null)
                    if ((e = p.next) == null) {
                    	//此时将新Node插入到尾部(作为之前最后一个Node的next引用)
                        p.next = newNode(hash, key, value, null);
                        //若链表长度大于或等于树化阈值,转化为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        //跳出循环
                        break;
                    }
                    //若遍历过程中发现存在key的值以及hash的值均相等,则直接跳出循环,此时e不为null
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //将p指向e继续遍历
                    p = e;
                }
            }
            //若链表中存在key值以及hash值均相等的节点
            if (e != null) { // existing mapping for key
            	//记录e的value值
                V oldValue = e.value;
                //若onlyIfAbsent为false或者旧值为null(onlyIfAbsent若为true,表示不替换已有的value)
                if (!onlyIfAbsent || oldValue == null)
                	//用新Node的值替换旧Node的值
                    e.value = value;
                //用于LinkedHashMap回调,HashMap不处理
                afterNodeAccess(e);
                //返回旧值
                return oldValue;
            }
        }
        //修改次数+1
        ++modCount;
        //若桶中Node数量超过阈值则扩容
        if (++size > threshold)
            resize();
        //用于LinkedHashMap回调,HashMap不处理
        afterNodeInsertion(evict);
        return null;
    }

总结一下HashMap插入元素的过程:
HashMap源码分析(JDK1.8)_第6张图片

④扩容

resize是HashMap最核心的一部分,由于桶数组的长度是有限的,若不扩容数组长度,那么随着元素的新增,碰撞概率会越来越大,极大的降低了效率。扩容的时机是初始化或元素个数超过阈值(桶数组长度*负载因子),每次扩容后桶数组长度扩大1倍,阈值也扩大一倍。之后重新计算元素的位置并调整

    final Node<K,V>[] resize() {
    	//定义oldTab、oldCap、oldThr、newCap、newThr
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //oldCap大于0,说明桶数组不为空,即已经初始化过了
        if (oldCap > 0) {
        	//若桶数组长度大于等于最大值,则将阈值赋为整数最大值并停止扩容,返回原桶数组
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //若oldCap扩容1倍后小于最大值且oldCap大于等于初始容量16,则将oldThr也扩容1倍并赋给newThr
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //oldThr大于0说明走的是有参的构造方法,这里将oldThr赋给newCap
        //这里可能会感觉很奇怪,为什么会把阈值赋给桶数组长度,我们后面具体分析
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //走的无参的构造方法,newCap为默认的桶数组容量16,newThr为默认的负载因子*默认的桶数组容量=0.75*16=12
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //newThr为0时,按照阈值公式计算阈值大小
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //将新的阈值赋给threshold
        threshold = newThr;
        //创建新的桶数组
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //table指向新的桶数组
        table = newTab;
        //若旧的桶数组不为空
        if (oldTab != null) {
        	//遍历桶数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //e指向oldTab[j],若不为空
                if ((e = oldTab[j]) != null) {
                	//将oldTabl[j]置空,便于之后gc
                    oldTab[j] = null;
                    //若e.next为null,也就是说旧的桶数组在j位置只有1个Node节点e
                    //那么对新的桶数组取模计算出存放位置并存放旧的Node节点e
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //旧桶数组该位置不止1个Node节点且e为TreeNode红黑树,则对红黑树进行拆分,这里之后会具体分析拆分过程
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //旧桶数组该位置不止1个Node节点,且为Node链表
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //遍历链表,进行分组
                        do {
                        	//next指向下一个节点
                            next = e.next;
                            //若e的hash值与旧桶数组长度位与后等于0
                            if ((e.hash & oldCap) == 0) {
                            	//若loTail为null,则将loHead指向e
                                if (loTail == null)
                                    loHead = e;
                                //否则将loTail.next指向e
                                else
                                    loTail.next = e;
                                //loTail指向e,做下一次循环
                                loTail = e;
                            }
                            //若e的hash值与旧桶数组长度位与后不等于0,同上
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            //一直循环到该链表尾部
                        } while ((e = next) != null);
                        //若loTail不为null
                        if (loTail != null) {
                        	//loTail.next置为null
                            loTail.next = null;
                            //将loHead赋给新的桶数组的j位置
                            newTab[j] = loHead;
                        }
                        //若hiTail不为null
                        if (hiTail != null) {
                        	//hiTail.next置为null
                            hiTail.next = null;
                            //将hiHead赋给新的桶数组的j+oldCap位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //返回新的桶数组
        return newTab;
    }

结合注释我们分析一下扩容的流程:

  1. 计算newCap、newThr,创建新的桶数组
  2. 遍历旧的桶数组并将Node节点存储到新的桶数组中

计算新数组的长度、阈值会分为几种情况,这在上面代码已经通过注释说明,大家可以自行理解,这里我们分析其中一种情况:oldCap=0 && oldThr>0。这是之前创建HashMap时走的有参的构造方法,相等于newCap = oldThr = tableSizeFor(initialCapacity)。也就是说我们调用有参的构造方法时传入的初始长度值会经过tableSizeFor方法变成大于或等于它的最小2的幂存入threshold,之后第一次插入元素初始化时再赋给newCap,最后再通过阈值公式重新计算newThr。
总结:threshold只是临时存储了经过二次计算(保证长度是2的幂)的桶数组长度,最终把这个值赋给了新的桶数组长度,新桶的阈值还是通过阈值公式计算的 这里就解答了前面分析构造方法时抛出的2个问题


遍历旧桶数组,重新分配到新桶数组中。在遍历过程中有3种情况:1、桶(指遍历桶数组的某一个位置)里只有1个Node 2、桶里是TreeNode红黑树 3、桶里是链表
第一种很简单,对新的桶数组取模计算出存放位置并存放旧的Node节点。第二种需要做红黑树拆分,这个放到后面和链表树化一起看。第三种对链表做分组,再分配到新桶对应的位置
在JDK1.7中,resize链表是循环遍历桶数组,元素的hash对新桶数组长度取模后定位新的位置并插入(而且有可能需要重新hash)。而在JDK1.8中,resize链表做了优化,也就是前面说的分组
HashMap源码分析(JDK1.8)_第7张图片
以桶数组大小为16举例,有2个元素,hash值为23、7,由于n-1=1111,只有后四位参与位与运算,所以得到的结果都是相同的,也就是这2个元素都存储在7号桶中,形成一条Node链表
我们将这个大小16的桶数组扩容1倍,即32,再对这2个元素取模确定在新桶中的位置
HashMap源码分析(JDK1.8)_第8张图片
扩容的时候把桶数组容量扩大1倍,那么n-1就会在高位多出1位。比如16扩容到32,n就是0001 0000->0010 0000,n-1就是0000 1111->0001 1111。也就是说参与运算的bit个数由4个变成5个,那么多出的1位bit就有可能影响运算结果,而这位bit就是扩容前的n二进制1所占的位置(n=16=0001 0000 第五位)。若hash在该位置为0,则位与后计算结果不变,该元素仍处于原位置;若hash在该位置为1,则位与后计算结果改变,该元素处于原位置+原桶数组长度。
所以,通过e.hash & oldCap就可以得知hash在oldCap二进制里1对应的位置是0还是1。说的可能有点抽象,结合上面2张图能更好的理解

根据e.hash & oldCap的结果将链表分组,为0的放在lo链表,为1的放在hi链表,最后把lo链表放在新数组原位置,hi链表放在新数组原位置+原桶数组长度的位置,这样就完成了这条链表的重分配。
对比JDK1.7,JDK1.8省去了重新计算hash的时间,并且由于扩容后hash所对应新增的1bit位置0、1是随机的,这样还可以把这条链表中的Node均匀分布到新的桶数组中。

⑤树化与拆分

开头已经列出TreeNode源码以及一些相关常量,红黑树内部的转化过程这里不细说,可以参考TreeMap源码分析(红黑树的实现过程),我们这里看一下树化和拆分

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //若桶数组长度小于树化阈值64,优先扩容数组长度而非转化为红黑树
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
        	//定义头结点hd,尾节点tl
            TreeNode<K,V> hd = null, tl = null;
            //遍历Node链表
            do {
            	//将Node节点转化为TreeNode节点
                TreeNode<K,V> p = replacementTreeNode(e, null);
                //若尾节点tl为null(第一次遍历),将头结点hd指向创建的TreeNode节点
                if (tl == null)
                    hd = p;
                //否则将p.prev指向tl节点,并且将tl.next指向p节点(将2个TreeNode节点通过next、prev关联起来,保持了顺序)
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                //将tl指向p节点,进行下一次循环
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
            	//将树形链表转化为红黑树,涉及到转化红黑树过程省略,可参考前面提到的文章
                hd.treeify(tab);
        }
    }

    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }

	//不详述
    final void treeify(Node<K,V>[] tab) {
        TreeNode<K,V> root = null;
        for (TreeNode<K,V> x = this, next; x != null; x = next) {
            next = (TreeNode<K,V>)x.next;
            x.left = x.right = null;
            if (root == null) {
                x.parent = null;
                x.red = false;
                root = x;
            }
            else {
                K k = x.key;
                int h = x.hash;
                Class<?> kc = null;
                for (TreeNode<K,V> p = root;;) {
                    int dir, ph;
                    K pk = p.key;
                    if ((ph = p.hash) > h)
                        dir = -1;
                    else if (ph < h)
                        dir = 1;
                    else if ((kc == null &&
                              (kc = comparableClassFor(k)) == null) ||
                             (dir = compareComparables(kc, k, pk)) == 0)
                        dir = tieBreakOrder(k, pk);

                    TreeNode<K,V> xp = p;
                    if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        x.parent = xp;
                        if (dir <= 0)
                            xp.left = x;
                        else
                            xp.right = x;
                        root = balanceInsertion(root, x);
                        break;
                    }
                }
            }
        }
        moveRootToFront(tab, root);
    }

我们发现树化不仅仅要满足之前说的Node链表长度大于等于阈值8,同时还要满足桶数组的容量大于等于阈值64。因为桶数组容量过小时元素的hash碰撞概率很高,与其树化链表不如扩容桶数组的容量。同时桶数组容量较小时扩容操作会比较频繁,扩容就会再次拆分红黑树,耗时耗力。

通过树化,链表不仅转化为了红黑树,同时也保留了原链表的顺序(next、prev引用),但红黑树的root节点会移动到链表首位,方便一些操作

如果之前接触过TreeMap源码那就会知道红黑树在实现的时候要求键key必须实现Comparable接口,只有这样TreeMap才知道如何比较2个值的大小。但HashMap在设计之初是没有考虑到这点的,我们从treeify方法里看看是如何做的:

  1. 比较2个元素key的hash值,相同继续判断
  2. 若key实现了Comparable接口,且2个比较元素的key是相同的Class,那么通过compareTo方法比较大小,相同继续判断
  3. 调用tieBreakOrder,其实就是调用System.identityHashCode比较大小

下面看一下拆分红黑树:

      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
          TreeNode<K,V> loHead = null, loTail = null;
          TreeNode<K,V> hiHead = null, hiTail = null;
          int lc = 0, hc = 0;
          //由于TreeNode保留了链表的next引用,所以和之前resize里遍历链表分组的方式是一样的
          for (TreeNode<K,V> e = b, next; e != null; e = next) {
              next = (TreeNode<K,V>)e.next;
              e.next = null;
              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) {
              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) {
              if (hc <= UNTREEIFY_THRESHOLD)
                  tab[index + bit] = hiHead.untreeify(map);
              else {
                  tab[index + bit] = hiHead;
                  if (loHead != null)
                      hiHead.treeify(tab);
              }
          }
      }

      final Node<K,V> untreeify(HashMap<K,V> map) {
          Node<K,V> hd = null, tl = null;
          for (Node<K,V> q = this; q != null; q = q.next) {
              Node<K,V> p = map.replacementNode(q, null);
              if (tl == null)
                  hd = p;
              else
                  tl.next = p;
              tl = p;
          }
          return hd;
      }

由于TreeNode在之前树化的过程中保留了Node的特性(next引用),所以我们在扩容拆分红黑树时,完全可以按照之前拆分Node链表一样的方式进行分组后重新塞入新桶数组。不同之处在于将红黑树拆分为lo和hi两条链表后,若链表长度小于等于阈值6,才将该链表转化为Node链表,否则将该链表重新树化

⑥删除

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //获得元素所在桶数组的位置index以及第一个Node-p,若p不存在则直接返回
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //删除元素和桶中第一个Node做比较,若key的值以及hash的值均相等,则将node指向第一个Node-p
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            //若桶数组该位置不止1个元素
            else if ((e = p.next) != null) {
            	//若p是红黑树,则调用红黑树查找节点方法,不细说
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                //若是普通链表
                else {
                	//循环该链表找到待删除Node
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //调用红黑树的删除方法,不细说
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //若node是链表的第一个节点,则将该位置第一个节点替换为下一个节点(相当于删除第一个节点)
                else if (node == p)
                    tab[index] = node.next;
                //若node不是链表第一个节点,则将p.next指向node.next,相当于删除node节点
                else
                    p.next = node.next;
                //修改次数+1
                ++modCount;
                //实际大小-1
                --size;
                afterNodeRemoval(node);
                //返回被删除的Node
                return node;
            }
        }
        return null;
    }

删除很简单了,看下注释就好了

四、JDK1.7 JDK1.8 HashMap对比

  • 数据结构由数组+链表修改为数组+链表+红黑树
  • 优化了hash算法,(h = key.hashCode()) ^ (h >>> 16),让高位参与了hash运算,增加了hash的复杂度,减少了碰撞概率
  • 扩容优化,不需要重新计算hash值,只需要根据新增的bit是1还是0决定将元素放入lo链表或hi链表。扩容后的元素要么在原位置,要么在原位置+旧桶数组长度,链表顺序和之前保持一致(jdk1.7为头插法,多线程下可能会导致死循环)

五、结语

感谢有耐心的你阅读到这里,JDK1.8 HashMap就介绍完了,和JDK1.7对比起来,优化点确实很多,源码很值得学习。本文没有细说红黑树的查找插入删除过程,有兴趣的可以参考我之前的一篇文章TreeMap源码分析(红黑树的实现过程)

参考文档

美团Java 8系列之重新认识HashMap
JDK1.8 HashMap源码分析
HashMap 源码详细分析(JDK1.8) 非常棒的一篇文章,强烈推荐

你可能感兴趣的:(源码分析)