Java HashMap底层实现和原理分析(四)

JDK1.8 HashMap介绍

  • 在JDK1.8 之前 HashMap 由 数组+链表 数据结构组成的。
  • 在JDK1.8 之后 HashMap 由 数组+链表 +红黑树数据结构组成的。

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突 (两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同) 而存在的(“拉链法”解决冲突).JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。

补充:将链表转换成红黑树前会判断,即使阈值大于8,但是数组长度小于64,此时并不会将链表变为红黑树。而是选择进行数组扩容。

这样做的目的是因为数组比较小,尽量避开红黑树结构,这种情况下变为红黑树结构,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。所以综上所述为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树。具体可以参考 treeifyBin方法。

当然虽然增了红黑树作为底层数据结构,结构变得复杂了,但是阈值大于8并且数组长度大于64时,链表转换为红黑树时,效率也变的更高效。

JDK1.8 HashMap源码分析

前面的博文中我们已经分析了JDK1.7中HashMap的源码,这篇博文我们来分析JDK1.8中HashMap的源码,看看JDK1.8中对HashMap做了哪些改变。

JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。 当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度(阀值)超过 8 时且当前数组的长度 > 64时,将链表转换为红黑树,这样大大减少了查找时间。jdk8在哈希表中引入红黑树的原因只是为了查找效率更高。

在这里插入图片描述

成员变量

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

    private static final long serialVersionUID = 362498820763181265L;

    /**
     * The default initial capacity - MUST be a power of two.
     */
     //定义默认的初始容量为16,必须是2的幂次方(面试常问,之前的博文已经分析过为啥一定是2的幂次方)
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    //最大的容量
    static final int MAXIMUM_CAPACITY = 1 << 30;

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

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
     // 当链表中元素个数大于8时,HashMap将会把链表转换为红黑树的结构。
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
     //当树中的元素个数小于6个时,HashMap会将红黑树转换为链表结构。
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
     //当链表中的元素等于8个进行创建树的时候,如果当前桶的数量小于64,则进行扩容重新分配 hash 值,而不是将节点变为红黑树。
    static final int MIN_TREEIFY_CAPACITY = 64;


    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
     //JDK1.8中改为了Node, JDK1.7中则是Entry
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;
    
    //....
}

构造方法

首先我们来看看构造方法,其实大体上跟JDK1.7是一样的。我这里重点介绍其中两个构造方法,这个构造方法跟JDK1.7是有些不同的,如下:

第一个就是无参构造方法

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

跟JDK1.7不同的是,这里的无参构造只是对成员变量loadFactor进行了赋值,并没有调用其他的构造方法,而在JDK1.7中则是调用了HashMap(int initialCapacity, float loadFactor)的构造方法

第二个就是我们两个参数的构造方法:

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        //判断初始容量是否小于0,初始容量不能小于0
        if (initialCapacity < 0)
             //如果小于0,则抛出非法的参数异常IllegalArgumentException
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //判断初始化容量initialCapacity是否大于集合的最大容量MAXIMUM_CAPACITY->2的30次幂 
        if (initialCapacity > MAXIMUM_CAPACITY)
             //如果超过MAXIMUM_CAPACITY,会将MAXIMUM_CAPACITY赋值给initialCapacity
            initialCapacity = MAXIMUM_CAPACITY;
         //判断负载因子loadFactor是否小于等于0或者是否是一个非数值
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            //如果满足上述其中之一,则抛出非法的参数异常IllegalArgumentException
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        //将指定的加载因子赋值给HashMap成员变量的负载因子loadFactor
        this.loadFactor = loadFactor;
        //前面部分跟JDK1.7都是一样的,区别就在下面
        this.threshold = tableSizeFor(initialCapacity);
    }

我们来看看这个tableSizeFor(initialCapacity)方法

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

