JDK1.8 HashMap部分源码阅读

HashMap核心属性分析

//默认的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;

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

//树化阈值
static final int TREEIFY_THRESHOLD = 8;

//解除树化的阈值
static final int UNTREEIFY_THRESHOLD = 6;

//树化要求的最小容量,在达到树化阈值后要求散列表的容量达到64才会树化
static final int MIN_TREEIFY_CAPACITY = 64;

/* ------------------------ Fields ------------------------ */

//散列表
transient Node<K,V>[] table;

//hashmap中存储的键值对的数量
transient int size;

//修改次数(修改映射结构和rehash等),修改值不算
transient int modCount;

//下一次进行扩容的阈值(等于容量 * 负载因子)
int threshold;

//负载因子
final float loadFactor;

结点结构分析

static class Node<K,V> implements Map.Entry<K,V> {
    //key的hashcode经过扰乱函数后得到的hash值
    final int hash;
    //原始的key
    final K key;
    //值
    V value;
    //下一个结点
    Node<K,V> next;
    //……
}

HashMap构造方法分析

构造方法无非一个个套娃,所以看参数最多的那个构造方法就可以了。

public HashMap(int initialCapacity, float loadFactor) {
    //对传进来的 initialCapacity 和 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);
}

所以要搞懂构造方法,就得看tableSizeFor(int cap)这个方法。

/**
  * 注意:对于给定的目标容量,返回两倍大小的幂,也就是说hashmap的容量一定是2的幂
  * Returns a power of two size for the given target capacity.
  *
  * 假定给定的目标容量为 5
  *      n = cap - 1 = 4 = 0b0000 0000 0000 0100
  *      n |= n >>> 1 => 0b0000 0000 0000 0100 | 0b0000 0000 0000 0010 => 0b0000 0000 0000 0110
  *      n |= n >>> 2 => 0b0000 0000 0000 0110 | 0b0000 0000 0000 0001 => 0b0000 0000 0000 0111
  *      n |= n >>> 4 => 0b0000 0000 0000 0111 | 0b0000 0000 0000 0000 => 0b0000 0000 0000 0111
 *      ……
 * 经过这些操作之后,n的值为 7  最后就会返回 8 (aka 2^3)
 *
 * 注意:在进行移位和或操作之前先将给定的目标容量 - 1 了,为什么要这么操作?
 * 假设给定的目标容量为 16 且不进行 -1 操作
 *      n |= n >>> 1 => 0b0000 0000 0001 0000 | 0b0000 0000 0000 1000 => 0b0000 0000 0001 1000
 *      n |= n >>> 2 => 0b0000 0000 0001 1000 | 0b0000 0000 0000 0110 => 0b0000 0000 0001 1110
 *      n |= n >>> 4 => 0b0000 0000 0001 1110 | 0b0000 0000 0000 1111 => 0b0000 0000 0001 1111
 *      n |= n >>> 8 => 0b0000 0000 0001 1111 | 0b0000 0000 0000 0000 => 0b0000 0000 0001 1111
 *      ……
 * 最后n的值为31 再经过 +1 就会返回 32  所以本来只要16的容量 最后返回值却翻了倍 所以一开始要进行 -1操作
 *
 */
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;
    //如果n < 0 则返回 1,否则判断 n 是否大于等于最大容量,如果大于等于最大容量则返回最大容量,否则返回 n + 1
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

HashMap put 方法分析

先看put方法。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

put方法套娃了putVal(),然后调用putVal()时还用了hash(Object key),所以先看hash(Object key)

