HashMap (jdk1.8)resize()方法详细注释

本文详细解析HashMap的初始化过程,包括容量计算、扩容条件判断,以及数据迁移策略,特别关注数组长度调整和红黑树节点处理。

   //hashMap初始化兼扩容
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; //创建node数组oldTab,并将hashMap的成员数组table 赋值给oldTab;
        int oldCap = (oldTab == null) ? 0 : oldTab.length; //获取oldTab数组的长度,如果直接int oldCap = oldTab.length,当table、oldTab为空是会报空指针
        int oldThr = threshold;     //oldTab数组的扩容临界点;
        int newCap, newThr = 0;     //扩容后新数组的长度和扩容临界点;
        if (oldCap > 0) {           // oldCap > 0 ,说明table、oldTab中有数据
          /*
            如果oldTab长度大于2~30,查过map能存储的最大值,不能再扩容了,
            把扩容临界点的值设置位Integer所能表示的最大值:2~31-1=0x7fffffff
             @Native public static final int   MAX_VALUE = 0x7fffffff;
          * */
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            /*
            * oldCap <2~30,把oldCap右移1位,即oldCap*2 = 16*2 = 32,扩容后数组的长度必须在[16,2~30)区间;
            * */
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold,新的扩容临界点也要改变,等于oldThr*2=12*2 =24
        }
        /*
        * oldCap = 0,说明table、oldTab为空,刚new出来一个hashMap,还没put数据,oldThr > 0,
        * */
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr; //newCap=12,不是2的n次幂?这是什么场景?没想明白
        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;   //把新的扩容临界点赋值给成员变量threshold
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new HashMap.Node[newCap];  //以newCap=32创建新node数组
        table = newTab;   //把newTab赋值给成员变量table;
        if (oldTab != null) {    //oldTab != null 说明有data,需要把这人data移动到newTab中;
            for (int j = 0; j < oldCap; ++j) {   //循环遍历oldTab中的data
                Node<K,V> e;                     //定义一个node,临时存放oldTab中的元素
                if ((e = oldTab[j]) != null) {   //oldTab当前位置有元素
                    oldTab[j] = null;            //清空oldTab当前位置的元素
                    if (e.next == null)          //oldTab当前元素的下一个节点为空,说明当前元素存储在数组中,不是存储在链表上的
                        /*file:/D:/workSpaces/workspaceIdea/shejimoshi/mvnw
                        * 把当前需要移动位置的元素oldTable[j]赋值给e
                        * 如果e.next的下一个节点为空,说明oldTable[j]只是数组中的一个元素,没有链表节点
                        * 下面将计算当前元素需要挪动到newTable中的位置:
                        * 通过debug看到e.hash的值是10进制,转换为二进制,看最后5位
                        * newCap - 1=32-1=31,转换为二进制:11111;
                        * e.hash & 11111 ,不影响e.hash的原有后5为的值,通过计算出当前需要挪动位置的元素在newTable中的位置;

                        * */
                        newTab[e.hash & (newCap - 1)] = e; //通过e.hash & (newCap - 1)计算出当前元素在newTab中的新的下标,把该位置存放oldTab[j]
                    else if (e instanceof HashMap.TreeNode) //处理红黑树的情况
                        ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                   /*
                   * e.next != null,说明元素是链表上的节点,需要把链表上的各个节点也移动到newTable中
                   * */
                    else { // preserve order   如果当前元素不是存储在数组中,不是红黑树,而是存储在链表节点中的
                        /*
                        * 定义4个node,临时存放链表节点中的元素
                        * 定义循环变量next
                        * */
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;   //把当前节点e指向的下一步节点赋值给循环变量next
                            /*
                            * (e.hash & oldCap) == 0,来判断当前节点元素的下标在newTable中是否需要改变?oldCap=16,对应二进制
                            * :10000,后4为全部为0,e.hash&0000,计算结果,e.hash 后4为也必定是0,0000能存储0~15个元素,
                            * 说明,当前这个节点在newTable中的0~15位能存储,下标就用原有的下标j.
                            * 如果(e.hash & oldCap) != 0,说明0~15位存储不了这个元素,oldCap的第五个元素(从右往左)是1,e.hash
                            * 的对应位置也必定是1,这样才能出现e.hash & oldCap) != 0,必定e.hash & oldCap =16,此时需要将该
                            * 元素暂时存放到hiHead中,该元素在newTable中的下标位置[j+16]
                             *链表上有3个以上元素时 loTail和loTail.Next都会被覆盖
                            * */
                            if ((e.hash & oldCap) == 0) {  //当前元素的下标在0~15之间,可以直接用原有的下标;
                                if (loTail == null)   //第一次走这个分支,loTail为空
                                    loHead = e;       //把当前节点赋值给loHead
                                else
                                    loTail.next = e;
                                loTail = e;          //把当前节点也赋值给loTail
                            }
                            else {//当前元素的下标在16~31之间,需要重新计算下标;,链表上有3个以上元素时,必定有一个元素要走hiHead分支
                                if (hiTail == null) //第一次走这个分支,hiTail为空
                                    hiHead = e;  //把当前节点赋值给hiHead
                                else
                                    hiTail.next = e;
                                hiTail = e;   //把当前节点也赋值给hiTail
                            }
                        } while ((e = next) != null);   //next赋值给e,现在e指向下一个节点
                        if (loTail != null) {     //loTail里面存储data
                            loTail.next = null;   //清空loTail,还有loHead存储的和loTail一样
                            newTab[j] = loHead;   //把loHead放到newTab中
                        }
                        if (hiTail != null) { //hiTail里面存储data
                            hiTail.next = null;//清空hiTail,还有hiHead存储的和hiTail一样
                            newTab[j + oldCap] = hiHead; //把hiHead放到newTab中
                        }
                    }
                }
            }
        }
        return newTab;   //返回初始化或扩容之后的新数组newTab;
    }