这个方法我们就不多介绍了,前面的博文也介绍过,这个方法的作用就是得到大于给定initialCapacity的最小2的n次幂的整数。然后注意我们是把这个最小2的n次幂的整数赋值给了threshold。这个threshold的值应该是等于(capacity * load factor),而这里我们却赋值为HashMap的容量,是有问题吗?我们后面继续往后看,后面又对threshold进行了赋值的。

tableSizeFor(initialCapacity) 判断指定的初始化容量是否是2的n次幂,如果不是那么会变为比指 定初始化容量大的最小的2的n次幂。这点上述已经讲解过。
但是注意,在tableSizeFor方法体内部将计算后的数据返回给调用这里了,并且直接赋值给threshold边 界值了。有些人会觉得这里是一个bug,应该这样书写:
this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。
但是,请注意,在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推 迟到了put方法中,在put方法中会对threshold重新计算,put方法的具体实现我们下面会进行讲解

put(K key, V value)方法

接下来我们来介绍一下put()方法。

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

我们看到里面调用了putVal方法,然后传入的第一个参数是hash(key),我们来看看这个hash方法做了什么?

hash(Object key)方法

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这个hash方法其实就是拿到key对象的hashCode,然后进行扰乱函数的处理,减少hash碰撞。这个我们前面的博文也有介绍过。

下面我们继续看这个putVal()方法

putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)方法

主要参数:

  • hash key的hash值
  • key 原始Key
  • value 要存放的值
  • onlyIfAbsent 如果true代表不更改现有的值
  • evict 如果为false表示table为创建状态

我们现在开始分析这个方法,加上我们HashMap<String,String> map = new HashMap();,在HashMap的无参构造方法中并没有做其他事情,只是对成员变量loadFactor进行了赋值。然后我们调用put方法往map对象中添加元素,接下来我们来分析这个put方法中做了些什么事情。

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
    /*
    	1)transient Node<K,V>[] table; 表示存储Map集合中元素的数组。
    	2)(tab = table) == null 表示将空的table赋值给tab,然后判断tab是否等于null,第一次肯定是null
    	3)(n = tab.length) == 0 表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0
    	由于if判断使用双或,满足一个即可,则执行代码 n = (tab = resize()).length; 进行数组初始化。
    	并将初始化好的数组长度赋值给n.
    	4)执行完n = (tab = resize()).length,数组tab每个空间都是null
    */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                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) {
                        p.next = newNode(hash, key, value, null);
                        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;
    }

当第一次的时候HashMap是没有初始化的,所以条件if ((tab = table) == null || (n = tab.length) == 0)是为true的,所以会调用n = (tab = resize()).length;来进行数组的初始化,并把初始化好的数组长度赋值给n。

那么我们接下来就来看看这个resize()方法中做了什么?

这个方法简单的来说就是初始化或者是扩容的操作。

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        //得到当前数组
        Node<K,V>[] oldTab = table;
        //如果当前数组等于null长度返回0,否则返回当前数组的长度,所以当前的table是空,所以oldCap为0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //因为new HashMap()的时候并没有给threshold赋值,所以oldThr的值也为0
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            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的逻辑
        else {               // zero initial threshold signifies using defaults
            //设置一个默认的容量,也就是16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //设置阈值为(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //赋值到成员变量的阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //初始化table数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //赋值到成员变量的table中
        table = newTab;
        //因为oldTab为空,所以不会走下面的逻辑
        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;
                        }
                    }
                }
            }
        }
        // 返回新的table
        return newTab;
    }

通过上面的代码我们可以看出来,经过resize()方法之后获得一个初始容量为16的table,然后threshold为
(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)