/**
  *
  * 对key的hashcode进行一个扰乱,让发生hash碰撞的概率降低
  * 最后返回 key 是否等于 null 如果等于null 则返回0 否则返回 (h = key.hashCode()) ^ (h >>> 16)
  *
  * 假设:
  *        h = 0b0101 0010 1101 0010 0001 1101 0010 0100
  * h >>> 16 = 0b0000 0000 0000 0000 0101 0010 1101 0010
  *         ^= 0b0000 0000 0000 0000 0001 0000 0000 0000
 * 可以看出 h >>> 16 的目的是为了将高位的数值移到低位,让高位的数值也参与计算
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

看完 hash(), 我们回到 putVal()。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //tab是散列表 n是散列表的长度
    if ((tab = table) == null || (n = tab.length) == 0)
        //如果散列表还没初始化则进行初始化 并将初始化后的长度赋给n,初始化后的散列表赋给tab
        n = (tab = resize()).length;

    //(n - 1) & hash :寻址算法 通过这个算法找到键值对要存放的位置
    //找到要存放的位置之后将 位置的值赋给i 并将散列表这个位置的结点赋给p
    //如果p == null 即表示在散列表这个位置还未存放数据
    if ((p = tab[i = (n - 1) & hash]) == null)
        //将传进来的键值对生成结点放在散列表的这个位置
        tab[i] = newNode(hash, key, value, null);

    //散列表中这个位置已经存放了结点
    else {
        Node<K,V> e; K k;

        //如果当前结点的hash值与传进来的hash值相等并且(key相同或key值相等)
        //总的来说就是 要增加的结点和当前结点相等,所以直接替换
        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或相同的key
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    //直接退出 因为找到了
                    break;


                p = e;
            }
        }
        //e不为空就代表找到了与新结点的key一样的结点
        if (e != null) { // existing mapping for key
            V oldValue = e.value;

            //onlyIfAbsent == false 或者 oldValue == null
            //onlyIfAbsent的意思是:如果存在值了是否替换,false表示替换,true则不替换
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //在LinkedHashMap中实现,目的是将结点e放到最后去
            afterNodeAccess(e);

            //返回旧的数值
            return oldValue;
        }
    }
    //修改次数+1
    //进行到这里,也就是说没有找到key值相同或相等的结点,也就是没有value值替换才会modCount++
    ++modCount;

    //存放的结点数量+1后如果大于扩容阈值(容量 * 负载因子)
    if (++size > threshold)
        //进行扩容操作
        resize();

    //LinkedHashMap里实现,是移除最老的首节点,但原文中说了是可能移除,也就是说要看removeEldestEntry是怎么重写的
    //但默认removeEldestEntry方法返回false,所以这个方法想要运行就要重写removeEldestEntry方法
    /**
     * 举例:
     * 当超出缓存容器大小时移除最老的首节点:
     *      @Override
     *      public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
     *              return size() > capacity;
     *      }
     */
    afterNodeInsertion(evict);
    return null;
}

看完putVal(),我们发现还有三个地方需要进一步深入去分析:

  • ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 即树版本的插入操作
  • treeifyBin(tab, hash); 树化操作
  • resize(); 扩容操作

