07讲深入浅出HashMap的设计与优化 - 图107讲深⼊浅出HashMap的设计与优化

你好,我是刘超。

07讲深入浅出HashMap的设计与优化 - 图2在上⼀讲中我提到过Collection接⼝,那么在Java容器类中,除了这个接⼝之外,还定义了⼀个很重要的Map接⼝,主要⽤来存储键值对数据。

HashMap作为我们⽇常使⽤最频繁的容器之⼀,相信你⼀定不陌⽣了。今天我们就从HashMap的底层实现讲起,深度了解下它的设计与优化。

常⽤的数据结构

我在05讲分享List集合类的时候,讲过ArrayList是基于数组的数据结构实现的,LinkedList是基于链表的数据结构实现的,⽽我今天要讲的HashMap是基于哈希表的数据结构实现的。我们不妨⼀起来温习下常⽤的数据结构,这样也有助于你更好地理解后⾯地内容。

数组:采⽤⼀段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1),但在数组中间以及头部插⼊数据时, 需要复制移动后⾯的元素。

链表:⼀种在物理存储单元上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表由⼀系列结点(链表中每⼀个元素)组成,结点可以在运⾏时动态⽣成。每个结点都包含“存储数据单元的数据域”和“存储下⼀个结点地址的指针域”这两个部分。

由于链表不⽤必须按顺序存储,所以链表在插⼊的时候可以达到O(1)的复杂度,但查找⼀个结点或者访问特定编号的结点需要
O(n)的时间。

哈希表:根据关键码值(Key value)直接进⾏访问的数据结构。通过把关键码值映射到表中⼀个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,存放记录的数组就叫做哈希表。

:由n(n≥1)个有限结点组成的⼀个具有层次关系的集合,就像是⼀棵倒挂的树。

HashMap的实现结构

了解完数据结构后,我们再来看下HashMap的实现结构。作为最常⽤的Map类,它是基于哈希表实现的,继承了AbstractMap 并且实现了Map接⼝。

哈希表将键的Hash值映射到内存地址,即根据键获取对应的值,并将其存储到内存地址。也就是说HashMap是根据键的Hash 值来决定对应值的存储位置。通过这种索引⽅式,HashMap获取数据的速度会⾮常快。

例如,存储键值对(x,“aa”)时,哈希表会通过哈希函数f(x)得到”aa”的实现存储位置。

但也会有新的问题。如果再来⼀个(y,“bb”),哈希函数f(y)的哈希值跟之前f(x)是⼀样的,这样两个对象的存储地址就冲突了, 这种现象就被称为哈希冲突。那么哈希表是怎么解决的呢?⽅式有很多,⽐如,开放定址法、再哈希函数法和链地址法。

开放定址法很简单,当发⽣哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置后⾯的空位置上去。这种⽅法存在着很多缺点,例如,查找、扩容等,所以我不建议你作为解决哈希冲突的⾸选。

再哈希法顾名思义就是在同义词产⽣地址冲突时再计算另⼀个哈希函数地址,直到冲突不再发⽣,这种⽅法不易产⽣“聚集”, 但却增加了计算时间。如果我们不考虑添加元素的时间成本,且对查询元素的要求极⾼,就可以考虑使⽤这种算法设计。

HashMap则是综合考虑了所有因素,采⽤链地址法解决哈希冲突问题。这种⽅法是采⽤了数组(哈希表)+ 链表的数据结构,当发⽣哈希冲突时,就⽤⼀个链表结构存储相同Hash值的数据。

HashMap的重要属性

从HashMap的源码中,我们可以发现,HashMap是由⼀个Node数组构成,每个Node包含了⼀个key-value键值对。

transient Node[] table;
Node类作为HashMap中的⼀个内部类,除了key、value两个属性外,还定义了⼀个next指针。当有哈希冲突时,HashMap会
⽤之前数组当中相同哈希值对应存储的Node对象,通过指针指向新增的相同哈希值的Node对象的引⽤。

static class Node implements Map.Entry { final int hash;
final K key; V value;
Node next;

Node(int hash, K key, V value, Node next) { this.hash = hash;
this.key = key; this.value = value; this.next = next;
}
}
HashMap还有两个重要的属性:加载因⼦(loadFactor)和边界值(threshold)。在初始化HashMap时,就会涉及到这两个

关键初始化参数。

