美文网首页
java8中hashmap源码分析,put()方法详细分析

java8中hashmap源码分析,put()方法详细分析

作者: 隔壁小新 | 来源:发表于2022-10-21 20:38 被阅读0次

一.源码大纲

1.了解红黑树原理(可翻看上一个文章,[红黑树原理分析](数据结构红黑树添加、修改原理分析 - 简书 (jianshu.com)
))
2.对hashmap源码进行详细解析。

二.代码解析

1.原理分析
hashmap原理图

在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构,结构变得复杂了,但是效率也变的更高效。当一个值中要存储到Map的时候会根据Key的值来计算出他的hash,通过哈希来确认到数组的位置,如果发生哈希碰撞就以(单向)链表的形式存储,但是这样如果链表过长来的话,HashMap会把这个链表转换成红黑树来存储。

1.2 hash冲突(hash碰撞)

根据key(键)即经过一个函数f(key)得到的结果的作为地址去存放当前的key value键值对(这个是hashmap的存值方式),但是却发现算出来的地址上已经被占用了。这就是所谓的hash冲突。

1.3 hashmap的初始
  • 首先我们看一下hashmap中成员变量:
//当桶(bucket)上的结点数大于8时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;

//当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;

//桶中结构转化为红黑树对应的数组长度最小的值 
static final int MIN_TREEIFY_CAPACITY = 64;

//集合的初始化容量为16,初始化容量一定是2的n次幂
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;

//存储元素的数组,长度必须是2的n次幂
transient Node<K,V>[] table;

//存放元素的个数,注意这个不等于数组的长度。
transient int size;

//HashMap被改变的次数
transient int modCount;

//临界值 当实际大小(容量*负载因子)超过临界值时,会进行扩容
int threshold;

//存储负载因子的常量
final float loadFactor;

//默认的构造函数
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//指定容量大小的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/*
     指定“容量大小”和“加载因子”的构造函数
     initialCapacity: 指定的容量
     loadFactor:指定的加载因子
*/
public HashMap(int initialCapacity, float loadFactor) {
    //判断初始化容量initialCapacity是否小于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;
    /*
            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重新计算
        */
    this.threshold = tableSizeFor(initialCapacity);
}

/**
      Returns a power of two size for the given target capacity.
      返回比指定初始化容量大的最小的2的n次幂
*/
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;
}

1.3.1 为什么初始化容量必须为2的n次方
1.为了提高效率使用&位运算符取代了%运算,取模算法hash%length ,hashmap将其优化成位运算hash&(length-1),但hash%length等于hash&(length-1)的前提是length是2的n次幂

1.3.2红黑树与链表之间关键成员变量
当这个链表的的长度大于8的时候并且数组的长度大于64的时候,则将单链表转化为红黑树,如果链表的长度大于8,但是数组的长度小于64的时候,此时并不会把单链表转化为红黑树,而是对数组进行扩容,再对数据重新排列。
当红黑树的长度小于6的时候,则会把红黑树转换为单链表。

