HashMap 源码浅析(jdk8 resize()详细说明)

2 篇文章 0 订阅

成员变量:

 /**
     * The default initial capacity - MUST be a power of two.
     * 默认的初始值大小,1*2的四次方也就是16.
     */
    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.
     *
     * 在构造函数中没有明确的指定负载因子时的默认值
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
     *
     * 桶上的链表转化为树的阈值,如果超过这个数值会转换为树。
     *
     *
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 桶上的树转化为链表的阈值,如果小于这个值会转换为链表
     *
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 桶的总数量大于这个值时才允许转换为树。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
/**
     * 桶
     */
    transient Node<K,V>[] table;
 /**
     *每次对map结构(put,remove)进行修改会增加1,修改存在的key不会增加1,用于fast-fail机制
     */
    transient int modCount;
 /**
     * The next size value at which to resize (capacity * load factor).
     *  阈值,如果当前map的容量大于这个值则容器需要扩容
     *  HashMap的阈值,用于判断是否需要调整HashMap的容量。
     *  threshold的值="容量*负载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍
     * @serial
     */
    int threshold;

主要方法源码解析:

tableSizeFor()方法会使map的容量一定是2的整数次幂。这个很重要,涉及到扩容rehash后键值对能正确的找到。

/**
     * Returns a power of two size for the given target capacity.
     * 返回大于输入参数且最近的2的整数次幂
     */
    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;
    }

查 get():

调用了getNode()方法

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //查找桶的位置tab[(n - 1) & hash]
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                    //比较key的hash值和equals
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                //如果是红黑树
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    //遍历链表
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

增:

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;
        //如果table么有初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            //初始化table。
            n = (tab = resize()).length;
        //如果待插入桶中没有数据则直接插入 n-1的二进制都是1(因为n是2的整数次幂100。。)
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //在上面的if判断初始化了p的值 p = tab[i = (n - 1) & hash])
            // 如果当前插入的键值对的键的hash值等于目前p的key的hash值且key的equals相等或者key的地址一致。
            //那么将p的值赋给e;
            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;
                    }
                    //如果已经存在 key在map中(key的hash和equals都相等)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e不为null说明有相同的key存在
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //如果为true则不修改已经存在的map的值除非oldValue为null
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //增加修改次数,用于fast-fail机制
        ++modCount;
        //如果当前map的大小超过了阈值则进行rehash操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在putVal()方法中有一个resize()方法中进行扩容。扩容也是HashMap中很重要的一个方法。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //如果旧table的长度大于最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //左移一位=乘以2 如果新的容量大小(旧的容量大小乘以2)小于最大的容量并且就的容量大小大于等于默认值16则新的阈值为旧的容量大小乘以2
            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
            //在就的容量和就的阈值都为0的情况下,也就是创建实例的时候没有给值,就是给新的容量默认值16,阈值为默认负载因子.75*默认容量16.
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果新的阈值为0,(旧的table的容量大于0,用户给了默认的容量大小,但是容量的大小的两倍大于最大容量值或者旧的容量的大小小于默认的容量值)
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            //新的阈值等于新的容量乘以负载因子,如果大于默认容量,则为int的最大值2的31次方-1
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //赋值新的阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
                //新容量的新的Node数组
            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) {
                    //用于GC
                    oldTab[j] = null;
                    //如果当前下标处的节点没有下一个元素,就将这个元素放入newTab的e.hash & (newCap - 1)
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //该节点是红黑树结构,也就是存在hash冲突,该hash桶有多个元素
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //将原来一个桶上的链表分为两段。
                        // 分析 e.hash & oldCap
                        //oldCap的二进制值的因为它的值肯定为2的整数次幂所以首位为1其他位为0,所以e.hash和oldCap做与运算是否为0取决于e.hash的首位是否为0
                        //根据e.hash的首位是否为0将原来一个桶上的链表分为两段,一段放j,一段放j + oldCap

                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //这里说明一下:为什么是e.hash&oldCap?插入的时候不是e.hash&(lodCap -1)吗?
                            //这里有一个很关键的点——容器的大小一定要是2的整数次幂。2的整数次幂的二进制都是以1开头后面都是0的形式。
                            //在插入的时候计算桶的位置使用的是e.hash & (oldCap-1)
                            //2的整数次幂-1的二进制特点是全部是1构成。
                            //在rehash这里使用e.hash & oldCap来判断的原因是e.hash在oldCap二进制首位的位置上是否是0,前面说过2的整数次幂的二进制都是以1开头后面都是0的形式
                            //只有在e.hash在oldCap二进制首位的位置上是1的情况下结果才不为0 。
                            //判断这个位置的数据是否为0和扩容后数据的位置有什么联系?
                            //当hashMap扩容后,容量大小的二进制在原来的基础上多了一个0.
                            // 比如原来是100-》扩容后:1000。扩容前寻找key的地址hash(key)&11->扩容后hash(key)&111
                            //可以看出进行运算的二进制多了一位1.
                            //可以看判断如果(e.hash & oldCap) == 0扩容过后的桶的位置还是在newTab[j] = loHead原来的地方。
                            //如果不为0则是在newTab[j + oldCap] = hiHead。原来的基础上加上原来容量的大小。
                            //这么处理在get()中使用e.hash & (newCap -1)可以正确的得的扩容后key的位置吗?
                            //我们来比较一下。还是用hash(key)&11->扩容后hash(key)&111这个例子。
                            // 如果(e.hash & oldCap) == 0,也就是说e.hash&100==0,e.hash从左到右的第三位上是0.
                            //如果从左到右的第三位是0,那么在get()方法中e.hash & (Cap-1)=》e.hash&111的结果是和e.hash&11一样的。
                            // 所以得到了newTab[j + oldCap] = hiHead,位置不变
                            //如果从左到右第三位是1,那么e.hash&11的结果和e.hash&111的结果差了100,正好是oldCap的容量的二进制。
                            //所以得到了newTab[j + oldCap] = hiHead;rehash后桶的位置是j + 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[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

如果不还是不理解为什么通过(e.hash & oldCap) == 0将原来的桶分为两个桶,并且这样rehash后使用get()还是能查找到对应的键值对,可以跑一下下面的程序:

for(int i = 0;i<1000;i++){
            System.out.println("老地方:"+((8-1)&i));
            if((8&i) == 0){
                System.out.println("1新地方:"+((8-1)&i));
            }else{
                System.out.println("2新地方:"+(((8-1)&i)+8));
            }

            System.out.println("计算新地方:"+((16-1)&i));
            System.out.println("-------");

        }

它模拟了容量为8,扩容后为16的HashMap的rehash。

删:

如果理解了前面的几个方法,移除键值对的方法也就很好理解了。

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
 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;
        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;
            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 {
                    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差不多。下面是移除
            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;
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值