<think>我们正在讨论的是HashMap的源码解析。用户之前已经询问了ConcurrentHashMap,现在转向HashMap。根据引用内容,用户希望获取HashMap源码的详细解析。我们需要结合引用内容,覆盖HashMap的主要方面:结构、初始化、增删改查、扩容、树化等。 注意:引用中提到了JDK1.7和1.8的不同,因为JDK1.8HashMap引入了红黑树,所以我们将主要解析JDK1.8HashMap。 结构: HashMapJDK1.8中由数组+链表+红黑树组成。当链表长度超过8(且数组长度>=64)时,链表转换为红黑树;当树节点少于6时,红黑树退化为链表。 初始化: 有多个构造方法,可以指定初始容量和负载因子。如果不指定,默认初始容量为16,负载因子为0.75。构造方法中并没有直接创建数组,而是在第一次put时通过resize()方法创建。 主要方法: put: 添加键值对 get: 根据键获取值 remove: 删除键值对 resize: 扩容 treeifyBin: 将链表转换为红黑树 由于用户要求详细解析,我们将按以下步骤进行: 1. 类结构:重要的静态内部类(Node, TreeNode)和字段(table, threshold, loadFactor等) 2. 初始化过程:构造方法如何计算初始容量(tableSizeFor方法) 3. 插入过程(putVal方法详细步骤,包括哈希计算、处理碰撞、树化判断、扩容触发等 4. 扩容机制(resize方法):如何重新分配元素 5. 树化过程(treeifyBin方法) 6. 查找(get方法)和删除(remove方法)过程 由于源码较长,我们将摘取关键部分进行解释。 注意引用[1][2][3][4][5]中的内容,我们会适当引用并标注。 根据引用[1]:我们可以从类注释开始,掌握HashMap大致框架。 引用[4]提到了构造方法中指定初始容量和负载因子,以及tableSizeFor方法。 引用[5]对比了JDK1.7和1.8的结构变化(1.7是数组+链表,1.8是数组+链表+红黑树)。 现在开始解析: </think>### HashMap 源码深度解析(JDK 1.8) #### 1. **核心结构** HashMap 由 **数组 + 链表 + 红黑树** 组成(JDK 1.8+): - **数组**:`Node<K,V>[] table`,初始长度 16,存放链表头节点 - **链表**:解决哈希冲突,节点为 `Node` 类(含 `key`, `value`, `next` 指针) - **红黑树**:当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为 `TreeNode` ```java // 节点定义 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 链表指针 } // 树节点定义(继承LinkedHashMap.Entry) static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // 额外prev指针用于快速拆树 boolean red; } ``` --- #### 2. **初始化过程** ##### 2.1 构造方法 ```java // 关键参数 int threshold; // 扩容阈值 = capacity * loadFactor final float loadFactor; // 负载因子(默认0.75) // 常用构造器 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity"); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor"); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); // 计算实际容量 } ``` ##### 2.2 容量计算(`tableSizeFor`) 保证数组长度总是 **2 的幂**(便于位运算定位下标): ```java 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; } ``` > **示例**:输入 `initialCapacity=10` → 输出 `16`(大于 10 的最小 2 的幂)[^4]。 --- #### 3. **插入数据(`putVal`)** 流程如下: 1. 计算哈希值:`hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)` 2. 定位数组下标:`i = (n - 1) & hash` 3. 处理哈希冲突: - 桶为空 → 直接插入新节点 - 桶为链表 → 遍历链表插入(或更新值) - 桶为红黑树 → 调用 `TreeNode.putTreeVal()` 4. 触发扩容或树化 ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 首次插入初始化数组 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // CASE1:桶为空 else { // CASE2/3:桶非空(链表或树) 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) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // 达到树化阈值 treeifyBin(tab, hash); break; } if (e.hash == hash && (...)) break; // 找到相同key p = e; } } if (e != null) { // 更新已存在key的值 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; return oldValue; } } if (++size > threshold) resize(); // 触发扩容 return null; } ``` --- #### 4. **扩容机制(`resize`)** 当元素数量 ≥ `threshold` 时触发: 1. 新容量 = 旧容量 × 2 2. 迁移数据(链表拆分为高低位链表): ```java // 节点迁移逻辑(链表拆分) Node<K,V> loHead = null, loTail = null; // 低位链表 Node<K,V> hiHead = null, hiTail = null; // 高位链表 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); ``` --- #### 5. **树化过程(`treeifyBin`)** 链表 → 红黑树的转换条件: 1. 链表长度 ≥ 8 2. 数组长度 ≥ 64(否则优先扩容) ```java final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 64 resize(); // 不满足条件时扩容 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; // 构建TreeNode链表 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); // 调用红黑树平衡方法 } } ``` --- #### 6. **查找与遍历** ##### 6.1 查找(`get`) ```java public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { 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); } } return null; } ``` ##### 6.2 遍历(`keySet`) ```java // 遍历示例(推荐使用EntrySet迭代器) HashMap<String, String> map = new HashMap<>(); map.put("name", "Alice"); map.put("age", "30"); for (String key : map.keySet()) { String val = map.get(key); System.out.println(key + " : " + val); // 输出:name : Alice, age : 30 } ``` > **注意**:直接遍历 `keySet` 会触发多次哈希计算,建议用 `entrySet()`[^3]。 --- ### 设计要点总结 | **特性** | **JDK 1.7** | **JDK 1.8** | |------------------|--------------------------|----------------------------------| | 数据结构 | 数组+链表 | 数组+链表+红黑树 | | 哈希冲突解决 | 头插法 | 尾插法(避免死循环) | | 节点定位 | `indexFor()` | `(n-1) & hash`(位运算优化) | | 扩容后节点迁移 | 全部重新哈希 | 高低位链表拆分(性能提升 50%) | | 树化条件 | 不支持 | 链表≥8 且 数组长度≥64 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值