/**
     * 链表转为红黑树的临界值,必须大于2且最少为8
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 红黑树退化为链表的临界值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 链表转红黑树的最小容量
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
1.4 hashmap核心代码put()方法分析
  • hashmap的put()方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//获取当前key的hashCode值。
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这个是java8的散列扰动函数,用于优化散列效果。通过它获取hash值

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)
            //如果未初始化,调用resize方法 进行初始化
            n = (tab = resize()).length;
        //通过 & 运算求出该数据(key)的数组下标并判断该下标位置是否有数据
        //(n-1&hash= hash%n;对hashcode取模,获取当前hashcode在数组中的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            //如果没有,直接将数据放在该下标位置
            tab[i] = newNode(hash, key, value, null);
        //该数组下标有数据的情况
        else {
            Node<K,V> e; K k;
            //判断该位置数据的key和新来的数据是否一样
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //如果一样,证明为修改操作,该节点的数据赋值给e,后边会用到
                e = p;
            //判断是不是红黑树
            else if (p instanceof TreeNode)
                //如果是红黑树的话,进行红黑树的操作
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //新数据和当前数组既不相同,也不是红黑树节点,证明是链表
            else {
                //遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //判断next节点,如果为空的话,证明遍历到链表尾部了
                    if ((e = p.next) == null) {
                        //把新值放入链表尾部
                        p.next = newNode(hash, key, value, null);
                        //因为新插入了一条数据,所以判断链表长度是不是大于等于8
                        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;
                }
            }
            //判断e是否为空(e值为修改操作存放原数据的变量)
            if (e != null) { // existing mapping for key
                //不为空的话证明是修改操作,取出老值
                V oldValue = e.value;
                //一定会执行  onlyIfAbsent传进来的是false
                if (!onlyIfAbsent || oldValue == null)
                    //将新值赋值当前节点
                    e.value = value;
                afterNodeAccess(e);
                //返回老值
                return oldValue;
            }
        }
        //计数器,计算当前节点的修改次数
        ++modCount;
        //当前数组中的数据数量如果大于扩容阈值
        if (++size > threshold)
            //进行扩容操作
            resize();
        //空方法
        afterNodeInsertion(evict);
        //添加操作时 返回空值
        return null;
    }

当前方法分为两种情况,如果下标下没有值,直接放入值,如果下表下有值,对值进行判断,当出现key值相同的,直接覆盖,当链表小于8时,直接添加,当链表大于8的时候进行单链表转换红黑树的操作。

final void treeifyBin(Node < K, V > [] tab,int hash)
 {
    int n, index;
    Node < K, V > e; 
    // 这块就是我们上面提到的,不一定树化还可能只是扩容。主要桶数组容量是否小于64 MIN_TREEIFY_CAPACITY
//如果数组的容量小于64的时候,不进行转换红黑树,而是进行扩容
    if(tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
        resize();
    else if((e = tab[index = (n - 1) & hash]) != null){ 
    // 又是单词缩写;hd = head (头部),tl = tile (结尾) 
    TreeNode<K,V> hd = null, tl = null; 
    do {
        // 将当前单项列表转化为双向链表
        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); 
    } 
}

判断当前数组的容量是否大于64如果小于64,对当前数组进行扩容操作。如果当前容量大于64,把单项链表转化为双向链表,并把双向链表放置到当前列表的位置,调用treeify方法进行红黑树的转换。

/**
 * 参数为HashMap的元素数组
 */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null; // 定义树的根节点
  // 遍历遍历双向链表,x指向当前节点、next指向下一个节点
    for (TreeNode<K,V> x = this, next; x != null; x = next) { 
        next = (TreeNode<K,V>)x.next; // 下一个节点
        x.left = x.right = null; // 设置当前节点的左右节点为空
   // 如果还没有根节点,设置当前点为根节点,红黑树变化规则root节点变黑
        if (root == null) {
            x.parent = null; // 当前节点的父节点设为空
            x.red = false; // 当前节点的红色属性设为false(把当前节点设为黑色)
            root = x; // 根节点指向到当前节点
        }
// 如果已经存在根节点了
        else { 
            K k = x.key; // 取得当前链表节点的key
            int h = x.hash; // 取得当前链表节点的hash值
            Class<?> kc = null; // 定义key所属的Class
  // 从根节点开始遍历,寻找插入点,此遍历没有设置边界,只能从内部跳出
            for (TreeNode<K,V> p = root;;) { 
                // GOTO1
                //两值比较大小,判断插入方向。
                int dir, ph; // dir 标识方向(左右)、ph标识当前树节点的hash值
                K pk = p.key; // 当前树节点的key
                if ((ph = p.hash) > h) // 如果当前树节点hash值 大于 当前链表节点的hash值
                    dir = -1; // 标识当前链表节点会放到当前树节点的左侧
                else if (ph < h)
                    dir = 1; // 右侧
 
                /*
                 * 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
                 * 如果当前链表节点的key实现了comparable接口,并且当前树节点和      
                 链表节点是相同Class的实例,那么通过comparable的方式再比较两者。
                 * 如果还是相等,最后再通过tieBreakOrder比较一次
                 */
                else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk); 
                /*
                 * 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧位置,继续遍历找位置。
                 * 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧位置,继 
                  续遍历找位置
                 */
                //如果左边或者右边不为空的话,循环下一个节点继续找寻插入点
                        TreeNode<K,V> xp = 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;
                        }
            }
        }
    }
    // 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
    // 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象一定根节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。
    moveRootToFront(tab, root); // 单独解析
}

