Java HashMap 源码浅析 二

一. resize()扩容方法

    final Node<K,V>[] resize() {    
        Node<K,V>[] oldTab = table;	//当前hash桶
        //当前hash桶的大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;	//获取原始HashMap数组的长度。
        int oldThr = threshold;	//容量扩展的临界值
        //初始化新的hash桶的大小和hashMap阈值
        int newCap, newThr = 0;        
        if (oldCap > 0) {	//如果当前hash桶的大小大于0        	
            if (oldCap >= MAXIMUM_CAPACITY) {	//如果当前hash桶的大小到达最大值,不再进行扩容
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果不超过最大值,它将被扩展为原始值的两倍。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                 //如果旧的hash桶大小 >= 16,那么新的hashMap阈值也翻倍     
                newThr = oldThr << 1; // double threshold
        }
        //如果当前hash桶大小为 0,但是阈值不为 0,那么表示初始化的HashMap对对象进行扩容
        else if (oldThr > 0) 
	        //将当前hashMap的阈值赋值给新hash桶的大小,此时阈值和hash桶大小一致
            newCap = oldThr;
        else {     //如果当前hash桶的大小和hashMap阈值都是0,则使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY; //设置新hash桶大小为默认值 16
            //设置新hashMap阈值为 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {	//如果当前hashMap的阈值为 0,则根据当前hash桶的大小和负载因子计算新的阈值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;	//更新阈值
        //根据新hash桶的大小构建新的Node数组
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;	//将新构建的Node数组赋值给table
        //遍历存储桶
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;	 //定义节点e用来指向待转移的节点
                if ((e = oldTab[j]) != null) {//如果当前下标为 j 的桶中没有元素,直接结束
                    oldTab[j] = null;	//释放原始表地址
                     //如果当前桶的中只有一个节点,直接计算出该key在新hash桶中对应的位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e; //这里又一次使用了 & 代替取余算法
                    else if (e instanceof TreeNode)	//如果当前节点存在于树中而不是链表中
                    	//采用红黑树的转移逻辑
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { //如果当前节点存在于链表中而不是树中
                    	//定义低位链表的头尾节点
                        Node<K,V> loHead = null, loTail = null;
                        //定义高位链表的头尾节点,其中高位链表在hash桶中的下标比低位节点大oldCap
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next; //定义临时节点
                        do {
                        	//新表的容量是旧表的两倍。单个链表分为高位链表和低位链表。
                            next = e.next;//使用next指针指向链表下一个节点,防止下一个节点丢失
                            //低位链表,关注的对象是oldCap,而不是oldCap-1 
                            if ((e.hash & oldCap) == 0) {	//利用 & 运算得出key落在低位链表中
                                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);	//循环直到当前hash桶对应下标中没有元素为止
                        if (loTail != null) {//如果低位链表不为空,将低位链表赋值给低位hash桶中
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                         //高位链表放置在新表中,索引=原始索引+ oldCap 
                        if (hiTail != null) {//如果高位链表不为空,将高位链表赋值给高位hash桶中
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

扩容流程概括:

  1. 计算新hash桶的大小和阈值
  2. 根据新hash桶的大小生成新的hash桶数组,如果当前hash桶为空,构造一个长度默认的hash桶
  3. 将新hash桶以及新hash桶的大小以及阈值设置到当前HashMap对象
  4. 对当前hash桶中的元素进行转移
  5. 遍历hash桶
  6. 遍历指定下标hash桶中的待转移节点
  7. 如果指定下标hash桶中待转移节点只有一个,直接计算在新hash桶中的落点并转移到新hash桶中
  8. 如果指定下标hash桶中存储的是树,按照树的结构来转移(暂不做介绍)
  9. 如果指定下标hash桶中存的是链表
  10. 创建低位链表头尾指针和高位链表头尾指针
  11. 将待转移元素按照尾插法插入到低位链表和高位链表中
  12. 将低位hash桶和高位hash桶分别指向低位链表和高位链表
  13. 返回新的hash桶

从resize()的实现中可以看出,如果是多节点链表,则将生成高位链表和低位链表,即(e.hash & oldCap) == 0是低位链表,(e.hash & oldCap) != 0是高低链表。

为什么将列表分为高位和低位?
想象一下,如果所有索引都是使用下标=(hash &(新制表符长度-1))计算出来的,这是因为它们基于下标存储,从而导致了额外的时间(寻址等)或空间(辅助参数,结构,索引冲突时的开销),这也是一种出色的优化技术,可以先保存数据,然后搜索附加项。

二. put(k , v )添加

 public V put(K key, V value) {
	  	//先调用hash(key)方法获得key的hash值,在调用putVal方法插入数据,并返回结果
        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;	 //定义临时变量
        if ((tab = table) == null || (n = tab.length) == 0)	//表未初始化或长度为0,hash桶为空。
            n = (tab = resize()).length;	 //扩容并将扩容后hash桶的大小赋值给n
         //(n - 1) & hash确定元素存储在哪个存储桶中,该存储桶为空以及将新生成的节点放置在存储桶中
         //在这种情况下,该节点放置在数组中
        if ((p = tab[i = (n - 1) & hash]) == null)	//如果key对应hash桶的位置上没有节点
         	//将put进来的key,value生成一个Node节点,并将该节点赋值给hash桶指定下标上
            tab[i] = newNode(hash, key, value, null);
        else {	 //如果key对应hash桶的位置上有节点
            Node<K,V> e; K k;	 //定义临时变量
            //比较值区中第一个元素(数组中的节点)的哈希值是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;  //将头节点赋值给临时变量p,用于下面返回
            else if (p instanceof TreeNode)	//哈希值不相等,即key不相等;它是一个红黑树节点。
	            //调用红黑树的putTreeVal方法将key,value设置到红黑树中
	            //并且如果红黑树中存在该key,将对应的value赋值给e
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {	//对于链表节点
             	//累计便利次数,用于在下面链表长度达到 8 的时候转成红黑树
                for (int binCount = 0; ; ++binCount) {	
                    if ((e = p.next) == null) {	//如果链表中不存在下一个节点
                        p.next = newNode(hash, key, value, null);  //在尾部插入一个新节点
                        //如果便利次数达到 7 次,那么插入新节点后链表长度将达到 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 = p.next,可以遍历列表
                }
            }
            if (e != null) { //如果e != null,表示链表(红黑树)中存在相同的key
                V oldValue = e.value;	//记录旧值,用于返回
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);	 //访问后回调
                return oldValue;	 //返回旧值
            }
        }
        //能走到这一步,在上面if (e != null)判断中 e == null
	    //表示新增了元素而不是替换了元素
	    //modCount记录了HashMap在节点数量上变化的次数,在这里加一
        ++modCount;	
        if (++size > threshold)	 //当实际大小大于阈值时,将扩大容量。
            resize();
        afterNodeInsertion(evict);	//插入后的回调
        return null;	//由于是新增节点,没有覆盖旧节点的值,返回null
    }
    
    final void treeifyBin(Node<K,V>[] tab, int hash) { //将链表转换为红黑树
        int n, index; Node<K,V> e;
        //如果地图的容量小于64(默认值),则会调用resize()扩容,并且不会将其转换为红黑树。
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            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);	//调用TreeNode的树排序方法
        }
    }

扩容流程:

  1. 计算需要put的key的hash值
  2. 判断hash桶是不是空,为空先进行扩容
  3. 判断该key值对应在hash桶上是否存在节点,不存在则直接在hash桶中创建节点
  4. 对比头节点的hash值和key是否和需要查询的key一致,如果一致直接覆盖头节点
  5. 判断头节点在红黑树中还是链表中
  6. 如果在红黑树中,则在红黑树中查找该节点,如果在链表中,则遍历链表查询该节点
  7. 如果在链表(红黑树)中存在节点的hash值和key和需要put的key一致,进行覆盖操作
  8. 如果不存在,创建新的节点,添加到链表(红黑树)中
  9. 如果当前HashMap中元素数量超过阈值,进行扩容
  10. 如果put()方法覆盖了某个节点,则返回这个节点的value,否则返回null

索引计算:
计算索引时,此值必须在[0,length]的左右闭合间隔内。基于此条件,例如,默认表长度为16,用公式(n 1)和哈希代替,结果必须在[0,length]范围内。这里还有一个小技巧。在容量必须为2 ^ n的情况下,H&(长度1)= h%长度。这里使用位运算的原因是,位运算由计算机直接处理,效率高于%运算。

红黑树的转换:
在put方法中,当列表的长度大于(TREEIFY_THRESHOLD-1)时,逻辑将转换为红黑树。实际上,这只是初步判断。在转换后的方法treeifyBin()方法中,将对制表符长度进行第二次检查。

put流程图:
在这里插入图片描述

三. HashMap中使用的哈希算法

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

此哈希首先将key向右移动16位,然后与key进行XOR。由于int只有32位,因此16位的无符号右移等效于将高位的一半移到低位:
在这里插入图片描述
在这里插入图片描述
这样,可以避免仅由低级数据计算散列引起的冲突。计算结果由高位和低位数据的组合确定,可以避免哈希值的不均匀分布。而且,位操作更有效。

这涉及put方法中的另一操作。

tab[i = (n - 1) & hash]

ab是两个表,n是映射集的大小,hash是上述方法的返回值。由于通常不指定map集合的大小或在初始化时创建大型map对象,因此基于容量大小和键值的哈希算法只会在开始时计算低位。尽管开始时容量的二进制高位全为0,但是key的二进制高位通常很有价值,因此在哈希方法中首先使用key.hashCode的右移与其自身相差16位,这使较高的位置参与了hash,并在更大程度上降低了冲突率。

这里将不再展开,为避免篇幅过长,将hashmap的remove(),get()等常用方法,以及常见问题和1.7与1.8的改动写到下面这篇↓

链接: HashMap源码 保姆式教程 三

参考链接:
链接: HashMap源码分析,基于1.8对比1.7
链接: JDK1.8 HashMap源代码分析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

香辣奥利奥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值