接下来我们继续看这个putVal的方法。

 /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       /*
        	1)transient Node<K,V>[] table; 表示存储Map集合中元素的数组。
        	2)(tab = table) == null 表示将空的table赋值给tab,然后判断tab是否等于null,第一次肯定是null
        	3)(n = tab.length) == 0 表示将数组的长度0赋值给n,然后判断n是否等于0,n等于0
        	由于if判断使用双或,满足一个即可,则执行代码 n = (tab = resize()).length; 进行数组初始化。
        	并将初始化好的数组长度赋值给n.
        	4)执行完n = (tab = resize()).length,数组tab每个空间都是null
       */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
         /*
        	1)i = (n - 1) & hash 表示计算数组的索引赋值给i,即确定元素存放在哪个桶中
        	2)p = tab[i = (n - 1) & hash]表示获取计算出的位置的数据赋值给节点p
        	3) (p = tab[i = (n - 1) & hash]) == null 判断节点位置是否等于null,如果为null,则执行代码:tab[i] = newNode(hash, key, value, null);根据键值对创建新的节点放入该位置的桶中
            小结:如果当前桶没有哈希碰撞冲突,则直接把键值对插入空间位置    
        */     
        if ((p = tab[i = (n - 1) & hash]) == null)
             //创建一个新的节点存入到桶中
            tab[i] = newNode(hash, key, value, null);
        else {
            // 执行else说明tab[i]不等于null,表示这个位置已经有值了。
            Node<K,V> e; K k;
        /*
        	比较桶中第一个元素(数组中的结点)的hash值和key是否相等
        	1)p.hash == hash :p.hash表示原来存在数据的hash值  hash表示后添加数据的hash值 比较两个hash值是否相等
                 说明:p表示tab[i],即 newNode(hash, key, value, null)方法返回的Node对象。
                    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) 
                    {
                        return new Node<>(hash, key, value, next);
                    }
                    而在Node类中具有成员变量hash用来记录着之前数据的hash值的
             2)(k = p.key) == key :p.key获取原来数据的key赋值给k key表示后添加数据的key 比较两个key的地址值是否相等
             3)key != null && key.equals(k):能够执行到这里说明两个key的地址值不相等,那么先判断后添加的key是否等于null,如果不等于null再调用equals方法判断两个key的内容是否相等
        */
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                 /*
                	说明:两个元素哈希值相等,并且key的值也相等
                	将旧的元素整体对象赋值给e,用e来记录
                */ 
                e = p;
            // hash值不相等或者key不相等;判断p是否为红黑树结点
            else if (p instanceof TreeNode)
                 // 放入树中
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 说明是链表节点 
            else {
              /*
            	1)如果是链表的话需要遍历到最后节点然后插入
            	2)采用循环遍历的方式,判断链表中是否有重复的key
             */
                for (int binCount = 0; ; ++binCount) {
                  /*
                	1)e = p.next 获取p的下一个元素赋值给e
                	2)(e = p.next) == null 判断p.next是否等于null,等于null,说明p没有下一个元素,那么此时到达了链表的尾部,还没有找到重复的key,则说明HashMap没有包含该键
                	将该键值对插入链表中
                  */
                    if ((e = p.next) == null) {
                     /*
                    	1)创建一个新的节点插入到尾部(尾插法)
                    	 p.next = newNode(hash, key, value, null);
                    	 Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) 
                    	 {
                                return new Node<>(hash, key, value, next);
                         }
                         注意第四个参数next是null,因为当前元素插入到链表末尾了,那么下一个节点肯定是null
                         2)这种添加方式也满足链表数据结构的特点,每次向后添加新的元素
                    */
                        p.next = newNode(hash, key, value, null);
                        /*
                    	1)节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树
                    	2)int binCount = 0:表示for循环的初始化值。从0开始计数。记录着遍历节点的个数。值是0表示第一个节点,1表示第二个节点。。。。7表示第八个节点,加上数组中的第一个元素,元素个数是9
                    	TREEIFY_THRESHOLD - 1 --> 8 - 1 ---> 7
                    	如果binCount的值是7(加上数组中的的一个元素,元素个数是9)
                    	TREEIFY_THRESHOLD - 1也是7,此时转换红黑树
                    */
                        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))))
                         // 相等,跳出循环
                        /*
                    		要添加的元素和链表中的存在的元素的key相等了,则跳出for循环。不用再继续比较了
                    		直接执行下面的if语句去替换去 if (e != null) 
                    	*/
                        break;
                 /*
                	说明新添加的元素和当前节点不相等,继续查找下一个节点。
                	用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                */
                    p = e;
                }
            }
            /*
            	表示在桶中找到key值、hash值与插入元素相等的结点
            	也就是说通过上面的操作找到了重复的键,所以这里就是把该键的值变为新的值,并返回旧值
            	这里完成了put方法的修改功能
             */
            if (e != null) { // existing mapping for key
                // 记录e的value
                V oldValue = e.value;
                 // onlyIfAbsent为false或者旧值为null
                if (!onlyIfAbsent || oldValue == null)
                     //用新值替换旧值
                    //e.value 表示旧值  value表示新值 
                    e.value = value;
                // 访问后回调
                afterNodeAccess(e);
                 // 返回旧值
                return oldValue;
            }
        }
         //修改记录次数
        ++modCount;
         // 判断实际大小是否大于threshold阈值,如果超过则扩容
        if (++size > threshold)
            resize();
         // 插入后回调
        afterNodeInsertion(evict);
        return null;
    }

