JDK1.8 HashMap学习

Posted dazhu123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK1.8 HashMap学习相关的知识,希望对你有一定的参考价值。

1:源码分析

 1.1:构造方法

    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);
    }

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 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
    }

HashMap的源码中,含有两个参数的构造函数,其参数分别是initialCapacity和loadFactor,这两个参数和HashMap中原有的threshold的关系是什么尼?代码中有一行 this.threshold = tableSizeFor(initialCapacity);要将initialCapacity的值通过tableSizeFor的方法来返回一个值赋给HashMap的threshold,那这个方法有什么用处尼?

    /**
     * Returns a power of two size for the given target capacity.
     */
    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的2的幂数,详细介绍我们参考https://www.cnblogs.com/loading4/p/6239441.html

这个位运算十分高效的,写出JDK的人真的太厉害了。

 1.2:put方法

分析put方法之前,我们要分析一下hash方法的,

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

当key是null时,返回0,当不等于null时,将key的hashCode的值异或上其本身右移16位,这个操作有什么意义尼?简单来说,是为了对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。这是因为直接使用hashCode&length-1,得到数据只与hashCode的低位有关,为了避免出现两个key的hashcode的低位相同,高位不停而索引到相同的数组下标,我们将hashcode的高位数据也通过右移向异或的方式,将其不同的也影响到索引的位置,所以这样操作。

下面讲解put方法中使用的putVal的方法作用。以下图的注释讲解。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //定义一个节点Node的数组tab,和节点p,
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断table是不是null,或者长度为0,即再第一次put数据的时候,tab时空数组
        if ((tab = table) == null || (n = tab.length) == 0)
            //如果为空则,进行扩容,其中resize()方法是用来扩容使用的,这里先跳过。n的值是扩容后的数据长度
            n = (tab = resize()).length;
        //(n - 1) & hash这里是通过hash与上length-1取出hash的低位当作数组的下标
        //判断该数组是不是null,这里p指向的是用过待输入节点hasd索引的位置的节点
        if ((p = tab[i = (n - 1) & hash]) == null)
            //若为null,则newNode后,放在上面找到的位置。
            tab[i] = newNode(hash, key, value, null);
        //若该位置不为null,进入else
        else {
            Node<K,V> e; K k;
            //由于该位置不为null,所以判断该位置的节点的hash值与待输入节点的hash比较,若hash相等,且
            //该位置节点的key也等于带输入节点的key,这里用两个方法判断之间是或的关系,==或者equals方法
            //对象相等或者内容相等,二者成立一个即可。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //若该位置节点和带输入节点相等,则用e指向该位置节点。
                e = p;
            //如果该位置节点与输入节点不相等,则判断该节点是不是TreeNode。
            else if (p instanceof TreeNode)
                //若为树节点,则通过putTreeVal方法将该节点加入到red black tree中,
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //若不是树节点,则就是链表节点,按照链表进行操作
            else {
                //
                for (int binCount = 0; ; ++binCount) {
                    //e指向p的next节点,若e为null,
                    if ((e = p.next) == null) {
                        //则p.next指向输入节点即可,输入节点接在链表的尾部。
                        p.next = newNode(hash, key, value, null);
                        //如果binCount大于等于8,即超过jdk1.8规定的链表长度,
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //则进行将红黑树的转换,将链表转为红黑树
                            treeifyBin(tab, hash);
                        //然后跳出循环。
                        break;
                    }
                    //这一步操作与上面的代码功能类似,用来判断链表中是不是有与输入节点一样的节点存在,
                    //若有,直接break
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //前面有e = p.next,结合下面的操作实现向链表的后面移动的操作步骤,e始终指向下一个节点
                    p = e;
                }
            }
            //若e不等于null,意味着输入节点没有放在尾部,而是找到了相等的节点,进行替换,
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                //将替换节点的return oldValue
                return oldValue;
            }
        }
        //迭代器中实现fall fast的功能
        //This field is used to make iterators on Collection-views of the HashMap fail-fast.
        ++modCount;
        //加入后的带大小如果大于负载,则扩容,
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

上面代码注释,说明了putValue方法,何时扩容?如何查询index?如何判断是链表还是红黑树?如果返回oldValue的机制,相信见注释。

扩容分为初始化空位,使用initialCpacity或者大于设置值的的the power of two!,和size超过了有效负载后的进行扩容的机制。下面详细介绍扩容方法的原理!同样以注释的方式进行讲解。