当前过程,首先,对双向链表进行循环操作,如果root没有把当前节点设置为root节点,如果root节点存在,从root节点开始对比,当前值需要插入到左边,还
是右边,找到当前值的插入点,插入当前值,对当前值进行红黑树平衡操作

/**
 * 红黑树插入节点后,需要重新平衡
 * root 当前根节点
 * x 新插入的节点
 * 返回重新平衡后的根节点
 */
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
    x.red = true; // 新插入的节点标为红色
 
    /*
     * 这一步即定义了变量,又开起了循环,循环没有控制条件,只能从内部跳出
     * xp:当前节点的父节点、xpp:爷爷节点、xppl:左叔叔节点、xppr:右叔叔节点
     */
    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); // 针对爷爷节点做左旋
                    }
                }
            }
        }
    }
}

流程:

  1. 当节点为root节点的话,修改为黑色,直接平衡不需要操作。
  2. 当节点的父亲节点为黑色的时候,直接平衡不需要操作。
  3. 当父节节点(红色)为左节点的时候,叔叔节点为红色,父节点,和叔叔节点变为黑色,父节点的父亲节点点变为红色,已当前祖父节点为当前节点进行第二次循环判断。
  4. 当父亲节点为左节点,叔叔节点为黑色或者不存在的时候,如果当前节点为父亲节点的右孩子,进行以父节点进行左旋转,当前节点变为父亲节点,父亲节点变为当前节点,把父亲节点变为黑色,父节点的父亲节点(祖父节点变为红色),以祖父节点进行右旋,得到平衡。
    5.当父亲节点为右节点情况与左节点相反。

/**
 * 节点左旋
 * root 根节点
 * p 要左旋的节点
 */
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                              TreeNode<K,V> p) {
    TreeNode<K,V> r, pp, rl;
    if (p != null && (r = p.right) != null) { // 要左旋的节点以及要左旋的节点的右孩子不为空
        if ((rl = p.right = r.left) != null) // 要左旋的节点的右孩子的左节点 赋给 要左旋的节点的右孩子 节点为:rl
            rl.parent = p; // 设置rl和要左旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
 
        // 将要左旋的节点的右孩子的父节点  指向 要左旋的节点的父节点,相当于右孩子提升了一层,
        // 此时如果父节点为空, 说明r 已经是顶层节点了,应该作为root 并且标为黑色
        if ((pp = r.parent = p.parent) == null) 
            (root = r).red = false;
        else if (pp.left == p) // 如果父节点不为空 并且 要左旋的节点是个左孩子
            pp.left = r; // 设置r和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
        else // 要左旋的节点是个右孩子
            pp.right = r; 
        r.left = p; // 要左旋的节点  作为 他的右孩子的左节点
        p.parent = r; // 要左旋的节点的右孩子  作为  他的父节点
    }
    return root; // 返回根节点
}
 
