HashMap动态扩容解析

1、Hashmap的数据结构

1.1、先来回顾一下java中两种比较简单的数据结构数组和链表。

(1)、数组的特点:
①、优点:是可以通过指定下标直接访问任何元素,时间复杂度为O(1)。这是因为数组在内存中是连续存储的,可以通过计算偏移量直接访问元素。如果知道元素下标可以通过下标直接获取,如果不知道下标遍历的话效率和链表一样。
②、缺点:插入或删除元素时需要移动其他元素来保持顺序,时间复杂度为O(n)。数组的插入和删除操作相对较慢。
(2)、链表的特点:
①、优点:插入或删除元素时只需修改指针,时间复杂度为O(1)。链表的插入和删除操作相对较快。
②、缺点:只能通过遍历链表来访问元素,时间复杂度为O(n)。链表不是连续存储的,需要通过指针逐个访问元素。

1.2、 HashMap 的数据结构

(1)、HashMap 的数据结构在jdk1.8之前
采用了数组+链表,当key发生哈希冲突的时候使用了链地址法解决的哈希冲突,即发生哈希冲突就往链表里面存储数据,但是哈希冲突多了链表就会很长,查询时需要遍历时间复杂度O(n),影响查询效率。
(2)、HashMap在jdk1.8后采用数组+链表/红黑树来解决hash冲突,红黑树的时间复杂度O(logN)。数组是HashMap的主体,链表主要用来解决哈希冲突。这个数组是Node类型的数组,Node是HashMap的内部类,他包含hash值,key,value,next指向下一个node。
(3)、为什么选择使用红黑树,不用二叉树或平衡二叉树呢
①、二叉树极端情况,当子节点都比父节点大或者小的时候,二叉查找树又会退化成链表,查询复杂度会重新变为O(N)。
②、二叉平衡树(AVL树),他会在每次插入操作时来检查每个节点的左子树和右子树的高度差至多等于1,如果>1,就需要进行左旋或者右旋操作,使其查询复杂度一直维持在O(logN)。虽然能让查询的复杂度降低,但是插入时需要左旋或右旋增加了插入的复杂度。
③、在红黑树中,所有的叶子节点都是黑色的空节点,也就是叶子节点不存数据;任何相邻的节点都不能同时为红色,红色节点是被黑色节点隔开的,每个节点,从该节点到达其可达的叶子节点的所有路径,都包含相同数目的黑色节点。所以红黑树不会像AVL树一样追求绝对的平衡,它的插入最多两次旋转,删除最多三次旋转,在频繁的插入和删除场景中,红黑树的时间复杂度,是优于AVL树的。
在这里插入图片描述
在这里插入图片描述

2、hash算法

hash方法的功能就是根据key来获取在hashmap数组中的下标,输入object类型的key,输出int类型的数组下标。

2.1、jdk1.8取hashcode的方法较1.7有优化,添加了扰动计算。

key.hash ^ (key.hash >> 16)或而不是用key.hash:这是因为增加了扰动计算,使得hash分布的尽可能均匀。因为hashCode是int类型,虽然能映射40亿左右的空间,但是,HashMap的table.length毕竟不可能有那么大,所以为了使hash%table.length之后,分布的尽可能均匀,避免哈希冲突,就需要对实例的hashCode的值进行扰动,就是将hashCode的高16和低16位,进行异或,使得hashCode的值更加分散一点。
在这里插入图片描述

2.2、jdk1.8中hashmap根据hashcode获取数组下标

hashMap定位tableIndex的时候,是通过(table.length - 1) & (key.hashCode ^ (key.hashCode >> 16)),而不是常规的key.hashCode % (table.length),因为&是基于内存的二进制直接运算,比转成十进制的取模快的多。以下运算等价:X % 2^n = X & (2^n – 1)。这也是hashMap每次扩容都要到2^n的原因之一。

3、扩容解析

(1)、HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。临界值(threshold) = 负载因子(loadFactor) * 容量(capacity)。loadFactor默认0.75。如果不设置HashMap容量是默认容量是16,最大容量2^30。
(2)、当数组容量大于64时,hash冲突链表大于8时,会把链表转成红黑树,如果数组容量小于等于64时,hash冲突时链表大于8时,会先扩容数据,每次扩容为2的倍数。
(3)、当创建Hashmap时如果初始数组容量设置为7,其实构造函数会把数组容量设置为8,容量设置为9时,构造函数会设置数组容量为16。但是如果设置为7时,扩容临界值为6=8*0.75,所以当put6个元素时就会扩容,显然不是很合理。这里有个公式,根据公式计算设置数组的容量, (int) ((float) expectedSize / 0.75F + 1.0F),10=7/0.75+1,构造函数会把数组容量设置为16,这就大大的减少了扩容的几率,也可以使Maps.newHashMapWithExpectedSize(7)的写法。
在这里插入图片描述

4、put解析

基于1、2、3的分析,下面来看一下put的源码。

4.1、根据key计算hashcode,hashcode一共32位,使用高16异或低16位,对hashcode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。

例如:2.1中n-1=15,转成32位二进制,前28位全部是0,后四位是1。如果两个key的高16位不同但是低16位相同时,直接使用key的hashcode不进行扰动计算会造成哈希冲突。
在这里插入图片描述

