然后就是进入一个 for(;;) 循环。
先看标号为 ② 的地方,你注意看,这个时候的判断条件是 value[index] == null
,是判断算出来的 index 对应的 value[] 数组对应的下标是否有值。
前面我专门强调了一句,还给你画了一个图:
key[] 和 values[] 这两个数组的容量是一样的。
为什么不先判断该 index 在 key[] 中是否存在呢?
可以倒是可以,但是你想想如果 value[] 对应下标中的值是 null 的话,那么说明这个位置上并没有维护过任何东西。key 和 value 的位置是一一对应的,所以根本就不用去关心 key 是否存在。
如果 value[index] == null
为 true,那么说明这个 key 之前没有被维护过,直接把对应的值维护上,且 key[] 和 values[] 数组需要分别维护。
假设以我的演示代码为例,第四次循环结束后是这样的:
在这个方法里面,我们可以看到 IntObjectHashMap 的扩容机制是一次扩大 2 倍。
额外说一句:这个地方就有点 low 了,源码里面扩大二倍肯定得上位运算,用 length << 1 才对味儿嘛。
但是扩容之前需要满足一个条件:size > maxSize
size,我们知道是表示当前 map 里面放了几个 value 。
那么 maxSize 是啥玩意呢?
前面我们讨论的是 value[index] == null
为 true 的情况。那么如果是 false 呢?
就来到了标号为 ③ 的地方。
判断 key[] 数组 index 下标处的值是否是当前的这个 key。
如果是,说明要覆盖。先把原来该位置上的值拿出来,然后直接做一个覆盖的操作,并返回原值,这个逻辑很简单。
但是,如果不是这个 key 呢?
说明什么情况?
是不是说明这个 key 想要放的 index 位置已经被其他的 key 先给占领了?
这个情况是不是就是出现了 hash 冲突?
出现了 hash 冲突怎么办?
那么就来到了标号为 ③ 的地方,看这个地方的注释:
Conflict, keep probing ...
冲突,继续探测 ...
继续探测就是看当前发生冲突的 index 的下一个位置是啥。
如果让我来写,很简单,下一个位置嘛,我闭着眼睛用脚都能敲出来,就是 index+1 嘛。
但是我们看看源码是怎么写的:
确实看到了 index+1,但是还有一个先决条件,即 index != values.length -1
。
如果上述表达式成立,很简单,采用 index+1。
如果上面的表达式不成立,说明当前的 index 是 values[] 数组的最后一个位置,那么就返回 0,也就是返回数组的第一个下标。
要触发这个场景,就是要搞一个 hash 冲突的场景。我写个代码给你演示一下:
看起来就是一个环,对不对?
是的,它就是一个环。
但是你再细细的看这个判断:
每次计算完 index 后,还要判断是否等于本次循环的 startIndex。如果相等,说明跑了一圈了,还没找到空位子,那么就抛出 “Unable to insert” 异常。
有的朋友马上就跳出来了:不对啊,不是会在用了一半空间以后,以 2 倍扩容吗?应该早就在容量满之前就扩容了才对呀?
这位朋友,你很机智啊,你的疑问和我第一次看到这个地方的疑问是一样的,我们都是心思缜密的好孩子。
但是注意看,在抛出异常的地方,源码里面给了一个注释:
Can only happen if the map was full at MAX_ARRAY_SIZE and couldn't grow.
这种情况只有 Map 已经满了,且无法继续扩容时才会发生。
扩容,那肯定也是有一个上限才对,再看看扩容的时候的源码:
最大容量是 Integer.MAX_VALUE - 8,说明是有上限的。
但是,等等,Integer.MAX_VALUE 我懂,减 8 是什么情况?
诶,反正我是知道的,但是咱就是不说,不是本文重点。你要有兴趣,自己去探索,我就给你截个图完事:
如果我想要验证一下 “Unable to insert” 怎么办呢?
这还不简单吗?源码都在我手上呢。
两个方案,一个是修改 growSize() 方法的源码,把最长的长度限制修改为指定值,比如 8。
第二个方案是直接严禁扩容,把这行代码给它注释了:
你会发现在插入第 10 个值的时候,抛出了 “Unable to insert” 异常。
第 10 个值,89,就是这样似儿的,转一圈,又走回了 startIndex:
满足这个条件,所以抛出异常:
(index = probeNext(index)) == startIndex
到这里,put 方法就讲完了。你也了解到了它的数据结构,也了解到了它的基本运行原理。
那你还记得我写这篇文章要追寻的问题是什么吗?
IntObjectHashMap 性能更好的原因是什么呢?
前面提到了一个点是 key 可以使用原生的 int 类型而不用包装的 Integer 类型。
现在我要揭示第二个点了:value 没有一些乱七八糟的东西,value 就是一个纯粹的 value。你放进来是什么,就是什么。
你想想 HashMap 的结构,它里面有个 Node,封装了 Hash、key、value、next 这四个属性:
这部分东西也是 IntObjectHashMap 节约出来的,而这部分节约出来的,才是占大头的地方。
你不要看不起着一点点内存占用。在一个巨大的基数面前,任何一点小小的优化,都能被放大无数倍。
不知道你还记不记得《深入理解Java虚拟机》一书里面的这个案例: