Java集合-源码篇-HashMap-初始化篇

类的初始化顺序

1、成员变量

    //初始默认容量16,是2的倍数
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ;

    //最大容量,如果两个构造函数中的任何一个带参数地隐式指定了更高的值,则使用最大容量。必须是2的幂<=1<<30。
    static final int MAXIMUM_CAPACITY = 1 << 30;

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

    //树化阈值,达到8个节点数量,就转为树
    static final int TREEIFY_THRESHOLD = 8;

    //非树化阈值,节点数量变为6时,转为链表
    static final int UNTREEIFY_THRESHOLD = 6;

    //最小树化数组容量,即当数组容量达到64以上才会树化
    static final int  MIN_TREEIFY_CAPACITY = 64;

    //------------------------------------------------------------

    //存储数据的节点数组
    transient Node<K,V>[]  table;

    //entry集合
    transient Set<Entry<K,V>> entrySet;

    //元素数量
    transient int size;

    //修改次数,用于fastFail机制
    transient int modCount;

    //扩容阈值
    int threshold;

    //负载因子,可以自定义初始化
    final float loadFactor;

2、构造方法

构造方法就是如下四种,主要就是通过传入的参数赋值给负载因子loadFactory和扩容阈值threshold

这里比较特殊一点的就是第四个方法,传入了一个map,具体实现如下

    public HashMap(Map<? extends K,? extends V> map){
        //1、给负载因子赋值,默认0.75
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        //2、将参数map的值给放进新构造的map中
        putMapEntries(map, false);
    }

    // Implements Map.putAll and Map constructor.
    // 用于构造 和 putAll方法
    final void putMapEntries(Map<? extends K,? extends V> map,boolean evict){
        int size = map.size();
        if(size > 0){
            //当参数map中有值时才进行
            if(table == null){

                //table为null,即构造逻辑分支,计算map数量的扩容阈值
                float ft =  ((float) size /loadFactor) + 1.0f;
                int t = (ft < (float) MAXIMUM_CAPACITY) ? (int) ft : MAXIMUM_CAPACITY;
                if(t > threshold){
                    threshold = tableSizeFor(t);
                }

            }else if(size > threshold){
                //map数量超过扩容阈值,需要进行扩容后再进行保存map的值
                resize();
            }

            for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
                K key = entry.getKey();
                V value = entry.getValue();
                putVal(hash(key),key,value,false,evict);
            }
        }
    }

正常来说,size > threshold * loadFactory 就要扩容了,在构造逻辑判断分支里面,这里的threshold可能是没有值,就不能拿来这样计算,所以这里反过来用参数map的size来反计算出threshold的值,用于构造。