4.2、put过程

①、如果数组长度为0,延迟初始化的逻辑,初始化长度16的数组。
②、使用(n - 1) & hash计算数组下标,如果此下标没有数据,根据hash, key, value创建node赋值到此下标。
③、如果下标中有值,比较key和hash值是否相等,如果都相等,进行替换。
④、基于③中如果key或hash值不相等,如果节点已经树化执行putTreeVal,把数据插入到红黑树。
⑤、基于④中如果是链表,需要判断链表长度是否到8,如果没有使用未插入插入到链表最后,如果链表长度超过8调用treeifyBin,treeifyBin方法中,判断数组长度是否小于64,如果小于则动态扩展数组长度,否则链表转换成红黑树。

 /**
     * Implements Map.put and related methods.
     *
     * @param hash         key 的 hash 值
     * @param key          key 值
     * @param value        value 值
     * @param onlyIfAbsent true:如果某个 key 已经存在那么就不插了;false 存在则替换,没有则新增。这里为 false
     * @param evict        不用管了,我也不认识
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // tab 表示当前 hash 散列表的引用
        Node<K, V>[] tab;
        // 表示具体的散列表中的元素
        Node<K, V> p;
        // n:表示散列表数组的长度
        // i:表示路由寻址的结果
        int n, i;
        // 将 table 赋值发给 tab ;如果 tab == null,说明 table 还没有被初始化。则此时是需要去创建 table 的
        // 为什么这个时候才去创建散列表?因为可能创建了 HashMap 时候可能并没有存放数据,如果在初始化 HashMap 的时候就创建散列表,势必会造成空间的浪费
        // 这里也就是延迟初始化的逻辑,初始化长度16的数组
        if ((tab = table) == null || (n = tab.length) == 0) {
            n = (tab = resize()).length;
        }
        // 如果 p == null,说明寻址到的桶的位置没有元素。那么就将 key-value 封装到 Node 中,并放到寻址到的下标为 i 的位置
        if ((p = tab[i = (n - 1) & hash]) == null) {
            tab[i] = newNode(hash, key, value, null);
        }
        // 到这里说明 该位置已经有数据了,且此时可能是链表结构,也可能是树结构
        else {
            // e 表示找到了一个与当前要插入的key value 一致的元素
            Node<K, V> e;
            // 临时的 key
            K k;
            // p 的值就是上一步 if 中的结果即:此时的 (p = tab[i = (n - 1) & hash]) 不等于 null
            // p 是原来的已经在 i 位置的元素,且新插入的 key 是等于 p中的key
            //说明找到了和当前需要插入的元素相同的元素(其实就是需要替换而已)
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                //将 p 的值赋值给 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) {
                    //如果 p.next == null 说明 p 是最后一个元素,说明,该元素在链表中也没有重复的,那么就需要添加到链表的尾部
                    //jdk1.8使用的是尾插法
                    if ((e = p.next) == null) {
                        //直接将 key-value 封装到 Node 中并且添加到 p的后面
                        p.next = newNode(hash, key, value, null);
                        // 当元素已经是 7了,再来一个就是 8 个了,那么就需要进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) {
                            treeifyBin(tab, hash);
                        }
                        break;
                    }
                    //在链表中找到了某个和当前元素一样的元素,即需要做替换操作了。
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        break;
                    }
                    //将e(即p.next)赋值为e,这就是为了继续遍历链表的下一个元素
                    p = e;
                }
            }
            //如果条件成立,说明找到了需要替换的数据,
            if (e != null) {
                //这里不就是使用新的值赋值为旧的值嘛
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null) {
                    e.value = value;
                }
                //这个方法没用,里面啥也没有
                afterNodeAccess(e);
                //HashMap put 方法的返回值是原来位置的元素值
                return oldValue;
            }
        }
        // 上面说过,对于散列表的 结构修改次数,那么就修改 modCount 的次数
        ++modCount;
        //size 即散列表中的元素的个数,添加后需要自增,如果自增后的值大于扩容的阈值,那么就触发扩容操作
        if (++size > threshold) {
            resize();
        }
        //啥也没干
        afterNodeInsertion(evict);
        //原来位置没有值,那么就返回 null 呗
        return null;
    }

在这里插入图片描述

5、get解析

①、使用(n - 1) & hash获取数组下标,如果数组中此下标有数据并且key和hash值都相等,则获取到目标值。
②、如果下标的key及hash值不相等,需要判断下标中数据是链表还是红黑树,如果是红黑树使用
getTreeNode获取目标值。
③、如果是链表,则遍历链表查询目标值。

final Node<K, V> getNode(int hash, Object key) {
        //当前HashMap的散列表的引用
        Node<K, V>[] tab;
        //first:桶头元素
        //e:用于存放临时元素
        Node<K, V> first, e;
        //n:table 数组的长度
        int n;
        //元素中的 k
        K k;
        // 将 table 赋值为 tab,不等于null 说明有数据,(n = tab.length) > 0 同理说明 table 中有数据
        //同时将 该位置的元素 赋值为 first
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            //定位到了桶的到的位置的元素就是想要获取的 key 对应的,直接返回该元素
            if (first.hash == hash && ((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);
            }
        }
        //遍历不到返回null
        return null;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值