/**
 * 节点右旋
 * root 根节点
 * p 要右旋的节点
 */
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
                                               TreeNode<K,V> p) {
    TreeNode<K,V> l, pp, lr;
    if (p != null && (l = p.left) != null) { // 要右旋的节点不为空以及要右旋的节点的左孩子不为空
        if ((lr = p.left = l.right) != null) // 要右旋的节点的左孩子的右节点 赋给 要右旋节点的左孩子 节点为:lr
            lr.parent = p; // 设置lr和要右旋的节点的父子关系【之前只是爹认了孩子,孩子还没有答应,这一步孩子也认了爹】
 
        // 将要右旋的节点的左孩子的父节点  指向 要右旋的节点的父节点,相当于左孩子提升了一层,
        // 此时如果父节点为空, 说明l 已经是顶层节点了,应该作为root 并且标为黑色
        if ((pp = l.parent = p.parent) == null) 
            (root = l).red = false;
        else if (pp.right == p) // 如果父节点不为空 并且 要右旋的节点是个右孩子
            pp.right = l; // 设置l和父节点的父子关系【之前只是孩子认了爹,爹还没有答应,这一步爹也认了孩子】
        else // 要右旋的节点是个左孩子
            pp.left = l; // 同上
        l.right = p; // 要右旋的节点 作为 他左孩子的右节点
        p.parent = l; // 要右旋的节点的父节点 指向 他的左孩子
    }
    return root;
}

左旋,右旋方法,可以看上一篇红黑树分析,理解原理,当前方法理解与源码关联不大,不做解释。[红黑树原理分析](数据结构红黑树添加、修改原理分析 - 简书 (jianshu.com)
))

  //进行双向链表的调整 ,经过左旋,或者右旋操作后,双向链表的头节点可能发生了变化。需要把根节点放到链表头节点
        static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
            int n;
            if (root != null && tab != null && (n = tab.length) > 0) {
                //拿到tab的数组下标
                int index = (n - 1) & root.hash;
                //保存原始的双向链表
                TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
                // 头节点是否发生了改变
                if (root != first) {
                    
                    Node<K,V> rn; //root的下一个元素,为了后续的双向链表的指向做工作
                    tab[index] = root;   //将树结构覆盖原来的值
                    TreeNode<K,V> rp = root.prev;  // root的前继 为了root断开链表进行操作
                    if ((rn = root.next) != null)   //如果root后继不为空,说明root 节点处于中间 需要对root进行断开链表操作
                        ((TreeNode<K,V>)rn).prev = rp;   //将root的后继指向root的前继
                    if (rp != null)    //rp!=null 说明root有前继  
                        rp.next = rn;   //root前继的后继指向root的后继 这样root就移除了双链表。相当于从链表中删除了root节点
                    if (first != null)   //原来的双向链表不为空 进行头插法插入root
                        first.prev = root;     
                    root.next = first;
                    root.prev = null;    //前继置为空 完成头插法插入,root成为链表头节点
                }
                assert checkInvariants(root);
            }
        }

当前方法:如果当前的root不等于以前放置在数组中的双向链表的根节点的时候,在双向链表中删除root;把root的后继,与root的前驱进行关联,在使用头插法,把root插入的以前first的前面,最后把root放入当前的数组的位置中。