int threshold;

final float loadFactor;
LoadFactor属性是⽤来间接设置Entry数组(哈希表)的内存空间⼤⼩,在初始HashMap不设置参数的情况下,默认
LoadFactor值为0.75。为什么是0.75这个值呢?

这是因为对于使⽤链表法的哈希表来说,查找⼀个元素的平均时间是O(1+n),这⾥的n指的是遍历链表的⻓度,因此加载因⼦越⼤,对空间的利⽤就越充分,这就意味着链表的⻓度越⻓,查找效率也就越低。如果设置的加载因⼦太⼩,那么哈希表的数据将过于稀疏,对空间造成严重浪费。

那有没有什么办法来解决这个因链表过⻓⽽导致的查询时间复杂度⾼的问题呢?你可以先想想,我将在后⾯的内容中讲到。

Entry数组的Threshold是通过初始容量和LoadFactor计算所得,在初始HashMap不设置参数的情况下,默认边界值为12。如果我们在初始化时,设置的初始化容量较⼩,HashMap中Node的数量超过边界值,HashMap就会调⽤resize()⽅法重新分配
table数组。这将会导致HashMap的数组复制,迁移到另⼀块内存中去,从⽽影响HashMap的效率。

HashMap添加元素优化

初始化完成后,HashMap就可以使⽤put()⽅法添加键值对了。从下⾯源码可以看出,当程序将⼀个key-value对添加到
HashMap中,程序⾸先会根据该key的hashCode()返回值,再通过hash()⽅法计算出hash值,再通过putVal⽅法中的(n - 1) &
hash决定该Node的存储位置。

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) { int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;
//通过putVal⽅法中的(n - 1) & hash决定该Node的存储位置
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);

如果你不太清楚hash()以及(n-1)&hash的算法,就请你看下⾯的详述。

我们先来了解下hash()⽅法中的算法。如果我们没有使⽤hash()⽅法计算hashCode,⽽是直接使⽤对象的hashCode值,会出

现什么问题呢?

假设要添加两个对象a和b,如果数组⻓度是16,这时对象a和b通过公式(n - 1) & hash运算,也就是(16-1)&a.hashCode和(16-1)&b.hashCode,15的⼆进制为0000000000000000000000000001111,假设对象 A 的 hashCode 为
1000010001110001000001111000000,对象 B 的 hashCode 为 0111011100111000101000010100000,你会发现上述与运
算结果都是0。这样的哈希结果就太让⼈失望了,很明显不是⼀个好的哈希算法。

但如果我们将 hashCode 值右移 16 位(h >>> 16代表⽆符号右移16位),也就是取 int 类型的⼀半,刚好可以将该⼆进制数对半切开,并且使⽤位异或运算(如果两个数对应的位置相反,则结果为1,反之为0),这样的话,就能避免上⾯的情况发
⽣。这就是hash()⽅法的具体实现⽅式。简⽽⾔之,就是尽量打乱hashCode真正参与运算的低16位。

我再来解释下(n - 1) & hash是怎么设计的,这⾥的n代表哈希表的⻓度,哈希表习惯将⻓度设置为2的n次⽅,这样恰好可以保证(n - 1) & hash的计算得到的索引值总是位于table数组的索引之内。例如:hash=15,n=16时,结果为15;hash=17,n=16 时,结果为1。

在获得Node的存储位置后,如果判断Node不在哈希表中,就新增⼀个Node,并添加到哈希表中,整个流程我将⽤⼀张图来说明:

07讲深入浅出HashMap的设计与优化 - 图3

从图中我们可以看出:在JDK1.8中,HashMap引⼊了红⿊树数据结构来提升链表的查询效率。

这是因为链表的⻓度超过8后,红⿊树的查询效率要⽐链表⾼,所以当链表超过8时,HashMap就会将链表转换为红⿊树,这
⾥值得注意的⼀点是,这时的新增由于存在左旋、右旋效率会降低。讲到这⾥,我前⾯我提到的“因链表过⻓⽽导致的查询时间复杂度⾼”的问题,也就迎刃⽽解了。

以下就是put的实现源码:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//1、判断当table为null或者tab的⻓度为0时,即table尚未初始化,此时通过resize()⽅法得到初始化的table n = (tab = resize()).length;

