HashMap源码tableSizeFor()方法

深入解析HashMap的tableSizeFor方法,该方法用于确定大于等于给定初始容量的最小2的幂值。文章详细解释了算法步骤,包括无符号右移操作的意义,以及如何确保容量为2的幂次方。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

源码: 

static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

这个方法被调用的地方:

public HashMap(int initialCapacity, float loadFactor) {
        /**省略此处代码**/
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

 由此可以看到,当在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。 
下面分析这个算法: 
首先,为什么要对cap做减1操作。int n = cap - 1; 
这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。如果不懂,要看完后面的几个无符号右移之后再回来看看。 
下面看看这几个无符号右移操作: 
如果n这时为0了(经过了cap-1之后),则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作)。 
这里只讨论n不等于0的情况。 
第一次右移

n |= n >>> 1;

 由于n不等于0,则n的二进制表示中总会有一bit为1,这时考虑最高位的1。通过无符号右移1位,则将最高位的1右移了1位,再做或操作,使得n的二进制表示中与最高位的1紧邻的右边一位也为1,如000011xxxxxx。 
第二次右移

n |= n >>> 2;

注意,这个n已经经过了n |= n >>> 1; 操作。假设此时n为000011xxxxxx ,则n无符号右移两位,会将最高位两个连续的1右移两位,然后再与原来的n做或操作,这样n的二进制表示的高位中会有4个连续的1。如00001111xxxxxx 。 
第三次右移

n |= n >>> 4;

这次把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中会有8个连续的1。如00001111 1111xxxxxx 。 
以此类推 
注意,容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就32个1,但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。 
举一个例子说明下吧。 


è¿éåå¾çæè¿°

这个算法着实牛逼啊!

注意,得到的这个capacity却被赋值给了threshold。

this.threshold = tableSizeFor(initialCapacity);

开始以为这个是个Bug,感觉应该这么写:

this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;

这样才符合threshold的意思(当HashMap的size到达threshold这个阈值时会扩容)。 
但是,请注意,在构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算。

 

转载自   https://blog.youkuaiyun.com/fan2012huan/article/details/51097331

 

 

 

 