/**
 * 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 HashMap.Node<K,V>[] resize() {
    HashMap.Node<K,V>[] oldTab = table;
    // 记录Map当前的容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 记录Map允许存储的元素数量,即阈值(容量*负载因子)
    int oldThr = threshold;
    // 声明两个变量,用来记录新的容量和阈值
    int newCap, newThr = 0;

    // 若当前容量不为0,表示存储数据的数组已经被初始化过
    if (oldCap > 0) {
        // 判断当前容量是否超过了允许的最大容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 若超过最大容量,表示无法再进行扩容
            // 则更新当前的阈值为int的最大值,并返回旧数组
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 将旧容量*2得到新容量,若新容量未超过最大值,并且旧容量大于默认初始容量(16),
        // 才则将旧阈值*2得到新阈值
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }

    // 若不满足上面的oldCap > 0,表示数组还未初始化,
    // 若当前阈值不为0,就将数组的新容量记录为当前的阈值;
    // 为什么这里的oldThr在未初始化数组的时候就有值呢?
    // 这是因为HashMap有两个带参构造器,可以指定初始容量,
    // 若你调用了这两个可以指定初始容量的构造器,
    // 这两个构造器就会将阈值记录为第一个大于等于你指定容量,且满足2^n的数(可以看看这两个构造器)
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 若上面的条件都不满足,表示你是调用默认构造器创建的HashMap,且还没有初始化table数组
    else {               // zero initial threshold signifies using defaults
        // 则将新容量更新为默认初始容量(10)
        // 阈值即为(容量*负载因子)
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 经过上面的步骤后,newCap一定有值,但是若运行的是上面的第二个分支时,newThr还是0
    // 所以若当前newThr还是0,则计算出它的值(容量*负载因子)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }

    // 将计算出的新阈值更新到成员变量threshold上
    threshold = newThr;

    // 创建一个记录新数组用来存HashMap中的元素
    // 若数组不是第一次初始化,则这里就是创建了一个两倍大小的新数组
    @SuppressWarnings({"rawtypes","unchecked"})
    HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
    // 将新数组的引用赋值给成员变量table
    table = newTab;

    // 开始将原来的数据加入到新数组中
    if (oldTab != null) {
        // 遍历原数组
        for (int j = 0; j < oldCap; ++j) {
            HashMap.Node<K,V> e;
            // 若原数组的j位置有节点存在,才进一步操作
            if ((e = oldTab[j]) != null) {
                // 清除旧数组对节点的引用
                oldTab[j] = null;
                // 若table数组的j位置只有一个节点,则直接将这个节点放入新数组
                // 使用 & 替代 % 计算出余数,即下标
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 若第一个节点是一个数节点,表示原数组这个位置的链表已经被转为了红黑树
                // 则调用红黑树的方法将节点加入到新数组中
                else if (e instanceof HashMap.TreeNode)
                    ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                // 上面两种情况都不满足,表示这个位置是一条不止一个节点的链表
                // 以下操作相对复杂,所以单独拿出来讲解
                else { // preserve order
                    HashMap.Node<K,V> loHead = null, loTail = null;
                    HashMap.Node<K,V> hiHead = null, hiTail = null;
                    HashMap.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;
                    }
                }
            }
        }
    }
    // 将新创建的数组返回
    return newTab;
}

resize()方法总结下来就是,第一部分进行newCap新容量的计算,进行新的newThr扩容阈值的计算;下来对以前数组中的树进行差分,如果为一个值,直接放入新的列表,如果为红黑树,对红黑树进行拆分,进行插入操作,如果为单项列表,则对单项列表进行拆分,因为数组的长度发生了变化,hash&(newCap-1)的位置也会发生相应的变化,所以需要进行差分处理。

关于resize()方法链表拆分问题分析

resize方法中的最后一部分,是将原数组中的一条链表的节点,放入到扩容后的新数组中,而这一部分相对来说比较难理解。首先我们得知道是怎么实现的,然后再来逐句分析代码。首先,我们得知道一个结论,那就是:原数组中一条链表上的所有节点,若将它们加入到扩容后的新数组中,它们最多将会分布在新数组中的两条链表上。


链表拆分图

那这个结论是怎么来的呢?我们先说左边第一个节点,它的hash值是2,转换成二进制就是0010,而容量为2^3 == 8,通过num % 2^n == num & (2^n - 1)这个公式,我们知道2与容量8的余数是2 & (8 - 1) == 0010 & 0111 == 0010。任何数与0111做与运算(&),实际上就是取这个数二进制的最后三位。而扩容之后,容量变成了2^4 == 16,这时候,取模就是与16-1 == 15做与运算了,而15的二进制是1111,我们发现,1111与之前的0111唯一的区别就是第四位也变成了1(以下说的第几位都是从右往左)。而2 & 15 == 0010 & 1111 == 0010,和0010 & 0111 结果是一样的。为什么?因为0010的第四位是0,所以从0111变成1111,并不会对计算结果造成影响,因为0和任何数做与运算,结果都是0。所以扩容后,2这个节点,还是放在数字下标为2的位置。我们在来看看剩下的三个数:

hash值为10,转换成二进制1010,1010的第四位为1,所以 1010 & 0111 != 1010 & 1111

hash值为18,转换成二进制10010,10010的第四位为0,所以 10010 & 0111 == 10010 & 1111
    
hash值为26,转换成二进制11010,11010的第四位为1,所以 11010 & 0111 != 11010 & 1111

所以扩容后,余数是否发生改变,实际上只取决于多出来的那一位而已,那一位只有两种结果:0或者1,所以这些节点的新下标最终也只有两种结果。而多出来的那一位是哪一位呢?8转换成二进制是1000,而从8扩容到16,取余的数从0111变成了1111,多出的这个1刚好在第四位,也就是1000中,唯一一个1所在的位置;16的二进制是10000,扩容成32后,取余的数从1111变成11111,在第五位多出了一个1,正好是10000的1所在的位置。所以我们可以知道,扩容后,节点的下标是否需要发生改变,取决于旧容量的二进制中,1那一位。所以容量为8,扩容后,若节点的二进制hash值的第四位为0,则节点在新数组中的下标不变;若为1,节点的下标改变,而且改变的大小正好是+8,因为多出了最高位的1,例如1010 & 0111 = 0010,而1010 & 1111 = 1010,结果相差1000,也就是旧容量的大小8;所以若下标要发生改变,改变的大小将正好是旧数组的容量。

我们如何判断hash值多出来的那一位是0还是1呢,很简单,只要用hash值与旧容量做与运算,结果不为0表示多出的这一位是1,否则就是0。比如说,容量为8(二进制1000),扩容后多出来的是第四位,于是让hash值与1000做与运算,若hash值的第四位是1,与1000做与运算后结果就是1000,若第四位是0,与1000做与运算后就是0。好,下面我们来看看代码吧:

// 创建两个头尾节点,表示两条链表
// 因为旧链表上的元素放入新数组中,最多将变成两条链表
// 一条下标不变的链表,一条下标+oldCap
HashMap.Node<K,V> loHead = null, loTail = null;
HashMap.Node<K,V> hiHead = null, hiTail = null;
HashMap.Node<K,V> next;

// 循环遍历原链表上的每一个节点
do {
    // 记录当前节点的下一个节点
    next = e.next;
    // 注意:e.hash & oldCap这一步就是前面说的判断多出的这一位是否为1
    // 若与原容量做与运算,结果为0,表示将这个节点放入到新数组中,下标不变
    if ((e.hash & oldCap) == 0) {
        // 若这是不变链表的第一个节点,用loHead记录
        if (loTail == null)
            loHead = e;
        // 否则,将它加入下标不变链表的尾部
        else
            loTail.next = e;
        // 更新尾部指针指向新加入的节点
        loTail = e;
    }
    // 若与原容量做与运算,结果为1,表示将这个节点放入到新数组中,下标将改变
    else {
        // 若这是改变下标链表的第一个节点,用hiHead记录
        if (hiTail == null)
            hiHead = e;
        // 否则,将它加入改变下标链表的尾部
        else
            hiTail.next = e;
        // 更新尾部指针指向新加入的节点
        hiTail = e;
    }
} while ((e = next) != null);

// 所有节点遍历完后,判断下标不变的链表是否有节点在其中
if (loTail != null) {
    // 将这条链表的最后一个节点的next指向null
    loTail.next = null;
    // 同时将其放入新数组的相同位置
    newTab[j] = loHead;
}
// 另一条链表与上同理
if (hiTail != null) {
    hiTail.next = null;
    // 这条链表放入的位置要在原来的基础上加上oldCap
    newTab[j + oldCap] = hiHead;
}

至此hashmap的所有关键代码分析完毕,其余代码大家可自行分析
喜欢的话给个赞呗

相关文章

网友评论

      本文标题:java8中hashmap源码分析,put()方法详细分析

      本文链接:https://www.haomeiwen.com/subject/yvhmzrtx.html