这里还有个比较有趣的方法tableSizeFor(),我们都知道,map的容量都是2的次幂的数,但是由于传入的值不一定刚好就需要计算,而这里的计算逻辑还是比较有参考意义。

    /**
     * 找出大于给定capacity的最小2的幂的数
     * 3->4,5->8,11->16
     * 原理:因为任意数的二进制最高位数都是1,要求比他大的最小2的幂的数
     * 只需要让最高有效位的前一位变为1,后面的低位变为0,就是需要的值
     * case:c = 6
     * c:0110
     * c | c >>> 1 -> 0110 | 0011  -> 0111  这就得出全是1的值了,再加1就是1000了,就是8就是我们需要的值
     * 因为int是2个字节,有16个bit,所以为了让最高位及其一下的值都变为1,需要最高右移16位
     */
    static final int tableSizeFor(int capacity){

        int n = capacity - 1;//减一是为了避免初始就是2的幂的情况,以免算出的来的值比应该算的大一倍

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

举个例子就很清楚明白了

假设 capacity = 17 ,比17大的最小2的次幂数是32 换成2进制也就是 100000

n = 17 - 1 = 16; 换成2进制就是 10000

第一次:n |= n >>> 1;

10000

01000

---------

11000

第二次:n |= n >>> 2;

11000

00110

---------

11110

第三次:n |= n >>> 4;

11110

00001

---------

11111

其实到这已经ok了

第四次,第五次都是:值已经不变了,所有位都变为了1

11111

00000

---------

11111

最后n+1 = 32(100000 二进制)就求出来了

回到原来的putMapEntries()方法,在对初始字段计算并赋值后,就是遍历把参数map的k-v添加进来。

3、主要方法

3.1、putVal()方法

当通过map进行初始化时,就需要把参数map中的一个个k-v值写进初始化的map中,也就是这里的putVal方法,这里我们再来分析这个方法。

    final V putVal(int hash,K key,V value,boolean onlyIfAbsent,boolean evict){
        //
        Node<K,V>[] tab = table;
        int length ;
        if(tab == null || (length = tab.length) == 0){
            //1、当tab为空的时候,进行初始化
            tab = resize();
            length = tab.length;
        }
        //2、计算要存储的节点重新hash散列的索引下标,存储(就是放到哪一个数组块块中)
        int reHashIndex = (length - 1) & hash;
        Node<K,V> node = tab[reHashIndex];
        if(node == null){
            //2.1、如果重新散列的下标处没有节点,即该位置第一个节点,直接赋值放到数组中
            tab[reHashIndex] = newNode(hash,key,value,null);
        }else {
            //2.2、如果该节点有值,则分不同情况放到链表中
            Node<K,V> existNode;
            if(node.hash ==hash &&
                    (node.getKey() == key || (key != null && key.equals(node.getKey())))){
                existNode = node;
            }else if(node instanceof TreeNode){
                //2.2.1、如果是树节点,用TreeNode.putTreeVal方法进行保存
                existNode =((TreeNode<K,V>)node).putTreeVal(this, tab, hash, key, value);
            }else {
                //2.2.2、如果是链表就遍历链表,添加节点
                // 是链表节点的话,如果新插入的节点是第9个节点,才对链表进行转化为红黑树
                for (int binCount = 0 ; ;++binCount){

                    if((existNode = node.next) == null){
                        //如果是最后一个节点了,则在最末尾添加进去
                        node.next = newNode(hash,key,value,null);
                        if(binCount >= TREEIFY_THRESHOLD - 1){
                            //如果达到树化的标准,即链表原数量达到8后
                            treeifyBin(tab,hash);
                        }
                        break;
                    }
                    if (existNode.hash == hash &&
                            ((key = existNode.key) == key || (key != null && key.equals(existNode.key)))){
                        //如果发现需要存储的key已经在链表中,则找到该结点
                        break;
                    }
                    //将节点往链表后面移动
                    node = existNode;
                }
            }
            //通过上述步骤找到该key在map中的位置
            if(existNode != null){
                //如果发现该key已经存在在map中,则直接更新值,没在map中的情况已经通过新增的方式在前面步骤中添加进去了
                V oldValue = existNode.value;
                if(!onlyIfAbsent || oldValue == null){
                    //并非根据仅当缺席的时候才更新,或者值为null更新
                    existNode.value=value;
                }
                afterNodeAccess(existNode);
                return oldValue;
            }
        }
        //修改次数加一
        modCount++;
        if(++size > threshold){
            //数量超过阈值,进行扩容
            resize();
        }
        afterNodeInsertion(evict);
        return null;
    }

简略说一下大概逻辑

1、初始化数组

2、计算hash在数组中的索引下标

3、判断该位置是否有节点,无值直接复制放进去

4、有值判断是否是树节点,是树节点则添加树中

5、如果是链表,判断原链表数量是否达到8,有8个则转为红黑树,再添加新节点

6、没有达到树化阈值,则直接加在链表末尾

7、在上述步骤中,如果发现该key对应节点已存在,则是在最后进行新值替换

8、最后检查map的size是否超过扩容阈值,判断是否进行扩容

3.2、treeifyBin()方法

在上面的putVal方法中知道,当链表数量达到8时再插入节点就会进行树化(链表转红黑树),就是该方法,源码及注释分析如下:

    /**
     * 尝试将链表转为树
     */
    final void treeifyBin(Node<K,V>[] tab, int hash){

        int length ,index;
        Node<K,V> e ;
        if(tab == null || (length = tab.length) < MIN_TREEIFY_CAPACITY){
            //1、当数组容量小于64时,不转红黑树,只进行扩容
            resize();
        }else if((e = tab[index = (length - 1) & hash]) != null){
            //2、该hash的数组索引处头节点开始
            TreeNode<K,V> head = null, tail = null;
            do {
                //将链表转为树节点,将所有树节点连在一起
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if(tail == null){
                    //如果没有尾节点,就直接赋值head
                    head = p;
                }else {
                    //尾节点存在,将树节点加在tail的后面,
                    //树节点,指明前后节点(左右节点)
                    p.prev = tail;
                    tail.next = p;
                }
                tail = p;
            }while ((e = e.next) != null);
            if((tab[index] = head) != null){
                //如果头节点存在,则进行红黑树转换
                //前面的树节点只是连起来了,还没做平衡处理
                head.treeify(tab);
            }
        }
    }

通过源码发现,在满足了最开始的链表数量要求后,还有对数组长度的限制,当数组长度没有达到64时,只对数组进行扩容,不进行树化操作。

基本逻辑就是:

1、判断数组长度是否达到64,没有即进行扩容

2、遍历链表,将链表节点转为树节点连起来

3、平衡树节点,形成红黑树(具体后面再细细分析)

3.3、resize()方法

从上面的putVal和treeifyBin方法中都看到了resize方法,该方法的作用就是数组进行扩容,以及相应的数据处理。

    /**
     * 扩容方法
     */
    final Node<K,V>[] resize(){
        Node<K,V>[] oldTable = table;
        int oldCapacity = oldTable == null ? 0 : oldTable.length;
        int oldThreshold = threshold;

        //根据旧map的容量和阈值计算新的容量和阈值
        int newCapacity, newThreshold = 0;
        if(oldCapacity > 0){
            //1、旧map容量大于0.说明已经初始化扩容过
            if(oldCapacity >= MAXIMUM_CAPACITY){
                //1.1、如果扩容前map容量已经大于等于最大值,则最大阈值就是maxValue,不再扩容
                threshold = Integer.MAX_VALUE;
                return oldTable;
            }else if((newCapacity = oldCapacity << 1) < MAXIMUM_CAPACITY
                    && oldCapacity >= DEFAULT_INITIAL_CAPACITY){
                //1.2、扩容后,容量扩大一倍后仍小于最大容量,且扩容前容量不超过16时,把扩容阈值也直接扩大一倍
                //这里判断旧map容量要大于等于16,是因为初始化的容量为16.阈值threshold为16 * 0.75 = 12,不能直接扩大两倍来算
                newThreshold = oldThreshold << 2;
            }
        }else if(oldThreshold > 0){
            //2、map容量为0,阈值大于0,即初始化时指定的容量,
            // 当map中无值且扩容阈值大于0时,新的初始化容量用扩容阈值代替
            newCapacity = oldThreshold;
        }else {
            //当map中无值且扩容阈值也等于0时,默认初始容量为16.默认初始扩容阈值为12 = 16 * 0.75
            newCapacity = DEFAULT_INITIAL_CAPACITY;
            newThreshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        }

        if(newThreshold == 0){
            //如果计算出来的扩容阈值为0,则用容量 * 负载因子 计算扩容阈值
            float ft = (float) newCapacity * loadFactor;
            newThreshold = (newCapacity < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ? (int) ft : Integer.MAX_VALUE);
        }
        threshold = newThreshold;

        //初始化数组
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTable = (Node<K, V>[]) new Node[newCapacity];
        table = newTable;

        if(oldTable != null){
            //3、旧map中数组不为空时,map数据迁移
            for (int j = 0; j < oldCapacity;j++){
                Node<K,V> e ;
                if((e = oldTable[j]) != null){
                    oldTable[j] = null;
                    if(e.next == null){
                        //3.1、如果该数组节点下无链表节点,即单个元素节点,
                        // 则重新散列计算该节点的数组下标位置,进行保存
                        newTable[e.hash & (newCapacity - 1)] = e;
                    }else if(e instanceof TreeNode){
                        //3.2、如果是树节点,则进行拆分
                        ((HashMap.TreeNode<K,V>)e).split(this,newTable,j,oldCapacity);
                    }else {
                        //3.3、如果是链表节点,则拆分成两个链表,分别放在高位索引和低位索引
                        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 & oldCapacity) == 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;
                            newTable[j] = loHead;
                        }
                        if(hiTail != null){
                            hiTail.next = null;
                            //因为容量扩大了一倍,即使 j + oldCapacity 也不会超出范围
                            newTable[j + oldCapacity] = hiHead;
                        }
                    }
                }
            }
        }
        return newTable;
    }

具体每个步骤分析注释可以看源码,不一一赘述,大概说一下就是:

1、计算新的数组容量capacity和扩容阈值threshold,然后重新new一个新容量的数组,

2、把数组中的单节点重新计算数组下标存储,

3、如果是树节点则进行拆分保存,

4、链表节点拆分成两个新的链表,分别放在新table中的低位索引和高位索引

以上就是大概构造hashmap的相关源码情况解析,后面有空再细说一下树节点转为红黑树的情况

PS:记录纯属学习,有问题欢迎指出!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值