先分析红黑树相关的代码,红黑树相关的操作与红黑树博客上的类似,所以只写了一丢丢帮助理解的注释。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    //父结点
    TreeNode<K,V> parent;  // red-black tree links
    //左结点
    TreeNode<K,V> left;
    //右结点
    TreeNode<K,V> right;
    //前一个结点
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    //颜色 默认是红色
    boolean red;
    //……
}
/**
 * Tree version of putVal.
 * 红黑树版本的插入值
 */
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                               int h, K k, V v) {
    Class<?> kc = null;
    boolean searched = false;
    TreeNode<K,V> root = (parent != null) ? root() : this;
    for (TreeNode<K,V> p = root;;) {
        //ph:当前结点的hash值
        //pk:当前结点的key值
        int dir, ph; K pk;

        //要放入的结点的hash值小于根节点的hash值
        if ((ph = p.hash) > h)
            dir = -1;

        //要放入的结点的hash值大于根节点的hash值
        else if (ph < h)
            dir = 1;

        //要放入的结点的key值和当前结点相同或者key值相等
        //直接返回当前结点
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;


        else if ((kc == null &&
                  (kc = comparableClassFor(k)) == null) ||
                 (dir = compareComparables(kc, k, pk)) == 0) {
            if (!searched) {
                TreeNode<K,V> q, ch;
                searched = true;
                if (((ch = p.left) != null &&
                     (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                     (q = ch.find(h, k, kc)) != null))
                    return q;
            }
            dir = tieBreakOrder(k, pk);
        }

        //插入和平衡操作
        TreeNode<K,V> xp = p;
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((TreeNode<K,V>)xpn).prev = x;
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
    //n:散列表tab的长度
    //index:根据hash和寻址算法找到的要树化的桶的位置
    //e:存放当前结点
    int n, index; Node<K,V> e;

    //如果散列表未初始化或者散列表的长度未达到树化的最小容量 = 64
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //则进行扩容
        resize();

    //判断要树化的桶存放的头结点是否为null
    else if ((e = tab[index = (n - 1) & hash]) != null) {

        //hd:头结点
        //tl:尾结点
        TreeNode<K,V> hd = null, tl = null;

        //通过循环将每个结点转换成树结点,在前后结点建立起prev和next的指针,并且找到头节点和尾结点
        do {
            //将当前结点e转换成树结点
            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);
    }
}
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {
            x.parent = null;
            //将根结点的颜色变为黑色
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {
                //ph:结点p的hash值
                //pk:结点p的key值
                int dir, ph;
                K pk = p.key;
                //比较hash值,看是比p结点大还是小还是相等
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                //dir<=0即p结点的hash>=x结点的hash,就是p结点的左结点,否则则为右结点
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    //每新插入一个结点都要进行平衡操作
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}
//红黑树的平衡操作(变色、左旋、右旋)
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                            TreeNode<K,V> x) {
    x.red = true;
    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        if (xp == (xppl = xpp.left)) {
            if ((xppr = xpp.right) != null && xppr.red) {
                xppr.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.right) {
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        else {
            if (xppl != null && xppl.red) {
                xppl.red = false;
                xp.red = false;
                xpp.red = true;
                x = xpp;
            }
            else {
                if (x == xp.left) {
                    root = rotateRight(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}

插入、平衡操作都在红黑树博客里。

接着分析 resize(),也就是扩容操作。

final Node<K,V>[] resize() {
    //扩容前的散列表
    Node<K,V>[] oldTab = table;

    //oldCap:oldTab == null,也就是说还未初始化,则 = 0,否则等于 oldTab.length(扩容前的散列表的长度)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;

    //oldThr:扩容前的扩容阈值
    int oldThr = threshold;

    //newCap:扩容后的新的容量
    //newThr:扩容后新的扩容阈值
    int newCap, newThr = 0;

    //散列表已经初始化了
    if (oldCap > 0) {
        //要进行扩容的散列表的容量已经 >= 最大容量了(2^30)
        if (oldCap >= MAXIMUM_CAPACITY) {
            //将扩容阈值设置成整型的最大值(2^31 - 1),也就是说再也不会扩容了
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //扩容后的新容量(扩容前的容量 << 1,也就是乘以2)< 最大容量(2^30),并且扩容前的容量 >= 默认的初始容量(16)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //新的扩容阈值也等于旧的扩容阈值乘以2
            newThr = oldThr << 1; // double threshold
    }

    //散列表未初始化且旧的扩容阈值 > 0
    else if (oldThr > 0) // initial capacity was placed in threshold
        //初始化容量被旧的扩容阈值替代
        newCap = oldThr;

    //散列表未初始化且旧的扩容阈值 = 0
    else {               // zero initial threshold signifies using defaults  零初始阈值表示使用默认值
        //新容量等于默认初始容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        //新扩容阈值等于默认的初始阈值 * 负载因子
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    //新的扩容阈值 = 0,也就是上面散列表未初始化且旧的扩容阈值 > 0 的情况,在这个情况下 newThr未被赋值
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        //如果新容量 < 最大容量 且 ft,即上一行算出来的新的扩容阈值 < 最大容量,则新的扩容阈值等于上面算出来的
        //否则等于Integer.MAX_VALUE
        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,接下来的操作就是将旧的散列表的值转移到新的散列表了
    table = newTab;

    //旧的散列表是已初始化了的
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;

                //oldTab[j]这个桶里只存放了一个数据
                if (e.next == null)
                    //通过寻址算法重新寻址并将这个数据存放进去
                    newTab[e.hash & (newCap - 1)] = e;

                //oldTab[j]这个桶里放了一棵红黑树
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);


                /**
                 * Q:为什么要分高位、低位?为什么高位所在的桶位是低位所在桶位 + 旧的容量?
                 * A:因为长度增加了,一个桶位里的数据可能因为高一位的值不同而产生变化,而变化量就为旧的容量。
                 *      举例:
                 *          假设原来的容量是16,扩容后的容量就为32
                 *          扩容前:
                 *              e1.hash = 0b0000 1011 & 0b0000 1111 => 0b0000 1011 = 11
                 *              e2.hash = 0b0001 1011 & 0b0000 1111 => 0b0000 1011 = 11
                 *          扩容后:
                 *              e1.hash = 0b0000 1011 & 0b0001 1111 => 0b0000 1011 = 11
                 *              e2.hash = 0b0001 1011 & 0b0001 1111 => 0b0001 1011 = 27
                 */


                    //放了一个链表
                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;
                        //e.hash & oldCap == 0,也就是说e.hash高一位也是0
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        //e.hash高一位是1
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        //高一位是0的,桶位不变
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        //高一位是1的,桶位加上旧容量
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

HashMap的put方法就分析到这啦,其他的相信你们自己也能自行分析了。

如有错误,欢迎指正!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值