JDK1.8 hashMap源码分析

HashMap 1.8 最大的变化就是引入红黑树数据结构。

数据结构为数组+链表+红黑树。当链表的长度大于8,且总的元素大小大于等于64时,将链表修改为红黑树(时间复杂度为 O(logn)),将原来链表数据复制进去。

  问题?

  1、链表的长度大于8就转为红黑树吗?

    不是,需要满足新增元素时链表的长度已经为8,且HashMap的长度大于64,否则只是进行扩容操作

  2、为什么选择在链表长度大于8时转红黑树

    理想情况下,在随机哈希码下,哈希表中节点的频率遵循泊松分布,而根据统计,忽略方差,列表长度为K的期望出现的次数是以上的结果,可以看到其实在为8的时候概率就已经很小了,再往后调整并没有很大意义。

  3、为什么转成红黑树?

    因为链表是取一个数需要遍历链表,复杂度为O(N),而红黑树为O(logN)

  4、为什么不直接使用红黑树,而是要先使用链表再转红黑树呢?

    在HashMap源码的175行有说明,“因为树节点的大小是链表节点大小的两倍,所以只有在容器中包含足够的节点保证使用才用它”。

    显然尽管转为树使得查找的速度更快,但是在节点数比较小的时候,此时对于红黑树来说内存上的劣势会超过查找等操作的优势,自然使用链表更加好,但是在节点数比较多的时候,综合考虑,红黑树比链表要好。

  5、HashMap为什么还是线程不安全

    在JDK1.8中,在并发执行put操作时仍会发生数据覆盖的情况

 

  

 

 

  

源码:

默认初始容量为16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

最大容量为2^30

static final int MAXIMUM_CAPACITY = 1 << 30;

默认负载因子为0.75f

static final float DEFAULT_LOAD_FACTOR = 0.75f;
THRESHOLD 阀值,临界值,hashmap实际容量达到阀值后进行扩容。

hashMap的构造函数
1.无参构造,使用默认的初始容量16,默认的负载因子0.75f
   public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

  2.指定初始容量

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

  3.指定初始容量和负载因子,如果指定的初始容量大于支持的最大容量2^30次方则重设初始容量为2^30次方。

    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);  // jdk1.7此处 threshold = initialCapacity;阀值直接等于初始容量,会在第一次put时重设阀值。
    }

  然后设置阀值

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

对设置的初始容量加一后进行一系列的右移然后或运算,如设置初始容量为9,结果为16,设置初始容量为16,则结果为16。也就是说找到 小于等于(n-1)*2的最大的2的次方的值。

所以如果不指定初始容量,则初始容量和阀值都为16。

  4.直接根据接收一个map的构造函数创建map

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

put方法,才初始化node数组

    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;
      // transient Node<K,V>[] table;  如果node数组没有初始化则进行一个resize。
        if ((tab = table) == null || (n = tab.length) == 0)
      //对数组执行扩容操作。
            n = (tab = resize()).length;
      // 对数组长度减一和key的hash值进行与运算得到数组下标,查询此下标是否有值。
        if ((p = tab[i = (n - 1) & hash]) == null)
      // 没值就新建一个node将key,value放在此下标上
            tab[i] = newNode(hash, key, value, null);
        else {
      // 运算得到的数组对应的下标已经有值了,则判断已经存在的值的key和将要保存的key是否都相同
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
          // 如果相同,则将旧值p赋给e
                e = p;
            else if (p instanceof TreeNode)
          // 如果旧值是一个树结点,则将新值放进这个红黑树中。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
          // 是链表结构
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
              // 将新的key,value放在链接的新的节点上
                        p.next = newNode(hash, key, value, null);
              // TREEIFY_THRESHOLD =8,如果链表的长度大于8,且table的容量大于或等于64时(binCount=7时,链表长度为9了),则转为红黑树。
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

resize方法:在第一次put元素的时候就执行一次。final Node<K,V>[] resize()         Node<K,V>[] oldTab = table;

     // 第一次put时table为null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;  // 旧的node数组的长度
        int oldThr = threshold;  // 旧的阀值
        int newCap, newThr = 0; 
        if (oldCap > 0) {
      // 如果旧的数组的长度大于2^30次方,则旧的阀值为int型的最大值即为2^31次方
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
        // 新的数组的长度为旧的数组长度的两倍,如果旧的数组的长度的两倍小于最大容量2^30,且旧的数组的长度大于等于默认初始容量16,则新的阀值为旧的阀值的两倍。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
      //如果旧的阀值大于0,且node数组没有初始化,即刚创建hashMap,且指定了初始容量。第一次put。则,新的容量等于旧的阀值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;  // 16
        else { 
              // zero initial threshold signifies using defaults
        // 即创建HashMap用的是无参构造,还未初始化数组,则设置数组长度为默认容量16,阀值为默认负载因子0.75f*16=12

            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    // 只有在用hashmap的带参构造创建map且第一次put数组没有初始化时。此时新数组的长度为创建map时的阀值。
        if (newThr == 0) {
       // 修改创建map时赋的阀值,为其自身的0.75.而这时创建的数组的长度为创建map时赋的阀值,如创建时指定了初始容量为9,则会创建一个初始容量为16的数组,后将阀值设为12.
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof 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;
                            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;
    }

 

创建红黑树替换之前的链表

  /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY 64
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

 

1.7和1.8的区别

1、(无参构造不同)1.8的无参构造没有初始化阀值,在第一次put时如果阀值未初始化的话才设数组的容量为默认初始容量,阀值为容量的0.75。1.7的无参构造和带参构造都指定了了阀值等于初始容量。

2、(带参构造的初始阀值不同)用带参构造的话1.7的阀值等于初始容量,第一次put时设为f(n)*0.75。1.8用带参构造创建map时,阀值为f(n)即小于等于(n-1)*2的最大的2的次方的值,n为指定的初始容量或为默认初始容量

3、(数组长度的值来源不同,但结果相同)1.7和1.8都是在第一次put时初始化数组。不同的是1.7直接将数组的长度设为了f(n),将阀值设为了长度的0.75。1.8是在第一次put中的resize中初始化数组,如果是带参构造创建的map则将初始阀值设为数组的长度,再修改阀值为其自身的0.75

4、(数据结构不同)1.7的数据结构为数组+链表。1.8的数据结构为数组+链表+红黑树。当链表的长度大于8,且总的元素大小大于等于64时,将链表修改为红黑树(时间复杂度为 O(logn)),将原来链表数据复制进去

5、(resize方法不同)1.8数组未初始化时也是通过resize进行初始化的

6、1.8 扩容后链表元素不会出现倒置

7、1.7的扩容条件是(size >= threshold) && (null != table[bucketIndex]),即达到阀值,并且当前需要存放对象的slot上已经有值。从代码上看,是先扩容,然后进行新增元素操作,而1.8是增加元素之后扩容

 

未完待续...

posted @ 2019-04-02 18:46  杨岂  阅读(204)  评论(0编辑  收藏  举报