2:扩容流程

    final Node<K,V>[] resize() {
        //用oldTab指向table
        Node<K,V>[] oldTab = table;
        //如果当前table为null,则oldCap至0,若不为null,则将table.length赋给oldCap
        //则oldCap始终为当前数组(table)的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //保存原来的有效负载数,
        int oldThr = threshold;
        //创建新的Capacity和threshold为0
        int newCap, newThr = 0;
        //如果原有容量大于0,第一次扩容是,原有的oldCap就是0.
        if (oldCap > 0) {
            //如果已经超过了最大值,
            if (oldCap >= MAXIMUM_CAPACITY) {
                //将有效负载至为整形最大值,和恐怖
                threshold = Integer.MAX_VALUE;
                //并且返回原始的table,因为超过了最大值,所以不扩容了,直接返回原始的数组。
                return oldTab;
            }
            //如果,将原来的oldCap扩到二倍赋给newCap,且小于最大容量,且oldCap是大于等于原始容量16的。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //则将oldThr也扩大二倍后赋给newThr,这里得到的newCapu和newThr都是原来的二倍。
                newThr = oldThr << 1; // double threshold
        }
        //假如,oldCap小于等于0,其实上面可以保证第一次扩容时,oldCap是等于0的。且oldThr大于0
        else if (oldThr > 0) // initial capacity was placed in threshold
            //这将oldThr赋给newCap。
            newCap = oldThr;
            //如果再oldCap为0,且oldThr也不大于0情况下,使用默认是来初始化newCap和newThr,分别为16,16*0.75 = 12
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果newThr==0,则通过newCap和loadFactor来计算得到newThr
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //将newThr赋给threshold,同时使用newcCap来创建新的newTab
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //table指向newTab
        table = newTab;
        //如果oldTab不等于null,这下面进行的就是最复炸的操作,
        //扩容后,将原数组,链表,红黑树的节点数据进行转移到新的数组中,
        //进行重新的找到位置即可!
        if (oldTab != null) {
            //以oldCap为上限来循环
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //遍历原数组,如果原数组位置不为null,则
                if ((e = oldTab[j]) != null) {
                    //将该位置至null
                    oldTab[j] = null;
                    //在上面的if中,e = oldTab[j],即e已经指向了该数组的节点了
                    //oldTab[j] = null;这操作只是将数组指向null,并不会改变e已经指向了原来的节点
                    //这意味着这里只有一个节点,
                    if (e.next == null)
                        //加入e的next指向null,则将通过e的hash与上新的length-1索引的新的坐标
                        //将e放在新的数组位置。
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        //如果该节点是treeNode,这进行红黑树的操作,暂时不会,,,,,,
                        ((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;
                            //判断扩容后,原始链表需不需要移动改变位置,如果==0.则不需要改变,否则改变
                            //而改变位置为原下标+oldCap构成新的数组下标
                            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中
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            //将需要改变位置的链表,按照原位置+olodCap放在newTab中
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        //返回扩容后的newTab
        return newTab;
    }

难点主要集中在扩容后,如何将原节点转移到新的数组中问题,扩容的判断和操作是很简单的。

 主要分为链表和红黑树的转移问题。红黑树由于我不是很懂,暂时跳过。如上面注释的那样,现在原数组的节点链表中,分成两个链表:

需要index变化的链表,不需要index变化的链表。我们分析发现index变化的链表其变化后的index都是原始index+oldCap固定在,所以可以分成两个链表后,在统一在新的index位置直接将两个链表的head节点插入数组即可!

3:多线程问题

在JDK1.7中由于,是在扩容时,将链表会顺序反过来放在newTab中,所以多线程有形参循环链表的问题,而JDK1.8中都是在尾部放入新的节点,同时也是一次性的将一条链表移动到newTab而不是,循环的插入过程,所以不会再有多线程中的循环链表的问题出现。

以上是关于JDK1.8 HashMap学习的主要内容,如果未能解决你的问题,请参考以下文章

Java中HashMap底层实现原理(JDK1.8)源码分析

JDK1.8 HashMap学习

Java中HashMap底层实现原理(JDK1.8)源码分析

jdk1.8的HashMap和ConcurrentHashMap学习摘要

高强度学习训练第十四天总结:HashMap

Java面试必问之Hashmap底层实现原理(JDK1.8)