[Java基础]HashMap
HashMap
数据结构
HashMap是:数组+链表/红黑树(JDK1.8增加了红黑树部分)
数据底层具体存储的是Node<k,v>
这样的存储方式有什么优点呢?
// 默认初始容量(数组默认大小):16,2的整数次方
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
装载因子用来衡量HashMap满的程度,表示当map集合中存储的数据达到当前数组大小的75%则需要进行扩容
// 链表转红黑树边界
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转离链表边界
static final int UNTREEIFY_THRESHOLD = 6;
// 哈希桶数组
transient Node<K,V>[] table;
// 实际存储的元素个数
transient int size;
// 当map里面的数据大于这个threshold就会进行扩容
// 阈值 = table.length * loadFactor
int threshold
1. `DEFAULT_INITIAL_CAPACITY`: 默认初始容量,即哈希表的数组默认大小,被设置为 `1 << 4`,也就是 2 的 4 次方,即 16。这是因为 HashMap 的数组大小通常是 2 的整数次方,这样有助于在计算哈希索引时更高效。
2. `MAXIMUM_CAPACITY`: 最大容量,被设置为 `1 << 30`,即 2 的 30 次方。这是 HashMap 可以容纳的最大元素数量。
3. `DEFAULT_LOAD_FACTOR`: 默认负载因子,被设置为 0.75f。负载因子是一个衡量 HashMap 满的程度的参数,当存储的数据达到数组大小的 75% 时,会触发扩容操作。
4. `TREEIFY_THRESHOLD`: 链表转红黑树的阈值,被设置为 8。当哈希桶中的链表长度达到 8 时,链表会被转换成红黑树,以提高查询效率。
5. `UNTREEIFY_THRESHOLD`: 红黑树转链表的阈值,被设置为 6。当红黑树中的元素减少到 6 时,红黑树会被转换回链表。
6. `table`: 哈希桶数组,用于存储键值对。`transient` 关键字表示该字段不会被默认的序列化机制序列化。
7. `size`: 实际存储的元素个数,即 HashMap 中键值对的数量。
8. `threshold`: 扩容阈值,计算方式为 `table.length * loadFactor`。当实际存储的元素个数大于等于这个阈值时,触发扩容操作。
Node结构
从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是一个Node的数组。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//用来定位数组索引位置
final K key;
V value;
Node<K,V> next;//链表的下一个Node节点
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
HashMap的数据存储
哈希表
HashMap采用哈希表来存储数据。
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构,只要输入待查找的值即key,即可查找到其对应的值。
哈希表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。
哈希函数
哈希表中元素存储地址是由哈希函数确定的,将数据元素的关键字Key作为自变量,通过一定的函数关系(称为哈希函数),计算出的值,即为该元素的存储地址。
表示为:Addr = H(key),如下图所示:
哈希表中哈希函数的设计是相当重要的,这也是建哈希表过程中的关键问题。
核心问题
建立一个哈希表之前需要解决两个主要问题:
1)构造一个合适的哈希函数,均匀性 H(key)的值均匀分布在哈希表中
2)冲突的处理,在哈希表中,不同的关键字值对应到同一个存储位置的现象。哈希表为解决冲突,可以采用地址法和链地址法等来解决问题,Java中HashMap采用了链地址法。
HashMap的哈希函数
1.计算hashcode
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对key进行了hashCode运算,得到一个32位的int值h,然后用h 异或 h>>>16位。
在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)。
这样做的好处是,可以将hashcode高位和低位的值进行混合做异或运算,而且混合后,低位的信息中加入了高位的信息,这样高位的信息被变相的保留了下来。
等于说计算下标时把hash的高16位也参与进来了,掺杂的元素多了,那么生成的hash值的随机性会增大,减少了hash碰撞。
2.计算槽位
hash & (n - 1)
n-1的二进制实际上是00000001111的形式,
与运算的结果是保留hash的后几位,例如,n=16,那么n-1 = 1111, h&1111 就是保留后四位,散列到0-15
为什么槽位数必须使用2^n?
当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
位运算的运算效率高于算术运算,原因是算术运算还是会被转化为位运算。
最终目的还是为了让哈希后的结果更均匀的分布,减少哈希碰撞,提升hashmap的运行效率。
备注:
^异或:不同为1,相同为0
>>>:无符号右移:右边补0
&运算:两位同时为“1”,结果才为“1,否则为0
h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方。
put方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
{
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 当前对象的数组是null 或者数组长度时0时,则需要初始化数组
// 需要这个判断是因为hashmap有延迟初始化的机制,也就是说,new hashmap的时候并没有分配空间,而是等到put第一个元素的时候才创建数组
if ((tab = table) == null || (n = tab.length) == 0)
{
n = (tab = resize()).length;
}
// 使用hash与数组长度减一的值进行异或得到分散的数组下标,预示着按照计算现在的
// key会存放到这个位置上,如果这个位置上没有值,那么直接新建k-v节点存放
// 其中长度n是一个2的幂次数
if ((p = tab[i = (n - 1) & hash]) == null) {
tab[i] = newNode(hash, key, value, null);
}
// 如果走到else这一步,说明key索引到的数组位置上已经存在内容,即出现了碰撞
// 这个时候需要更为复杂处理碰撞的方式来处理,如链表和树
else
{
Node<K,V> e; K k;
//节点key存在,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
{
e = p;
}
// 判断该链为红黑树
else if (p instanceof TreeNode)
{
// 其中this表示当前HashMap, tab为map中的数组
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);
// TREEIFY_THRESHOLD = 8
// 从0开始的,如果到了7则说明满8了,这个时候就需要转
// 重新确定是否是扩容还是转用红黑树了
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;
}
}
// 此时的e是保存的被碰撞的那个节点,即老节点
if (e != null)
{ // existing mapping for key
V oldValue = e.value;
// onlyIfAbsent是方法的调用参数,表示是否替换已存在的值,
// 在默认的put方法中这个值是false,所以这里会用新值替换旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// Callbacks to allow LinkedHashMap post-actions
afterNodeAccess(e);
return oldValue;
}
}
// map变更性操作计数器
// 比如map结构化的变更像内容增减或者rehash,这将直接导致外部map的并发
// 迭代引起fail-fast问题,该值就是比较的基础
++modCount;
// size即map中包括k-v数量的多少
// 超过最大容量 就扩容
if (++size > threshold)
resize();
// Callbacks to allow LinkedHashMap post-actions
afterNodeInsertion(evict);
return null;
}
HashMap的put方法执行过程整体如下:
- 判断键值对数组table[]是否为空或为null,否则执行resize()进行扩容;
- 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
- 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
- 遍历table[i],判断链表长度是否大于等于8,大于等于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
- 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
- 先判断桶数组是不是空,
- 如果是,先扩容
- 如果不是空,
- 判断hash位置上有没有元素
- 如果没有,直接插入
- 如果有,判断该位上的元素是不是和即将插入的元素有相同的key
- 如果key相同,直接覆盖value
- 如果key不同,说明拉了链,或者拉了树
- 如果拉了树,对树进行插入操作
- 如果拉了链,遍历这个链,查找链的末尾,或者链上有相同的key,覆盖value,
- 如果插入后链的长度等于8,转为红黑树。
- 判断hash位置上有没有元素