<think>我们正在分析HashMap源码。根据引用内容,HashMap是Java中用于键值映射的高性能容器,基于哈希表实现,包含数组和链表(或红黑树)结构。我们将按照以下步骤分析源码: 1. 类声明与继承关系 2. 核心字段(如数组、负载因子等) 3. 构造方法 4. 关键方法(put, get, resize等) 5. 树化与反树化条件 注意:由于源码较长,我们只展示关键部分并解释。 根据引用[1],HashMap的类声明如下: ```java public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable ``` 引用[4]提到,HashMap的基本结构是一个数组和一组链表(或红黑树)。数组的每个桶(bucket)存储链表的头节点或红黑树的根节点。当链表长度超过阈值(默认为8)且数组长度达到64时,链表转换为红黑树;当树节点数小于6时,红黑树退化为链表。 下面我们逐步分析: ### 1. 核心字段 ```java // 默认初始容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16 // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认负载因子0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 树化阈值(链表长度大于等于8时可能树化) static final int TREEIFY_THRESHOLD = 8; // 树退化阈值(节点数小于等于6时退化) static final int UNTREEIFY_THRESHOLD = 6; // 最小树化容量(数组长度至少达到64才树化) static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,长度总是2的幂 transient Node<K,V>[] table; // 键值对集合 transient Set<Map.Entry<K,V>> entrySet; // 元素个数 transient int size; // 修改次数(用于迭代器的快速失败) transient int modCount; // 扩容阈值(容量*负载因子),当size超过threshold时扩容 int threshold; // 负载因子 final float loadFactor; ``` ### 2. 节点结构 链表节点(Node): ```java static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 构造方法等 // ... } ``` 树节点(TreeNode,继承自LinkedHashMap.Entry,而后者继承自Node): ```java 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; // 前驱节点(删除后取消链接) boolean red; // 颜色 // ... } ``` ### 3. 构造方法 HashMap有多个构造方法,最常用的是指定初始容量和负载因子的: ```java public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; // 计算扩容阈值,注意此时数组还没有初始化,在第一次put时初始化 this.threshold = tableSizeFor(initialCapacity); } ``` 其中`tableSizeFor`方法返回大于等于给定容量的最小的2的幂次方数(如给定10,返回16)。 ### 4. put方法 这是HashMap最核心的方法之一: ```java public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } ``` 其中`hash(key)`计算key的哈希值(高16位与低16位异或,减少哈希冲突): ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` `putVal`方法: ```java 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为空,则通过resize()初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 计算桶下标:i = (n-1) & hash if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶为空,直接放入 else { Node<K,V> e; K k; // 判断桶中第一个节点是否与待插入节点相同(hash和key都相同) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果是树节点,则调用树节点的putTreeVal方法 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) // -1 for 1st treeifyBin(tab, hash); break; } // 找到相同key的节点,跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 如果存在相同key,则更新value if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); // 回调(用于LinkedHashMap) return oldValue; } } ++modCount; // 如果size超过阈值,则扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); // 回调 return null; } ``` ### 5. resize方法(扩容) 当数组元素数量超过阈值时,进行扩容(容量翻倍): ```java final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 超过最大容量则不再扩容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 新容量为旧容量的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 新阈值为旧阈值的2倍 } else if (oldThr > 0) // 初始化时指定了初始容量 newCap = oldThr; else { // 无参初始化,使用默认值 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<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 { // 链表节点 // 将链表拆分成两个链表:一个在原位置,一个在原位置+oldCap Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // 判断节点在扩容后属于低位(原位置)还是高位(原位置+oldCap) 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; } ``` ### 6. 树化方法 当链表长度达到8且数组长度达到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) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { // 将Node节点替换为TreeNode节点 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); } } ``` ### 总结 HashMap通过数组+链表/红黑树实现高效的键值存储。在插入元素时,通过哈希函数计算桶位置,解决冲突的方式是链表法,当链表过长时转换为红黑树以提高效率。当元素数量超过阈值(容量×负载因子)时,进行扩容(翻倍),并重新映射所有元素。 §§相关问题§§ 1. HashMap的负载因子为什么是0.75? 2. 为什么HashMap的容量总是2的幂? 3.HashMap中,当链表长度超过8时为什么选择转化为红黑树而不是其他数据结构? 4. HashMap在并发环境下会出现什么问题? 5. HashMap的key为null时存储在什么位置?</think>### HashMap 源码分析 HashMap 是 Java 中基于哈希表的 `Map` 接口实现,用于存储键值对(key-value)。以下是其核心实现原理和源码关键逻辑分析: --- #### **1. 底层数据结构** HashMap 由 **数组 + 链表/红黑树** 组成(JDK 1.8+): - **数组(桶)**:`Node<K,V>[] table`,初始长度 16(2 的幂)。 - **链表**:解决哈希冲突,节点为 `Node` 类(含 `hash, key, value, next` 字段)。 - **红黑树**:当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树(`TreeNode` 类),提高查询效率[^5]。 ```java // Node 类定义(链表节点) static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 指向下一个节点 } ``` --- #### **2. 核心参数** ```java static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量 16 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子 static final int TREEIFY_THRESHOLD = 8; // 链表转红黑树的阈值 static final int MIN_TREEIFY_CAPACITY = 64; // 最小树化容量 transient Node<K,V>[] table; // 存储桶的数组 int threshold; // 扩容阈值 = capacity * loadFactor ``` --- #### **3. 哈希计算与索引定位** 1. **计算哈希值**: 通过 `hash(key)` 将高 16 位与低 16 位异或,减少冲突。 ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 2. **计算桶下标**: `index = (table.length - 1) & hash`(等价于 `hash % length`,但效率更高)。 --- #### **4. put 方法流程** ```java public V put(K key, V value) { 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; // 步骤1:若 table 为空,则初始化(resize()) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 步骤2:计算桶下标 i,若桶为空则直接插入新节点 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 步骤3:若桶的首节点 key 相同,直接覆盖 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 步骤4:若节点是红黑树,调用树插入方法 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 步骤5:遍历链表 else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 插入到链表尾部(尾插法) p.next = newNode(hash, key, value, null); // 链表长度≥8时尝试树化 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } // 发现 key 相同的节点,跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 步骤6:更新已存在的 key 的值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; return oldValue; } } // 步骤7:检查扩容 if (++size > threshold) resize(); return null; } ``` --- #### **5. 扩容机制(resize)** 当元素数量超过 `threshold` 时,容量翻倍(`newCap = oldCap << 1`),并重新分配节点: 1. 创建新数组(长度为原数组的 2 倍)。 2. 遍历旧数组的每个桶: - 若桶内只有一个节点:直接迁移到新位置。 - 若桶内是红黑树:调用 `split()` 方法拆分。 - 若桶内是链表:将链表拆分为 **低位链表**(原下标)和 **高位链表**(原下标 + oldCap),再放入新数组[^5]。 ```java // 扩容后节点新位置的计算逻辑 if ((e.hash & oldCap) == 0) { // 放入低位链表(原下标) } else { // 放入高位链表(原下标 + oldCap) } ``` --- #### **6. 线程安全性** HashMap **非线程安全**,并发场景下可能引发问题: - **死循环**:JDK 1.7 头插法扩容时可能形成环形链表。 - **数据覆盖**:多线程同时 put 时可能导致数据丢失。 解决方案:使用 `ConcurrentHashMap` 或 `Collections.synchronizedMap()`。 --- #### **7. 关键设计思想** 1. **容量为 2 的幂**:使 `(n-1) & hash` 等效于取模运算,且位运算更快。 2. **负载因子 0.75**:平衡时间与空间开销(过小则频繁扩容,过大则冲突增加)。 3. **红黑树优化**:解决链表过长时查询效率低的问题($O(n) \to O(\log n)$)。 --- ### 总结 HashMap 通过哈希函数快速定位桶,结合链表/红黑树解决冲突,在扩容时重新分布节点以保持高效性。其设计在时间与空间效率上达到了较好平衡,是 Java 最常用的数据结构之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值