HashMap的几个构造方法解析

HashMap的几个构造方法源码解析(基于JDK1.8中的HashMap源码)

1、无参构造方法HashMap()

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
*/
public HashMap() {    //无参构造器
        //负载因子为默认值 0.75f
        //容量为默认初始值 16
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

2、有一个初始容量参数的构造方法HashMap(int initialCapacity)

      参数: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) {
        //此处通过把第二个参数负载因子使用默认值0.75f,然后调用有两个参数的构造方法
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

       这个一个参数的构造方法,使用HashMap的默认负载因子,把该初始容量和默认负载因子作为入参,调用HashMap的两个参数的构造方法

3、有两个参数的构造方法HashMap(int initialCapacity, float loadFactor)

参数:initialCapacity 初始容量

参数:loadFactor  负载因子

/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     * 通过指定的初始容量和负载因子初始化一个空的HashMap
     *
     * @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) {
        if (initialCapacity < 0)   //如果初始容量小于0,抛出异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)  //如果初始容量超过最大容量(1<<32)
            initialCapacity = MAXIMUM_CAPACITY;  //则使用最大容量作为初始容量
        if (loadFactor <= 0 || Float.isNaN(loadFactor))  //如果负载因子小于等于0或者不是数字,则抛出异常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;                //把负载因子赋值给成员变量loadFactor

//调用tableSizeFor方法计算出不小于initialCapacity的最小的2的幂的结果,并赋给成员变量threshold
        this.threshold = tableSizeFor(initialCapacity); 
    }

我们下面看看tableSizeFor()这个方法是如何计算的,这个方法的实现原理很巧妙,源码如下:

/**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;      //容量减1,为了防止初始化容量已经是2的幂的情况,最后有+1运算。
        n |= n >>> 1;         //将n无符号右移一位再与n做或操作
        n |= n >>> 2;         //将n无符号右移两位再与n做或操作
        n |= n >>> 4;         //将n无符号右移四位再与n做或操作
        n |= n >>> 8;         //将n无符号右移八位再与n做或操作
        n |= n >>> 16;        //将n无符号右移十六位再与n做或操作
        //如果入参cap为小于或等于0的数,那么经过cap-1之后n为负数,n经过无符号右移和或操作后仍未负 
        //数,所以如果n<0,则返回1;如果n大于或等于最大容量,则返回最大容量;否则返回n+1
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

借用一位博主对tableSizeFor()方法的梳理:该算法分析原文地址(https://blog.csdn.net/fan2012huan/article/details/51097331)

首先,为什么要对cap做减1操作。int n = cap - 1; 
这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。如果不懂,要看完后面的几个无符号右移之后再回来看看。 
下面看看这几个无符号右移操作: 
如果n这时为0了(经过了cap-1之后),则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作)。 
这里只讨论n不等于0的情况。 
第一次右移

n |= n >> 1;

由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。 
第二次右移

n |= n >>> 2;

注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。 
第三次右移

n |= n >>> 4;

这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。 
以此类推 
注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就32个1,但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。 
举一个例子说明下吧。 


这个算法着实牛逼啊!

注意,得到的这个capacity却被赋值给了threshold。

this.threshold = tableSizeFor(initialCapacity);
开始以为这个是个Bug,感觉应该这么写:

this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;
这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。 
但是,请注意,在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。
 

4、有一个Map类型的参数的构造方法

/**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     * 根据传入的指定的Map参数去初始化一个新的HashMap,该HashMap拥有着和原Map中相同的映射关系
     *  以及默认的负载因子(0.75f)和一个大小充足的初始容量
     * @param   m the map whose mappings are to be placed in this map
     * 参数 m  一个映射关系将会被新的HashMap所取代的Map
     * @throws  NullPointerException if the specified map is null
     * 如果这个Map为空的话,将会抛出空指针异常
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;      //将默认的负载因子赋值给成员变量loadFactor
        putMapEntries(m, false);                    //调用PutMapEntries()来完成HashMap的初始化赋值过程
    }

我们看下putMapEntries()方法,这个方法调用了HashMap的resize()扩容方法和putVal()存入数据方法,源码如下:

/**
     * Implements Map.putAll and Map constructor
     *
     * @param m the map
     * @param evict false when initially constructing this map, else
     * true (relayed to method afterNodeInsertion).
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();        //定义一个s,大小等于map的大小,这个未做非空判断,可能抛出空指针异常
        if (s > 0) {                 //如果map键值对个数大于0
            if (table == null) {     // pre-size   如果当前的HashMap的table为空
                float ft = ((float)s / loadFactor) + 1.0F;   //计算HashMap的最小需要的容量
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?    //如果该容量大于最大容量,则使用最大容量
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)        //如果容量大于threshold,则对对容量计算,取大于该容量的最小的2的幂的值
                                        //并赋给threshold,作为HashMap的容量。tableSizeFor()方法讲解在上面
                                        //已经有了说明,不明白的小伙伴可以往上翻翻看。
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold) //如果table不为空,即HashMap中已经有了数据,判断Map的大小是否超过了HashMap的阈值
                resize();                //如果超过阈值,则需要对HashMap进行扩容
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {  //对Map的EntrySet进行遍历
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);        //调用HashMap的put方法的具体实现方法来对数据进行存放。
            }
        }
    }

要看懂putMapEntries()方法,就必须弄懂resize()方法和putVal()方法,下面我们对这两个方法源码进行分析。

我们下面看看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.
     * 初始化或把table容量翻倍。如果table是空,则根据threshold属性的值去初始化HashMap的容                
     * 量。如果不为空,则进行扩容,因为我们使用2的次幂来给HashMap进行扩容,所以每个箱子里的元素 
     * 必须保持在原来的位置或在新的table中以2的次幂作为偏移量进行移动
     *
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        //定义一个oldTab存放当前的table
        Node<K,V>[] oldTab = table;
        //判断当前的table是否为空,如果为空,则把0赋值给新定义的oldCap,否则以table的长度作为oldCap的大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;                //把table的阈值赋值给oldThr变量
        int newCap, newThr = 0;                //定义变量newCap和newThr来存放新的table的容量和阈值
        if (oldCap > 0) {                            //如果原来的table长度大于0
            if (oldCap >= MAXIMUM_CAPACITY) {        //判断长度是否大于HashMap的最大容量
                threshold = Integer.MAX_VALUE;        //以int的最大值作为原来HashMap的阈值,并把原来的table返回
                return oldTab;
            }
//如果原table容量不超过HashMap的最大容量,将 原容量*2 赋值给变量newCap,如果newCap不大于HashMap的最大容量,并且原容量大于HashMap的默认容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //将newThr的值设置为 原HashMap的阈值*2
                newThr = oldThr << 1; // double threshold
        }
        //如果原容量不大于0,即原table为null,并且原阈值大于0
        else if (oldThr > 0) // initial capacity was placed in threshold
            //将原阈值作为容量赋值给newCap当做newCap的值
            newCap = oldThr;
        // 如果原容量不大于0,别切原阈值也不大于0
        else {               // zero initial threshold signifies using defaults
            //则以默认容量作为newCap的值
            newCap = DEFAULT_INITIAL_CAPACITY;
            //以初始容量*默认负载因子的结果作为newThr值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //经过上面的处理过程,如果newThr值为0,给newThr进行赋值
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //将新的阈值newThr赋值给threshold,为新初始化的HashMap来使用
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
            //初始化一个新的容量大小为newCap的Node数组
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //给table重新赋值
        table = newTab;
        if (oldTab != null) {        //如果原来的HashMap中有值,则遍历
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {    //如果原来的table数组中第j个位置不为空
                    oldTab[j] = null;             //把e = oldTab[j],然后让oldTab[j]置空
                    if (e.next == null)           //如果e.next = null,说明e.next不存在其他Node
                        newTab[e.hash & (newCap - 1)] = e; //此时以e.hash&(newCap-1)的结果作为e在newTab中的位置
                    else if (e instanceof TreeNode)  //否则判断e的类型是TreeNode还是Node,即链表和红黑树判断
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果时红黑树,则进行红黑树的处理
                    else { // preserve order    //如果是链表
                        //定义了五个Node变量,我一直想知道lo和hi是是哪两个单词的缩写,
                        //根据代码来看应该是lower和higher吧,也就是高位和低位,
                        //因为我们知道HashMap扩容时,容量会扩到原容量的2倍,
                        //也就是放在链表中的Node的位置可能保持不变或位置变成 原位置+oldCap ,
                        //这里的高低应该就是这个意思吧,当然这只是个人理解。
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {                    //循环链表中的Node
                            next = e.next;
                            //如果e.hash & oldCap == 0,注意这里是oldCap,而不是oldCap-1。
                            //我们知道oldCap是2的次幂,也就是1、2、4、8、16...转化为二进制之后,
                            //都是高位为1,其它位为0。所以oldCap & e.hash 也是只有e.hash值在oldCap二进制不为0的位对应的位也不为0时,
                            //才会得到一个不为0的结果。举个例子,我们知道10010 和00010 与1111的&运算结果都是 0010  ,
                            //但是110010和010010与10000的运算结果是不一样的,所以HashMap就是利用这一点,
                            //来判断当前在链表中的数据,在扩容时位置时保持不变还是位置移动oldCap。
                            if ((e.hash & oldCap) == 0) {     //如果结果为0,即位置保持不变   
                                if (loTail == null)            //如果是第一次遍历
                                    loHead = e;                //让loHead = e
                                else
                                    loTail.next = e;            //否则,让loTail的next = e
                                loTail = e;                     //最后让loTail = e
                            }
                            //其实if 和else 中做的事情是一样的,我们看到有loHead和loTail两个Node,
                            //我们其实可以把loHead当做头元素,然后loTail是用来维护loHead的,即每次循环,
                            //更新loHead的next。我们来举个例子,比如原来的链表是A->B->C->D->E。
                            //我们这里把->假设成next关系,这五个Node中,只有C的hash & oldCap != 0 ,
                            //然后这个代码执行过程就是:
//第一次循环:      先拿到A,把A赋给loHead,然后loTail也是A
//第二次循环:      此时e的为B,而且loTail != null,也就是进入上面的else分支,把loTail.next =                     
//                 B,此时loTail中即A->B,同样反应在loHead中也是A->B,然后把loTail = B
//第三次循环:      此时e = C,由于C不满足 (e.hash & oldCap) == 0,进入到了我们下面的else分支,其 
//                 实做的事情和当前分支的意思一样,只不过维护的是hiHead和hiTail。
//第四次循环:      此时e的为D,loTail != null,进入上面的else分支,把loTail.next =                     
//                 D,此时loTail中即B->D,同样反应在loHead中也是A->B->D,然后把loTail = D
//.
//.
//.             
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //遍历结束,即把table[j]中所有的Node处理完
                        if (loTail != null) {        //如果loTail不为空,此时保证了loHead不为空
                            loTail.next = null;      //此时把loTail的next置空
                            newTab[j] = loHead;      //把loHead放在newTab数组的第j个位置上
                        }
                        if (hiTail != null) {        同理,只不过hiHead放的位置是j+oldCap
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;            //最后返回newTab
    }

我们上面讲了resize()方法,下面看看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;
        if ((tab = table) == null || (n = tab.length) == 0)            //如果hashMap为空,则使用resize方法去初始化HashMap
            n = (tab = resize()).length;            //把table数组的长度赋值给n
        if ((p = tab[i = (n - 1) & hash]) == null)    //如果tab的第(n-1) & hash为空,即此处没有Node
            tab[i] = newNode(hash, key, value, null); //则初始化一个新的Node存放在此处
        else {
            Node<K,V> e; K k;                         //如果需要存放的位置已经存在了键值对
            if (p.hash == hash &&                    //判断此处键值对的key是否和我们要存入的键值对的key相同
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;            //如果相同,则把此处的Node赋给Node e
            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) { //如果当前节点的next为空,即当前链表中不存在我们要存放的键值对
                        p.next = newNode(hash, key, value, null); //则把当前节点的next赋值为我们要存放的键值对
                        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和我们要存入的键值对的key相同的Node,则跳出循环,此时e = p.next
                        break;
                    p = e;  //把p = p.next 进行遍历
                }
            }
            if (e != null) { // existing mapping for key  //如果e不为空,此处的e为我们要存放value的键值对
                V oldValue = e.value;                    //把原来的值取出来
                if (!onlyIfAbsent || oldValue == null)   //如果onlyIfAbsent为false或者oldValue为null则进行覆盖,默认onlyIfAbsent为false
                    e.value = value;
                afterNodeAccess(e);                    //这个为linkedHashMap中才有意义,HashMap为空方法
                return oldValue;
            }
        }
        ++modCount;                                //让HashMap的修改次数+1
        if (++size > threshold)                    //判断当前Hash的键值对数量是否超过扩容阈值
            resize();                              //如果超过扩容阈值则进行扩容
        afterNodeInsertion(evict);    //这个为linkedHashMap中才有意义,HashMap为空方法
        return null;
    }

终于分析完了,自己也从中获益很多,希望看到的同学也能从中获益。2018-12-20 今天英雄联盟德玛西亚杯哦~ 

猜你喜欢

转载自blog.csdn.net/gaotiedun1/article/details/85015759
今日推荐