get方法
根据key查找对应的值
// 根据key查询对应的val
public V get(Object key) {
// 定义一个node节点
Node<K,V> e;
// 根据key获取的节点为null返回这个null,否则获取的节点不为null,返回这个节点对应的值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
根据哈希值和指定的key获取节点
// 根据指定的key和val获取节点
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 如果哈希表不为空并且key对应的桶上不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 判断数组元素是否相等
// 根据索引的位置检查第一个节点
// 注意:总是检查第一个节点
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
// 如果是这个节点,返回这个节点
return first;
// 如果不是第一个节点,判断是否有后续节点
if ((e = first.next) != null) {
// 判断是否是红黑树,是的话调用红⿊树中的getTreeNode方法获取节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 不是红黑树的话,那就是链表结构了,通过循环的方法判断链表中是否存在该key
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 返回这个节点
return e;
} while ((e = e.next) != null);
}
}
// 未获取到节点返回null
return null;
}
根据指定的哈希值和key获取树节点
代码解读复制代码// 参数h为哈希值,参数k为指定的key
final TreeNode<K,V> getTreeNode(int h, Object k) {
// ((parent != null) ? root() : this)获取跟节点
// 从跟节点开始查找指定的key
return ((parent != null) ? root() : this).find(h, k, null);
}
根据哈希值和指定的key查询节点
代码解读复制代码// h:哈希值。 k:给定的key
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// 首次遍历this是桶里面的第一个节点
TreeNode<K,V> p = this;
// 循环遍历这棵红黑树
do {
int ph, dir; K pk;
// pl当前节点p的左孩子,pr当前节点p的右孩子
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 将当前节点的哈希值赋值给ph,判断给定的哈希值是否小于当前节点的哈希值
if ((ph = p.hash) > h)
// 将左孩子赋值给p
p = pl;
// 当前节点的哈希值小于给定的哈希值
else if (ph < h)
// 将右孩子赋值给p
p = pr;
// 判断当前节点的key赋值给pk,并且pk与给定的k相等
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
// 找到之后直接返回
return p;
// 判断左孩子是否null
else if (pl == null)
// 将右孩子赋值给p
p = pr;
// 判断右孩子pr是否为null
else if (pr == null)
// 将左孩子pl赋值给p
p = pl;
// 经过compare计算出dir
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
// 如果计算出的dir小于0,将pl左孩子赋值给p,否则将pr右孩子赋值给p
p = (dir < 0) ? pl : pr;
// 递归查找,查找到结果赋值给q
else if ((q = pr.find(h, k, kc)) != null)
// 返回q
return q;
else
// 将左孩子赋值给p
p = pl;
// 判断p是否为空,不为空接着循环
} while (p != null);
// 没有获取到节点直接返回
return null;
}
get方法的实现步骤
1、通过hash值获取该key映射到的桶
2、桶上的key就是要查找的key,则直接找到并返回
3、桶上的key不是要找的key,则查看后续的节点:
- 如果后续节点是红黑树节点,通过调用红⿊树的方法根据key获取value
- 如果后续节点是链表节点,则通过循环遍历链表根据key获取value
4、查找红黑树,由于之前添加时已经保证这个树是有序的了,因此查找时基本就是折半查找,效率更更高。
5、这⾥和插⼊时一样,如果对比节点的哈希值和要查找的哈希值相等,就会判断key是否相等,相等就直接返回。不相等就从⼦树中递归查找。 - 若为树,则在树中通过key.equals(k)查找,O(logn)
- 若为链表,则在链表中通过key.equals(k)查找,O(n)。
HashMap总结
HashMap底层结构?
基于Map接口的实现,数组+链表的结构, JDK 1.8后加入了红黑树,链表长度>=8(链表序号到达7)变红黑树,<6变链表
两个对象的hashcode相同会发生什么?
Hash冲突,HashMap通过链表或者红黑树来解决hash冲突
HashMap 中 equals() 和 hashCode() 有什么作用?
HashMap 的添加、获取时需要通过 key 的 hashCode() 进行 hash(),然后计算下标 ( n-1 & hash),从而获得要找的桶的位置。
当发生冲突(碰撞)时,利用 key.equals()(注意这里不是object.equals()) 方法去链表或树中去查找对应的节点
HashMap 何时扩容?
put的元素达到容量乘负载因子的时候,默认16*0.75
hash 的实现
hash = key.hashCode() ^ (key.hashCode() >>> 16), hashCode 进行无符号右移 16 位,然后进行按位异或,得到这个键的哈希值,由于哈希表的容量都是 2 的 N 次方,在当前,元素的 hashCode() 在很多时候下低位是相同的,这将导致冲突(碰撞),因此 1.8 以后做了个移位操作:将元素的 hashCode() 和自己右移 16 位后的结果求异或
HashMap线程安全吗?
HashMap读写效率较高,但是因为其是非同步的,即读写等操作都是没有锁保护的,所以在多线程场景下是不安全的,容易出现数据不一致的问题,在单线程场景下非常推荐使用
HashMap 在多线程环境下不是线程安全的。这是因为 HashMap 的实现是基于哈希表的,而哈希表的操作涉及到多个步骤,包括计算哈希码、定位桶位置、插入或检索元素等。在多线程环境下,多个线程同时对 HashMap 进行修改操作可能导致数据不一致或者丢失。
以下是一些可能导致线程不安全的情况:
- 竞态条件(Race Condition): 多个线程同时尝试插入或删除元素时,可能导致竞态条件。两个线程可能同时检测到某个位置为空,然后都尝试插入元素,导致其中一个线程的操作被覆盖。
- 扩容操作: 当 HashMap 需要扩容时,会创建一个新的数组并将旧的元素重新分配到新数组中。在这个过程中,如果有其他线程同时对 HashMap 进行修改,可能会导致元素在扩容过程中丢失或者被重复添加。
为了在多线程环境下保证线程安全,可以使用 ConcurrentHashMap 类,它提供了一些并发安全的操作。ConcurrentHashMap 使用分段锁的机制,将哈希表分成多个段,每个段上都有一个独立的锁,从而降低了锁的粒度,提高了并发性能。这样,不同的线程可以同时修改不同的段,避免了整个数据结构的锁竞争。
总的来说,如果需要在多线程环境中使用哈希表,推荐使用 ConcurrentHashMap 而不是 HashMap,以确保线程安全性。
如何做到让HashMap线程安全?
在Java中,HashMap本身不是线程安全的,但可以通过以下几种方式来实现线程安全的HashMap:
- 使用Collections.synchronizedMap方法:
Map<K, V> synchronizedMap = Collections.synchronizedMap(new HashMap<K, V>());
这将返回一个线程安全的Map,它在每个方法上都使用同步机制来确保线程安全。但请注意,虽然这确保了每个方法的原子性,但在多个操作之间,仍然可能需要额外的同步。
- 使用ConcurrentHashMap:
ConcurrentHashMap是Java提供的线程安全的Map实现。它使用分段锁机制,每个段相当于一个小的HashMap,不同的段之间互不影响,这样可以提高并发性能。
Map<K, V> concurrentMap = new ConcurrentHashMap<K, V>();
- 使用Collections.synchronizedMap包装HashMap的迭代器: 如果你使用Collections.synchronizedMap来创建线程安全的HashMap,当你迭代Map时,仍然需要手动同步。你可以通过在迭代器上使用synchronized块来实现:
Map<K, V> synchronizedMap = Collections.synchronizedMap(new HashMap<K, V>());
Set<K> keySet = synchronizedMap.keySet();
synchronized (keySet) {
Iterator<K> iterator = keySet.iterator();
while (iterator.hasNext()) {
K key = iterator.next();
// 在此处执行操作
}
}
如果需要线程安全的HashMap,推荐使用ConcurrentHashMap,因为它在并发场景下性能更好。根据具体的需求,选择适合的方法来保证线程安全。
ConcurrentHashMap怎么保证线程安全的?
ConcurrentHashMap是Java集合框架中的线程安全的Map实现。它采用了一些策略来确保在多线程环境中的安全性:
- 分段锁(Segmentation): ConcurrentHashMap将整个数据结构分割成多个独立的段(segments),每个段独立地管理一部分数据。每个段都类似于一个小的HashMap,有自己的锁。这样,不同段的数据可以在不同的锁上进行操作,提高了并发度。当一个线程在一个段上进行操作时,其他线程可以同时在其他段上进行操作,减小了竞争范围。
- 精细化的锁策略: 在ConcurrentHashMap中,只有在读写冲突的时候才会使用锁,而且只锁定与冲突相关的段,而不是整个Map。这种细粒度的锁策略减小了锁的争用,提高了并发性能。
- 读操作的无锁支持: ConcurrentHashMap对于读操作提供了无锁支持,允许多个线程同时进行
读取操作,不会阻塞。只有在写操作发生时才需要加锁,确保写操作的原子性和可见性。 - CAS(Compare and Swap)操作: ConcurrentHashMap使用CAS操作来确保对数据的原子更新。CAS是一种无锁算法,它比传统的锁机制更轻量级。通过CAS,ConcurrentHashMap可以在不加锁的情况下完成一些简单的操作。
适应性自动调整: ConcurrentHashMap在运行时会根据负载因子、并发度等参数进行自动调整。这使得它在不同的负载和并发情况下都能够保持高效。
ConcurrentHashMap通过使用分段锁、细粒度的锁策略、无锁的读操作和CAS操作等技术,以及适应性自动调整,来保证在多线程环境中的高并发性能和线程安全。这些特性使得ConcurrentHashMap成为处理高并发情况下Map操作的理想选择。
HashTable
线程安全,但是效率太低了,synchronized修饰方法,只允许一个线程访问
HashSet
基于哈希表实现的无序集合,它使用哈希算法来存储和检索元素。下面是向 HashSet 中加入元素的过程:
-
计算哈希码(Hash Code):
当你向 HashSet 中添加一个元素时,首先会调用该元素的 hashCode() 方法,得到元素的哈希码。
如果元素为 null,则它的哈希码为 0。 -
映射到桶位置(Bucket Position):
哈希码经过一系列的变换和运算,被映射到哈希表中的一个桶位置(bucket position)。
桶位置是一个数组索引,表示存储元素的位置。 -
处理哈希冲突:
哈希表可能存在冲突,即不同元素映射到相同的桶位置。为了解决冲突,HashSet 使用链表或红黑树(在JDK 8之后)来存储相同桶位置上的元素。如果桶位置上已经有一个元素,新元素会被添加到链表或红黑树的末尾。 -
检查元素唯一性:
在添加元素的过程中,HashSet 会通过调用元素的 equals() 方法来检查元素的唯一性。
如果已经存在相同的元素(根据 equals() 判断),新元素不会被加入。
HashSet 的添加过程通过哈希码和哈希表的桶来实现,确保元素的快速存储和检索。因为哈希表的桶位置是通过哈希码计算得到的,所以元素的存储位置在理想情况下是均匀分布的。这有助于在大多数情况下实现 O(1) 时间复杂度的添加、删除和查找操作。
列举HashMap在多线程下可能会出现的问题?
循环链表问题
- JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。
- JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
操作覆盖问题
- 多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
往hashmap存20个元素,会扩容几次?
当插入 20 个元素时,HashMap 的扩容过程如下:
初始容量:16
插入第 1 到第 12 个元素时,不需要扩容。
插入第 13 个元素时,达到负载因子限制,需要扩容。此时,HashMap 的容量从 16 扩容到 32。
扩容后的容量:32
插入第 14 到第 20 个元素时,不需要扩容。
因此,总共会进行一次扩容。
说说hashmap的负载因子
HashMap 负载因子 loadFactor 的默认值是 0.75,当 HashMap 中的元素个数超过了容量的 75% 时,就会进行扩容。
默认负载因子为 0.75,是因为它提供了空间和时间复杂度之间的良好平衡。
负载因子太低会导致大量的空桶浪费空间,负载因子太高会导致大量的碰撞,降低性能。0.75 的负载因子在这两个因素之间取得了良好的平衡。
Hashmap和Hashtable有什么不一样的?Hashmap一般怎么用?
HashMap线程不安全,效率高一点,可以存储null的key和value,null的key只能有一个,null的value可以有多个。默认初始容量为16,每次扩充变为原来2倍。创建时如果给定了初始容量,则扩充为2的幂次方大小。底层数据结构为数组+链表,插入元素后如果链表长度大于阈值(默认为8),先判断数组长度是否小于64,如果小于,则扩充数组,反之将链表转化为红黑树,以减少搜索时间。
HashTable线程安全,效率低一点,其内部方法基本都经过synchronized修饰,不可以有null的key和value。默认初始容量为11,每次扩容变为原来的2n+1。创建时给定了初始容量,会直接用给定的大小。底层数据结构为数组+链表。它基本被淘汰了,要保证线程安全可以用ConcurrentHashMap。
怎么用:HashMap主要用来存储键值对,可以调用put方法向其中加入元素,调用get方法获取某个键对应的值,也可以通过containsKey方法查看某个键是否存在。
为什么hahmap的扩容是两倍扩容
- 两倍扩容的时候计算新的长度比较简单,只需要向左移1位
- hahmap的初始长度是16,每次扩容两倍,这个长度就一直会是2的幂次,这种长度在计算槽位的时候可以直接hashcode & (length - 1),计算起来效率非常高

浙公网安备 33010602011771号