treeifyBin(Node<K,V>[] tab, int hash)方法

当节点添加完成之后判断此时节点个数是否大于TREEIFY_THRESHOLD临界值8,如果大于则将链表转换为红黑树,然后就会调用转换红黑树的方法treeifyBin。接下来我们来看看这个转换红黑树的treeifyBin方法。

   /**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     * 替换指定哈希表的索引处桶中的所有链接节点,除非表太小,在这种情况下会调整大小。
     * Node<K,V>[] tab = tab 数组名
     * int hash = hash表示哈希值
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
         /*
        	如果当前数组为空或者数组的长度小于进行树形化的阈值(MIN_TREEIFY_CAPACITY = 64),
        	就去扩容。而不是将节点变为红黑树。
        	目的:如果数组很小,那么转换红黑树,然后遍历效率要低一些。这时进行扩容,那么重新计算哈希值
        	,链表长度有可能就变短了,数据会放到数组中,这样相对来说效率高一些。
        */
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            //扩容方法
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
             /*
            	1)执行到这里说明哈希表中的数组长度大于阈值64,开始进行树形化
            	2)e = tab[index = (n - 1) & hash]表示将数组中的元素取出赋值给e,e是哈希表中指定位置桶里的链表节点,从第一个开始
            */
            //hd:红黑树的头结点   tl :红黑树的尾结点
            TreeNode<K,V> hd = null, tl = null;
            do {
                //新创建一个树的节点,内容和当前链表节点e一致
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    //将新创键的p节点赋值给红黑树的头结点
                    hd = p;
                else {
                    /*
                    	 p.prev = tl:将上一个节点p赋值给现在的p的前一个节点
                    	 tl.next = p;将现在节点p作为树的尾结点的下一个节点
                    */
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
                 /*
                	e = e.next 将当前节点的下一个节点赋值给e,如果下一个节点不等于null
                	则回到上面继续取出链表中节点转换为红黑树
                */
            } while ((e = e.next) != null);
             /*
            	让桶中的第一个元素即数组中的元素指向新建的红黑树的节点,以后这个桶里的元素就是红黑树
            	而不是链表数据结构了
            */
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

小结:上述操作一共做了如下几件事:

  1. 根据哈希表中元素个数确定是扩容还是树形化
  2. 如果是树形化遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
  3. 然后让桶中的第一个元素指向新创建的树根节点,替换桶的链表内容为树形化内容

resize()方法

接下来我们来分析一下resize()方法。

想要了解HashMap的扩容机制你要有这两个问题

  1. 什么时候才需要扩容
  2. HashMap的扩容是什么
什么时候才需要扩容

当HashMap中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。

补充:
当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,节点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的节点个数低于6,也会再把树转换为链表。

HashMap的扩容是什么

进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。

HashMap在进行扩容时,使用的rehash方式非常巧妙,因为每次扩容都是翻倍,与原来计算的 (n-1)&hash的结果相比,只是多了一个bit位,所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。

怎么理解呢?例如我们从16扩展为32时,具体的变化如下所示:

在这里插入图片描述

因此元素在重新计算hash之后,因为n变为2倍,那么n-1的标记范围在高位多1bit(红色),因此新的index就会发生这样的变化:

在这里插入图片描述

说明:5是假设计算出来的原来的索引。这样就验证了上述所描述的:扩容之后所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置。

因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就可以了,是0的话索引没变,是1的话索引变成“原索引+oldCap(原位置+旧容量)”。可以看看下图为16扩充为32的resize示意图:

在这里插入图片描述

正是因为这样巧妙的rehash方式,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,在resize的过程中保证了rehash之后每个桶上的节点数一定小于等于原来桶上的节点数,保证了rehash之后不会出现更严重的hash冲突,均匀的把之前的冲突的节点分散到新的桶中了。

下面是代码的具体实现:

  /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        //得到当前数组
        Node<K,V>[] oldTab = table;
         //如果当前数组等于null长度返回0,否则返回当前数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //当前阀值点 默认是12(16*0.75)
        int oldThr = threshold;
        int newCap, newThr = 0;
         //如果老的数组长度大于0
         //开始计算扩容后的大小
        if (oldCap > 0) {
          // 超过最大值就不再扩充了,就只好随你碰撞去吧
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
             /*
        	没超过最大值,就扩充为原来的2倍
        	1)(newCap = oldCap << 1) < MAXIMUM_CAPACITY 扩大到2倍之后容量要小于最大容量
          	2)oldCap >= DEFAULT_INITIAL_CAPACITY 原数组长度大于等于数组初始化长度16
              */
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //阈值扩大一倍
                newThr = oldThr << 1; // double threshold
        }
        //老阈值点大于0 直接赋值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr; // 老阈值赋值给新的数组长度
        else {               // zero initial threshold signifies using defaults
            // 直接使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 计算新的resize最大上限
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
         //新的阀值 默认原来是12 乘以2之后变为24
        threshold = newThr;
         //创建新的哈希表
        @SuppressWarnings({"rawtypes","unchecked"})
         //newCap是新的数组长度--> 32
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
         //判断旧数组是否等于空
        if (oldTab != null) {
         // 把每个bucket都移动到新的buckets中
        //遍历旧的哈希表的每个桶,重新计算桶里元素的新位置
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                   //原来的数据赋值为null 便于GC回收
                    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;
                            //这里来判断如果等于true e这个节点在resize之后不需要移动位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 原索引+oldCap
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 原索引放到bucket里
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                         // 原索引+oldCap放到bucket里
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

remove(Object key)方法

 /**
     * Removes the mapping for the specified key from this map if present.
     *
     * @param  key key whose mapping is to be removed from the map
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

remove方法的具体实现在removeNode方法中,所以我们重点看下removeNode方法

removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)方法
 /**
     * Implements Map.remove and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to match if matchValue, else ignored
     * @param matchValue if true only remove if value is equal
     * @param movable if false do not move other nodes while removing
     * @return the node, or null if none
     */
    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;
        //根据hash找到位置 
    	//如果当前key映射到的桶不为空
        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;
             //如果桶上的节点就是要找的key,则将node指向该节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
               //说明节点存在下一个节点
                if (p instanceof TreeNode)
                     //说明是以红黑树来处理的冲突,则获取红黑树要删除的节点
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                 //判断是否以链表方式处理hash冲突,是的话则通过遍历链表来寻找要删除的节点
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
             //比较找到的key的value和要删除的是否匹配
            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);
                else if (node == p)
                 //链表删除
                    tab[index] = node.next;
                else
                    p.next = node.next;
                //记录修改次数
                ++modCount;
                //变动的数量
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

参考

Java 8系列之重新认识HashMap

HashMap 工作原理

JDK1.8中的HashMap

HashMap学习笔记

https://gitee.com/cckevincyh/hashmap-learning

Java 7/8 HashMap源码详解与面试题分析

【黑马程序员】HashMap集合介绍

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值