if ((p = tab[i = (n - 1) & hash]) == null)
//1.1、此处通过(n - 1) & hash 计算出的值作为tab的下标i,并另p表示tab[i],也就是该链表第⼀个节点的位置。并判断p是否为null tab[i] = newNode(hash, key, value, null);
//1.1.1、当p为null时,表明tab[i]上没有任何元素,那么接下来就new第⼀个Node节点,调⽤newNode⽅法返回新节点赋值给tab[i]
else {
//2.1下⾯进⼊p不为null的情况,有三种情况:p为链表节点;p为红⿊树节点;p是链表节点但⻓度为临界⻓度TREEIFY_THRESHOLD,再插⼊任何Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//2.1.1HashMap中判断key相同的条件是key的hash相同,并且符合equals⽅法。这⾥判断了p.key是否和插⼊的key相等,如果相等,则将p的引

e = p;
else if (p instanceof TreeNode)
//2.1.2现在开始了第⼀种情况,p是红⿊树节点,那么肯定插⼊后仍然是红⿊树节点,所以我们直接强制转型p后调⽤TreeNode.putTreeVal⽅法, e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//2.1.3接下⾥就是p为链表节点的情形,也就是上述说的另外两类情况:插⼊后还是链表/插⼊后转红⿊树。另外,上⾏转型代码也说明了TreeNode for (int binCount = 0; ; ++binCount) {
//我们需要⼀个计数器来计算当前链表的元素个数,并遍历链表,binCount就是这个计数器

if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1)
// 插⼊成功后,要判断是否需要转换为红⿊树,因为插⼊后链表⻓度加1,⽽binCount并不包含新节点,所以判断时要将临界阈值减1
treeifyBin(tab, hash);
//当新⻓度满⾜转换条件时,调⽤treeifyBin⽅法,将该链表转换为红⿊树
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) break;
p = e;
}
}
if (e != null) { // existing mapping for key V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) e.value = value;
afterNodeAccess(e); return oldValue;
}
}
++modCount;
if (++size > threshold)

resize(); afterNodeInsertion(evict); return null;
}
07讲深入浅出HashMap的设计与优化 - 图4

HashMap获取元素优化

当HashMap中只存在数组,⽽数组中没有Node链表时,是HashMap查询数据性能最好的时候。⼀旦发⽣⼤量的哈希冲突,就会产⽣Node链表,这个时候每次查询元素都可能遍历Node链表,从⽽降低查询数据的性能。

特别是在链表⻓度过⻓的情况下,性能将明显降低,红⿊树的使⽤很好地解决了这个问题,使得查询的平均复杂度降低到了
O(log(n)),链表越⻓,使⽤⿊红树替换后的查询效率提升就越明显。

我们在编码中也可以优化HashMap的性能,例如,重写key值的hashCode()⽅法,降低哈希冲突,从⽽减少链表的产⽣,⾼效利⽤哈希表,达到提⾼性能的效果。

HashMap扩容优化

HashMap也是数组类型的数据结构,所以⼀样存在扩容的情况。

在JDK1.7 中,HashMap整个扩容过程就是分别取出数组元素,⼀般该元素是最后⼀个放⼊链表中的元素,然后遍历以该元素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标,然后进⾏交换。这样的扩容⽅式会将原来哈希冲突的单向链表尾部变成扩容后单向链表的头部。

⽽在 JDK 1.8 中,HashMap对扩容操作做了优化。由于扩容数组的⻓度是 2 倍关系,所以对于假设初始 tableSize = 4 要扩容到 8 来说就是 0100 到 1000 的变化(左移⼀位就是 2 倍),在扩容中只⽤判断原来的 hash 值和左移动的⼀位(newtable 的值)按位与操作是 0 或 1 就⾏,0 的话索引不变,1 的话索引变成原索引加上扩容前数组。

之所以能通过这种“与运算“来重新分配索引,是因为 hash 值本来就是随机的,⽽hash 按位与上 newTable 得到的 0(扩容前的索引位置)和 1(扩容前索引位置加上扩容前数组⻓度的数值索引处)就是随机的,所以扩容的过程就能把之前哈希冲突的元素再随机分布到不同的索引中去。

总结

HashMap通过哈希表数据结构的形式来存储键值对,这种设计的好处就是查询键值对的效率⾼。

