hash 方法只有两行。但是这两行都非常的关键。
先看第一个 System.identityHashCode,这个是什么东西?
看看 API 上的解释:
就是对于一个对象,不管你有没有重写 hashCode 方法,该方法返回的值都是不会变化的。
看两个示例代码:
注意 Person 对象是没有重写 hashCode 方法的。
程序的最终输出结果是这样的:
我们分成三个部分去看,我们可以发现。
当对象(Person)没有重写 hashCode 方法的时候,他们的 hashCode 和 identityHashCode 是一样的。
即使对象(String)重写了 hashCode 方法,对于不同的对象,hashCode 值是一样的,但是 identityHashCode 可能是不一样的。
注意是“可能不一样”。因为 identityHashCode 的底层逻辑是基于一个伪随机数生成的。
这个特性特别有用,但是也别乱用。用错了,就是一个 bug。
比如在 identityHashMap 里面的使用就是一个正确的使用。至于错误的使用,我们稍后会讲。
经过前面的分析我们知道了:hash 方法中的第一行代码,对于 new 出来的相同对象的不同实例,不管是否重写 hashCode 方法,会产生不同的 identityHashCode。
可以说 System.identityHashCode 方法,是整个 identityHashMap 的基石。
然后再看这一行代码:
很多朋友第一眼看到位运算,心里就稍微有点抵触。
别这样,我带你分析一下,很简单的。
首先,我前面画图示意了 identityHashMap 的存储套路,说了:key 的下一个位置就是这个 key 的 value。
那么 key 的位置一定要是一个偶数。
这一点能不能跟上?跟不上你就多想想再往下看。
而 hash 方法就是计算 key 的位置。
所以,该方法的返回值一定是一个偶数。
这缜密的逻辑,是不是无懈可击。
假设 length 为 64 的话,那么这一行代码的目的是为了生成一个 0 到 63 之间的偶数。
0 到 63 之间的数,是 &(length-1) 保证的。这个没啥说的。
那么为什么一定会生成一个偶数呢?
h<<1 的最终结果肯定是一个偶数吧?
h<<8 的最终结果肯定也是一个偶数吧?
那么偶数减去偶数是一个什么数?
什么,你问我会不会溢出?
你管它溢出不溢出,就算它变成负数了,变成 0 了,它也是一个偶数呀!
偶数的二进制的最后一位是不是 0?
length-1 这个数的二进制最后一位不是 0 就是 1,对不对?
0 & 上 0 或者 1,是不是还是 0?
那不就对了。所以,最终结果肯定是一个偶数的。
经过前面的分析,我们知道了标号为 ① 的地方返回的 i 肯定是一个 0 到 len-1 之间的偶数:
返回的这个偶数 i,在标号为 ② 和 ③ 的地方都有用到。
标号为 ② 的地方是检查传进来的这个 key 是否在数组中已经存在了,也就是我们说的是否 hash 冲突。
如果没冲突,继续往下执行。
如果冲突了,且 value 值存在,就替换 value 值,然后返回。
如果冲突了,且 value 值不存在, i 值经过 nextKeyIndex 方法后也发生了变化。
下标 i 是怎么变化的呢?
假设我们来了一个 key=key2 的元素,经过 hash 计算后,对应数组下标为 2,但是该位置上已经有了一个 key1 ,那么就是发生了 hash 冲突:
发生冲突,i+2,也就是找到下一个偶数下标。
代码中是这样的体现的:
当 key2 的 identityHashCode 和 key1 一样,发生 hash 冲突之后,是这样存储的:
那势必会出现 i+2 的结果比 len 还长的情况:
你发现源码是怎么解决这个问题的吗?
这个 nextkeyIndex 这个方法首尾相接,它是一个圆啊:
这种情况,这个圆,画图是怎么体现的呢?
怎么样,是不是很骚。
执行到编号为 ③ 的地方,就很清晰了:
key 是放在 tab[i] 的位置的。
value 是放在 tab[i+1] 的位置的。
和我们画图的逻辑一致。
畅游源码-GET
接下来我们看看 get 方法:
标号为 ① 的地方,直接取到了对应的 key。
你注意这个地方,用的是 == 来判断对象是否相等,hashMap 用的是 equals 。
标号为 ② 的地方,是没有对应的 key,直接返回 null。
走到标号为 ③ 的地方,代表这个 key 发生过 hash 冲突。那么接着找下一个偶数位下标的 key。
比如我们这里的 key2:
整个过程还是非常清晰的。学习的时候可以和 hashMap 的 get 方法进行对比学习。
你会发现,思想是一个思想,但是解决方案是完全不同的解决方案。