窥探HashMap原密码【下】

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 本篇接着上篇文章继续。

五、HashMap对象hash冲突的优化

​ HashMap并没有使用传统的hash算法而是对他进行了进一步的优化从而避免了hash冲突。

​ 核心的代码:

 public V put(K key, V value) {
   
   
         // 存储数据的使用调用hash方法得到一个hash值
        return putVal(hash(key), key, value, false, true);
 }
//hash方法对hash算法进行了进一步的优化    
static final int hash(Object key) {
   
   
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

​ 从代码中可以看出来他是先得到了一个hash值然后再右移了16位然后再进行了一个按位异或的操作。


即使看看懂了他的算法可能还是很多有都会有一个疑问这样就可以降低hash冲突嘛?当前这样是不可以的但是但是在结合后面要说的高效寻址算法就可以,现在先简单的说一下这个寻址算法,该算法基本只会使用到hash值得低16位进行运算,而低16位最多表示到65535,范围并不大所以出现冲突得概率还是挺大的,但是如果加入了高16位的特征那么该hash值得低16位就多了很多的变化从而避免了hash冲突。

#


六、put方法存储数据

    // onlyIfAbsent 如果为true则表示不修改存在的value
    // evict 如果为false则表示hash表还处于创建模式
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
   
   
        Node<K,V>[] tab;  // 存储数据的Hash表
        Node<K,V> p;  //一个节点数据
        int n, i;
        //如果table为空
        if ((tab = table) == null || (n = tab.length) == 0)
        /*
        执行resize扩容操作,如果hash表为null那么使用默认的容量来进行扩容
        如果已经有了容量则在原来的基础上扩容2倍
        如果容量已经大于了最大容量则扩容为int的最大值
        */
            n = (tab = resize()).length; //hash表的容量
        // 通过高效寻址算法得到一个索引然后获取当前索引的值,
        // 如果值为null则直接再该节点上创建对象
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
   
    //如果获取的索引节点上已经存在了数据则走else
            Node<K,V> e;
            K k;
        /* 
            如果获取到了索引上的节点的hash值等于传递进来的hash
            且索引上节点的key值等于传递过来的key值同时key值不为null
            则直接用新的数据替换老的数据-数据的修改操作
        */
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //如果p已经是一个树形节点数据则走树形结构的添加
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
   
   
                // 走链表的添加如果链表长度大于8则转换为树
                for (int binCount = 0; ; ++binCount) {
   
   
                    //如果p没有下一个节点数据了则直接添加一个节点
                    if ((e = p.next) == null) {
   
   
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                /*
                    如果p还有下一个链表节点则获取下一个节点的hash和传递过来的hash进行比较
                    如果hash相等且key值相当同时key值不为空则修改当前节点上的数据
                */
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 实现数据的修改操作
            if (e != null) {
   
    // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //如果存储的容量大于了threshold(总容量*负载因子)则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

值得注意的是当我们的链表长度到达了8的时候还会检测一下table的长度是否达到了MIN_TREEIFY_CAPACITY ,如果达到了则转换位树如果达不到则进行数组的扩容。

七、高效的寻址算法

​ 在put数据的时候会通过高效的寻址算法来获取数组的下标,具体的高效算法如下:

(n - 1) & hash // n代表的是数组的length

​ 一般情况下数组的长度都不会特别的大,同时之前也给大家讲过低16位全部是1就可以表示65535,所以该高效寻址算法基本上只会用到低16位进行运算,这也是为什么上面我们在讲高效hash算法的时候会把高16位全部设置位0的原因。

​ 同时为什么这个算法是高效的的呢?这其实是一个相对的概念,如果让我们去求一个索引的下标可能很多人会使用取余的方式去实现,那么取余的数学运算相对于位运算来说位运算就要高效很多了。

八、hash冲突了如何解决

  1. 如果是在hash表中冲突了则判断是否是同一个可以如果是则修改数据
  2. 如果是在链表中冲突了则遍历链表看是否有key值相同的如果有则进行数据的修改如果没有则添加一个节点
  3. 如果添加的节点大于8且hash表的总容量小于64则会进行数组的扩容然后重新rehash此时整个数组和链表都会发生变化-因为扩容以后rehash以后原本hash冲突的现在可能不会冲突了
  4. 如果是在树中冲突了则通过变色或者旋转的方式来解决

九、resize以后链表的rehash过程

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) {
   
   
            oldTab[j] = null;
            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 {
   
    // preserve order
                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 & 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;
                }
            }
        }
    }
}

数组扩容以后会重新rehash,然后重新计算一个数组下标的位置来存储数据,那么rehash的过程中他就会打破原来的链表比如原来链表中有四个数据的,rehash以后就可以只有2个数据另外2个数据在另一个索引下标中。

目录
相关文章
|
存储 算法 数据安全/隐私保护
窥探HashMap原密码【上】
本篇文章主要给大家介绍了: 什么是哈希表 哈希冲突 HashMap定义的基本属性 HashMap的构造方法
118 0
窥探HashMap原密码【上】
|
1月前
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
29 2
|
1月前
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
34 2
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
54 0
|
6月前
|
存储 安全 Java
HashMap源码全面解析
HashMap源码全面解析
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
53 5
|
1月前
|
算法 索引
让星星⭐月亮告诉你,HashMap的resize()即扩容方法源码解读(已重新完善,如有不足之处,欢迎指正~)
`HashMap`的`resize()`方法主要用于数组扩容,包括初始化或加倍数组容量。该方法首先计算新的数组容量和扩容阈值,然后创建新数组。接着,旧数组中的数据根据`(e.hash & oldCap)`是否等于0被重新分配到新数组中,分为低位区和高位区两个链表,确保数据迁移时的正确性和高效性。
64 3
|
1月前
|
Java 索引
让星星⭐月亮告诉你,HashMap中红黑树TreeNode的split方法源码解读
本文详细解析了Java中`HashMap`的`TreeNode`类的`split`方法,该方法主要用于在`HashMap`扩容时将红黑树节点从旧数组迁移到新数组,并根据`(e.hash & oldCap)`的结果将节点分为低位和高位两个子树。低位子树如果元素数少于等于6,则进行去树化操作;若多于6且高位子树非空,则进行树化操作,确保数据结构的高效性。文中还介绍了`untreeify`和`replacementNode`方法,分别用于将红黑树节点转换为普通链表节点。
24 2
|
1月前
|
存储 Java
HashMap之链表转红黑树(树化 )-treefyBin方法源码解读(所有涉及到的方法均有详细解读,欢迎指正)
本文详细解析了Java HashMap中链表转红黑树的机制,包括树化条件(链表长度达8且数组长度≥64)及转换流程,确保高效处理大量数据。
73 1
|
1月前
|
存储 缓存 Java
HashMap源码解析
HashMap源码解析