我们在使⽤HashMap时,可以结合⾃⼰的场景来设置初始容量和加载因⼦两个参数。当查询操作较为频繁时,我们可以适当地减少加载因⼦;如果对内存利⽤率要求⽐较⾼,我可以适当的增加加载因⼦。

我们还可以在预知存储数据量的情况下,提前设置初始容量(初始容量=预知数据量/加载因⼦)。这样做的好处是可以减少
resize()操作,提⾼HashMap的效率。

HashMap还使⽤了数组+链表这两种数据结构相结合的⽅式实现了链地址法,当有哈希值冲突时,就可以将冲突的键值对链成
⼀个链表。

但这种⽅式⼜存在⼀个性能问题,如果链表过⻓,查询数据的时间复杂度就会增加。HashMap就在Java8中使⽤了红⿊树来解决链表过⻓导致的查询性能下降问题。以下是HashMap的数据结构图:

07讲深入浅出HashMap的设计与优化 - 图5

思考题

实际应⽤中,我们设置初始容量,⼀般得是2的整数次幂。你知道原因吗?

期待在留⾔区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他⼀起学习。

07讲深入浅出HashMap的设计与优化 - 图6

  1. 精选留⾔ <br />![](https://cdn.nlark.com/yuque/0/2022/png/1852637/1646316010012-1f184baa-8991-407c-b086-99d7c599ba05.png#)陆离<br />2的幂次⽅减1后每⼀位都是1,让数组每⼀个位置都能添加到元素。<br />例如⼗进制8,对应⼆进制1000,减1是0111,这样在&hash值使数组每个位置都是可以添加到元素的,如果有⼀个位置为0, 那么⽆论hash值是多少那⼀位总是0,例如0101,&hash后第⼆位总是0,也就是说数组中下标为2的位置总是空的。<br />如果初始化⼤⼩设置的不是2的幂次⽅,hashmap也会调整到⽐初始化值⼤且最近的⼀个2的幂作为capacity。

2019-06-04 05:48
作者回复
回答正确,就是减少哈希冲突,均匀分布元素。
2019-06-04 09:08

07讲深入浅出HashMap的设计与优化 - 图7AiSmart4J
1)通过将 Key 的 hash 值与 length-1 进⾏ & 运算,实现了当前 Key 的定位,2 的幂次⽅可以减少冲突(碰撞)的次数,提⾼
HashMap 查询效率;
2)如果 length 为 2 的次幂,则 length-1 转化为⼆进制必定是 11111…… 的形式,在于 h 的⼆进制与操作效率会⾮常的快,⽽且空间不浪费;如果 length 不是 2 的次幂,⽐如 length 为 15,则 length-1 为 14,对应的⼆进制为 1110,在于 h 与操作,最后⼀位都为 0,⽽ 0001,0011,0101,1001,1011,0111,1101 这⼏个位置永远都不能存放元素了,空间浪费相当⼤,更糟的是这种情况中,数组可以使⽤的位置⽐数组⻓度⼩了很多,这意味着进⼀步增加了碰撞的⼏率,减慢了查询的效率!这样就会造成空间的浪费。
2019-06-04 06:52
作者回复
回答⾮常到位
2019-06-04 09:08

07讲深入浅出HashMap的设计与优化 - 图8嘉嘉
加载因⼦那块⼉,感觉有点跳跃,为什么加载因⼦越⼤,对空间利⽤越充分呢?
2019-06-13 09:37
作者回复
加载因⼦是扩容的参考标准,如果加载因⼦越⼤,例如默认数组初始化⼤⼩为16,加载因⼦由0.75改成1.0,原来数组⻓度到
了12就扩容,变成数组⼤⼩为16,也就是说被占满了,才会进⾏扩容,这也可能意味着已经发⽣了很多哈希冲突,这样导致某
些数组中的链表⻓度增加,影响查询效率。
2019-06-14 09:48

07讲深入浅出HashMap的设计与优化 - 图9tyul
⽼师,您好,请教⼀个问题,为什么 HashMap 的容量等于数组⻓度?但是扩容的时候却是根据 Map ⾥的所有元素总数去扩容
,这样会不会导致数组中的某⼀个 node 有很⻓的链表或红⿊树,数组中的其他位置都没有元素?谢谢
2019-06-05 19:20
07讲深入浅出HashMap的设计与优化 - 图10⼩⼩征
0 的话索引不变,1 的话索引变成原索引加上扩容前数组。 这句有点不理解 ⽼师
2019-06-05 10:25
作者回复
以下是resize中判断是否位移的部分代码,我们可以看到元素的hash值与原数组容量运算,如果运算结果为0,保持原位,如果运算结果为1,则意向扩容的⾼位。

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

