记录HashMap

HashMap的内部结构包括Node数组、加载因子和扩容阈值。数组长度始终为2的幂次方,以优化哈希计算。当元素数量达到特定阈值时,HashMap会进行扩容或将链表转换为红黑树,以减少冲突并保持性能。文章详细阐述了HashMap的putVal方法、resize方法以及元素在扩容时的重映射策略。
摘要由CSDN通过智能技术生成

HashMap的几个属性字段

transient Node<K,V>[] table: Node是HashMap的内部类,用来存储key和value用的。

loadFactor: 加载因子,可以通过构造函数的参数自定义,默认是0.75。

int threshold: 存储所有元素的阈值(不仅仅是数组元素)。如果使用的是HashMap的无参构造方法,那threshold的初始值就是默认值16*3/4=12,也就是元素个数达到12的时候会触发扩容,threshold的值会翻倍。如果使用的是HashMap的有参构造方法,threshold的初始值就会变为数组最大长度,扩容的时候也是翻倍。

HashMap中的一些设定

属性table数组的长度是2的幂次方

HashMap中有个tableSizeFor方法,这个方法是专门用来返回数组长度:

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;
    }

这个方法的作用会返回大于给定参数的最小2次幂的数字。

该算法让最高位的 1 后面的位全变为 1。最后再让结果 n+1,即得到了 2 的整数次幂的值了。
让 cap-1 再赋值给 n 的目的是另找到的目标值大于或等于原值。例如二进制 1000,十进制数值为 8。如果不对它减 1 而直接操作,将得到答案 10000,即 16。显然不是结果。减 1 后二进制为 111,再进行操作则会得到原来的数值 1000,即 8。通过一系列位运算大大提高效率。

数组长度设置为2的幂次方的好处就是:

  • 可以用数组元素的哈希值通过位运算来得到下标

(HashMap 是通过 index=hash&(table.length-1) 这条公式来计算元素在 table
数组中存放的下标,就是把元素的 hash 值和数组长度减 1 的值做一个与运算,即可求出该元素在数组中的下标,这条公式其实等价于 hash%
length,也就是对数组长度求模取余,只不过只有当数组长度为 2 的幂次方时,hash&(length-1) 才等价于 hash%
length,使用位运算可以提高效率。)

  • 增加 hash 值的随机性,减少 hash 冲突(这个不懂)

(如果 length 为 2 的幂次方,则 length-1 转化为二进制必定是 11111…… 的形式,这样的话可以使所有位置都能和元素
hash 值做与运算,如果是如果 length 不是 2 的次幂,比如 length 为 15,则 length-1 为
14,对应的二进制为 1110,在和 hash 做与运算时,最后一位永远都为 0 ,浪费空间。)

数据结构

HashMap中使用的数据结构是数组+链表+红黑树,在putVal方法和getNode方法中都可以到有针对这三种数据结构的if判断。
链表使用的还是Node类,红黑树节点使用的是TreeNode内部类,在putVal方法中有强转为TreeNode的步骤。

                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //  如果链表长度大于等于8,就会将链表转换为树
                        if (binCount >= TREEIFY_THRESHOLD - 1) //  TREEIFY_THRESHOLD   = 8
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
// 如果 table数组的长度小于 64 并不会直接将链表转换为树,而是会扩容。
  if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();

数组长度小于64,数组容量比较小的时候时候键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。

HashMap中的几个方法

putVal

// onlyIfAbsent:   如果为true,key不存在才会插入值,key已经存在不会进行更新,通常为false
evict参数在hashMap中没有用到,在LinkedHashMap中有使用
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //  如果table为空表明没有初始化过,先进行扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果key不存在,直接创建一个node节点 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 是数组元素  直接赋值
            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) {
                // binCount=0的时候,p是数组元素并且有了哈希冲突,接着判断当前p有没有后继节点,没有就将这个节点作为链表节点插入
                    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;
                    }
                    //  如果p节点的后继节点不为空,那就先判断后继节点的key可以要插入的key值是不是相等,
                    // 因为第一次判断是数组元素,所以是用p的后继节点,也就是e节点的key来判断是不是和传入的key相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //  e如果不等于null 那就说明是传入的key值是存在的,那就是更新操作;
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                //  进行节点的赋值
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //  e如果等于null  就说明是插入操作,就需要判断是否需要扩容
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize方法

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //  oldCap大于0也就是已经插入过值了
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //  原先的数组长度和threshold直接翻倍
            //  如果oldCap >= DEFAULT_INITIAL_CAPACITY条件不满足,说明oldCap是通过有参构造函数赋值的,那么threshold就是数组长度,就不会很小
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //  没有元素存在,但是threshold存在,说明调用的是有参构造函数,threshold就是数组长度
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        //  说明调用的是无参构造方法   使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //  溢出归零的情况
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //   创建一个新的Node数组,长度就是是newCap,
            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;
                   //  没有后继节点,说明是数组元素,直接赋值, 对应前面说数组长度是2的幂次方的好处,用e.hash & (newCap - 1) 计算出下标值
                    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;
                        }
                    }
                }
            }
        }
        return newTab;
    }

这里有一个需要注意的点就是在 JDK1.8 HashMap 扩容阶段重新映射元素时不需要像 1.7 版本那样重新去一个个计算元素的 hash 值,而是通过 hash & oldCap 的值来判断,若为 0 则索引位置不变,不为 0 则新索引 = 原索引 + 旧数组长度,为什么呢?具体原因如下:因为我们使用的是 2 次幂的扩展 (指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。因此,我们在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash,只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成 “原索引 + oldCap

上面内容借鉴于:从基础到实践,一文带你看懂 HashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值