HashMapが2回ハッシュする必要があるのはなぜですか

1はじめに

HashMapは、Javaプログラマーにとって見知らぬ人であってはなりません。日常の開発で頻繁に使用されるだけでなく、インタビュアーが尋ねるのが好きな知識ポイントでもあります。HashMapは、ハッシュテーブルの古典的な実装であり、基になるデータ構造は配列+リンクリストです。JDK8では、リンクリストの線形検索の効率の問題を解決するために赤黒ツリーも導入されました。HashMapのデザインはとても良いです。ソースコードは2,000行以上です。議論できる点がたくさんあります。この記事は主にHashMapの二次ハッシュの目的を分析します。

2.ハッシュコードの役割

まず、ハッシュコードの役割を理解する必要がありますか?HashMapの最下層は、配列+リンクリスト/赤黒木のデータ構造を使用して、キーと値のペアのマッピング関係を格納します。配列はハッシュスロットの数です。Solt。HashMapは、ハッシュコードを使用して添え字インデックスを計算します。キーによって計算され、インデックスによってキーが決定されます。値のペアはどのスロットに分類されますか。異なるハッシュコードが同じ添え字インデックスを計算するため、ハッシュの衝突が発生します。ハッシュの衝突が発生すると、HashMapの検索効率はO(1)からO(n)またはO(logn)に低下します。したがって、優れたハッシュ関数は可能な限り分散化する必要があります。そうしないと、HashMapの効率に影響します。

3.セカンダリハッシュ

HashMapがハッシュコードに従って添え字を計算することはすでにわかっています。ハッシュコードの分散が良いほど、HashMapの効率は高くなります。まず、HashMapの添え字を計算するプロセスを見てみましょう。そうすれば、2番目のハッシュを実行する必要がある理由がわかります。

static int indexFor(int h, int length) {
    return h & (length-1);
}
复制代码

上記は、セカンダリハッシュに従ってHashMapによって計算されたハッシュコードであり、キーと値のペアの添え字を計算するためのコードlengthは、基になる配列の長さです。HashMapは、一般的なモジュロ演算の代わりにビット演算を使用します。ここではスキップできます。2つの効果は同じです。

まず、二次ハッシュを行わないとどうなるかを見てみましょう。ここで、配列の長さを16と仮定すると、ハッシュコードが5の場合、添え字のインデックスの結果は5になります。

 00000000000000000000000000000101
&00000000000000000000000000001111
=00000000000000000000000000000101
=5
复制代码

ハッシュコードが65541の場合、添え字インデックスの結果は5のままです。異なるハッシュコードが同じ添え字を計算し、ハッシュが衝突します。

 00000000000000010000000000000101
&00000000000000000000000000001111
=00000000000000000000000000001101
=5
复制代码

从这个与运算的过程,大家肯定也都发现了,就是哈希码的高位压根就没有参与运算,全部被丢弃了。不管哈希码的高位是多少,都不会影响最终Index的计算结果,因为只有低位才参与了运算,这样的哈希函数我们认为是不好的,它会带来更多的冲突,影响HashMap的效率。

如何解决这个问题呢?最简单的办法就是让高位也参与到运算,高位不一样也会导致最终的Index结果不一样,减少哈希碰撞的概率。事实上,HashMap也就是这么做的,下面是HashMap做二次Hash的源码:

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

HashMap通过将哈希码的高16位与低16位进行异或运算,得到一个新的哈希码,这样就可以让高位也参与到运算,这个函数也被称作「扰动函数」。

我们用同样的哈希码,来看看经过二次Hash后的哈希码,是否会带来不一样的效果。 仍然假设数组长度为16,那么当哈希码为5时,下标Index是5,结果不变。

 0000000000000101
^0000000000000000
=0000000000000101

 00000000000000000000000000000101
&00000000000000000000000000001111
=00000000000000000000000000000101
=5
复制代码

当哈希码为65541时,下标Index结果是4,竟然没有发生哈希碰撞。

 0000000000000101
^0000000000000001
=0000000000000100

 00000000000000010000000000000100
&00000000000000000000000000001111
=00000000000000000000000000000100
=4
复制代码

可以看到,HashMap通过加入一个扰动函数,让原本会发生碰撞的两个哈希码,不再冲突。

4. 为啥右移16位

HashMap的扰动函数,是拿高16位和低16位做异或运算,把高位的特征和地位的特征组合起来,以此来降低哈希碰撞的概率。为啥是16位?而不是8位或24位或其它位?

根据哈希码计算下标Index的过程,大家也发现了。实际上,只有数组长度以内的低位才会参与运算。例如数组长度是16,那么只有低4位会参与计算;如果数组长度是256,那么只有低8位会参与计算;如果数组长度是65536,那么只有低16位会参与计算。HashMap取16位是一个折中的数字,绝大部分情况下,HashMap数组的长度都不会超过65536。

5. 总结

HashMap底层采用数组+链表/红黑树来存储键值对,会根据Key的哈希码来计算键值对落在数组的哪个下标。如果不同的哈希码算出相同的下标,就会导致哈希碰撞,影响HashMap的性能。HashMap要做的,就是尽量避免哈希碰撞,所以加入了扰动函数。扰动函数会将哈希码的高16位与低16位做异或运算,让高位也参与到下标的计算过程中来,从而影响最终下标的计算结果,减少哈希碰撞的概率。至于为啥是16位,这是因为哪些位会参与到下标的计算,取决于HashMap数组的长度,在绝大部分情况下,数组的长度都不会超过65536,16位是一个折中的数字。

おすすめ

転載: juejin.im/post/7100829047042605064