假设链表中有4、8、12,他们的⼆进制位00000100、00001000、00001100,⽽原来数组容量为4,则是 00000100,以下与运算:

00000100 & 00000100 = 0 保持原位
00001000 & 00000100 = 1 移动到⾼位
00001100 & 00000100 = 1 移动到⾼位
2019-06-06 10:05

07讲深入浅出HashMap的设计与优化 - 图11⼤⾍⼦
⽼师您好,能解答下,为什么JDK1.8之前,链表元素增加采⽤的是头插法,1.8之后改成尾插法了。1.8之前采⽤头插法是基于什么设计思路呢?
2019-06-04 10:19
作者回复
你好,JDK1.7是考虑新增数据⼤多数是热点数据,所以考虑放在链表头位置,也就是数组中,这样可以提⾼查询效率,但这种
⽅式会出现插⼊数据是逆序的。在JDK1.8开始hashmap链表在节点⻓度达到8之后会变成红⿊树,这样⼀来在数组后节点⻓度不断增加时,遍历⼀次的次数就会少很多,相⽐头插法⽽⾔,尾插法操作额外的遍历消耗已经⼩很多了。

也有很多⼈说避免多线程情况下hashmap扩容时的死循环问题,我个⼈觉得避免死循环的关键不在尾插法的改变,⽽是扩容时
,⽤了⾸尾两个指针来避免死循环。这个我会在后⾯的多线程中讲到hashmap扩容导致死循环的问题。
2019-06-05 10:56
07讲深入浅出HashMap的设计与优化 - 图12bro.
编程中有遇到这种情况,当判断⼀个数n 是否为偶数使⽤n%2 == 0 ,计算机语⾔已经做了优化为 n&(2-1) = n & 1 == 0,对于⼆进制来说与操作只需要⼀个电路即可完成⽐取余速度快多了,相同的对于hash扩容,只需要判断前⼀位hash值是0还是1,如果是0保持数组位置不变,如果为1增加原来扩容前数组⻓度即可,⽽且由于hash值计算每⼀位都是平均分配0或者1,所以保持均匀分布
2019-06-10 11:10
07讲深入浅出HashMap的设计与优化 - 图13晓杰
初始容量2的n次⽅是偶数,在计算key的索引位置时,是通过(n-1)&hash计算的,这样n-1得到的奇数,那么通过在进⾏与操作时,如果hash的第⼀位是0,那么(n-1)&hash得到的是偶数,如果hash的第⼀位是1,那么(n-1)&hash得到的是奇数,因此可以让数据分布更加均匀,减少hash冲突,相反如果n-1是偶数,那⽆论hash的第⼀位是偶数还是奇数,(n-1)&hash得到的都是偶数,不利于数据的分布
2019-06-05 15:40
07讲深入浅出HashMap的设计与优化 - 图14迎⻛劲草
主要是为了减少hash冲突,hash & (n-1) n为2的n次幂,n-1换成⽐特位都是1
2019-06-05 22:11

07讲深入浅出HashMap的设计与优化 - 图15孙志强
以前看源码,我记得好像链表转换红⿊树不光链表元素⼤于8个,好像还有⼀个表的⼤⼩⼤于64
2019-06-05 09:46
作者回复
对的,有这样⼀个条件。
2019-06-06 08:11

07讲深入浅出HashMap的设计与优化 - 图16Liam
Hash字典发⽣扩容时,需要复制元素,请问这个过程是⼀次完成的吗?redis⾥的字典是准备了两个字典,⼀个原始字典,⼀个rehash字典,扩容后,不是⼀次完成数据迁移的,每次操作字典都考虑两个数组并复制数据,扩容完毕后交换两个数组指针

2019-06-05 08:23
作者回复
HashMap的扩容是⼀次性完成的。
2019-06-05 09:19

07讲深入浅出HashMap的设计与优化 - 图17-W.LI-
⽼师好。hashmap的put和get的时间复杂度算多少啊?最好O(1)。最坏复杂度是O(log(n))平均是O(1)么?。。。treeMap 的,treeMap,putO(n),getO(1)?之前⾯试被问了,不晓得哪错了
2019-06-04 23:08
作者回复

⾯试的时候,问到时间复杂度,⼤部分是考察你对数据结构的了解程度。建议可以多复习下数据结构的知识。

hashmap的最优时间复杂度是O(1),⽽最坏时间复杂度是O(n)。在没有产⽣hash冲突的情况下,查询和插⼊的时间复杂度是O(1);
⽽产⽣hash冲突的情况下,如果是最终插⼊到链表,链表的插⼊时间复杂度为O(1),⽽查询的时候,时间复杂度为O(n); 在产⽣hash冲突的情况下,如果最终插⼊的是红⿊树,插⼊和查询的平均时间复杂度是O(logn)。
⽽TreeMap是基于红⿊树实现的,所以时间复杂度你也清楚了吧。

2019-06-05 09:33

07讲深入浅出HashMap的设计与优化 - 图18强哥
最主要的原因是位运算替代%取模操作,提⾼运算性能,说什么降低冲突的,是否⽐较过质数和偶数冲突概率呢?
2019-06-04 21:42
作者回复
⽤与运算是提⾼了运算性能,⽽容量⼤⼩为2的幂次⽅是为了降低哈希冲突。
2019-06-05 08:30

07讲深入浅出HashMap的设计与优化 - 图19WolvesLeader
hashmap在多线程情况下数据丢失,⼤师,能不能分析分析原因
2019-06-04 10:36
作者回复
你好,这个在后⾯多线程中会讲到,可以留意关注下。
2019-06-05 10:59

07讲深入浅出HashMap的设计与优化 - 图20Darren
因为选择下标和扩容的时候都依赖和(n-1)按位与,2的幂次⽅满⾜n-1都是1的情况,因此必须选择2的幂次⽅,且如果使⽤时传⼊的参数不是2的幂次⽅,HashMap会⾃动转成⼤于传⼊参数的最⼩的2的幂次⽅来确保下标和扩容机制的正常运⾏
2019-06-04 09:25
07讲深入浅出HashMap的设计与优化 - 图21
总不能等到每个数组idx都有元素才扩容吧。那样⻤知道什么时候填满,按照元素总数初以加载因⼦就是为了减少链表⻓度。
2019-07-14 17:12
07讲深入浅出HashMap的设计与优化 - 图22吾爱有三
⽼师您好,1.8中hashmap解决了并发场景的死循环,那么为什么在⾼并发还是不建议使⽤hashmap?
2019-07-08 11:30
作者回复
因为存在线程安全性问题,例如put数据丢失,size()最终的数据不对等等。
2019-07-08 16:25

07讲深入浅出HashMap的设计与优化 - 图23luzero
⽼师请教⼀个问题,HashMap 获取元素优化这个章节中的 “例如,重新 key 值的 hashCode() ⽅法”,是重写?还是重新? 哈哈哈、我有点搞蒙了哦

2019-06-28 15:10
作者回复
是重写
2019-06-30 10:41

07讲深入浅出HashMap的设计与优化 - 图24luzero
使⽤2的次幂是因为进⾏&运算的时候,每次都能落到tables数组内,并且2的次幂进⾏&运算和直接模运算(%)的值是⼀样的
,也就是说(n-1)&hash==hash%n,如果直接使⽤(%)模运算最终也会转换成⼆进制的去计算性能不如&运算,还有就是& 计算分布均匀,减少哈希冲突,如果是2的次幂,假设n=16,(16-1)&0==0、(16-1)&1==1、16-1)&2==2、……..、(16-1)&
19==3、(16-1)&20==4、(16-1)&21==5、。如果不是2的次幂的话,假设是n=15,(15-1)&1==0、(15-1)&1==2、(15
-1)&3==2、……、(15-1)&4==4、(15-1)&5==4. 哈哈、不知道我回答的沾不沾边?望⽼师您点评⼀下哈
2019-06-28 12:53
作者回复
理解正确,赞
2019-06-30 10:39

07讲深入浅出HashMap的设计与优化 - 图25Forwardジ
⽼师,hashmap的⼤⼩看⽂章⾥没讲解,这块怎么总结?
2019-06-27 19:42
作者回复
是指hashmap扩容吗?
2019-06-30 10:16