HashMap Java 8の実装


92

次のリンクドキュメントに従って:Java HashMap実装

の実装HashMap(またはの拡張HashMap)と混同しています。私の質問は:

まず

static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

これらの定数はなぜ、どのように使用されますか?これについて明確な例をいくつか挙げたい。 彼らはこれでどのようにパフォーマンスの向上を達成していますか?

第二に

のソースコードがHashMapJDKにある場合、次の静的内部クラスが見つかります。

static final class TreeNode<K, V> extends java.util.LinkedHashMap.Entry<K, V> {
    HashMap.TreeNode<K, V> parent;
    HashMap.TreeNode<K, V> left;
    HashMap.TreeNode<K, V> right;
    HashMap.TreeNode<K, V> prev;
    boolean red;

    TreeNode(int arg0, K arg1, V arg2, HashMap.Node<K, V> arg3) {
        super(arg0, arg1, arg2, arg3);
    }

    final HashMap.TreeNode<K, V> root() {
        HashMap.TreeNode arg0 = this;

        while (true) {
            HashMap.TreeNode arg1 = arg0.parent;
            if (arg0.parent == null) {
                return arg0;
            }

            arg0 = arg1;
        }
    }
    //...
}

使い方は?アルゴリズムの説明が欲しいだけです

回答:


225

HashMap特定の数のバケットが含まれています。これを使用hashCodeして、これらを入れるバケットを決定します。簡単にするために、それを係数として想像してください。

ハッシュコードが123456で、バケットが4つある場合123456 % 4 = 0、アイテムは最初のバケットであるバケット1に入ります。

HashMap

私たちのハッシュコード関数が良ければ、すべてのバケットがいくらか等しく使用されるように、均等な分布を提供するはずです。この場合、バケットはリンクされたリストを使用して値を格納します。

リンクされたバケット

しかし、優れたハッシュ関数を実装するために人々に依存することはできません。人々は、貧弱なハッシュ関数を書くことが多く、結果として不均等な分布になります。また、入力に不運を感じる可能性もあります。

悪いハッシュマップ

この分布の均一性が低いほど、O(1)演算から遠ざかり、O(n)演算に近づきます。

Hashmapの実装は、バケットが大きくなりすぎた場合に、リンクされたリストではなくツリーにいくつかのバケットを編成することにより、これを軽減しようとします。これがTREEIFY_THRESHOLD = 8目的です。バケットに8個を超えるアイテムが含まれている場合は、ツリーになるはずです。

ツリーバケット

この木は赤黒木です。最初にハッシュコードでソートされます。ハッシュコードが同じ場合は、オブジェクトがそのインターフェイスを実装しているかどうかのcompareToメソッドを使用しComparable、それ以外の場合はIDハッシュコードを使用します。

エントリがマップから削除されると、バケット内のエントリの数が減り、このツリー構造が不要になる場合があります。それUNTREEIFY_THRESHOLD = 6が目的です。バケットの要素数が6を下回った場合は、リンクリストの使用に戻ることもできます。

最後に、がありますMIN_TREEIFY_CAPACITY = 64

ハッシュマップのサイズが大きくなると、自動的にサイズが変更され、バケットが増えます。ハッシュマップが小さい場合、バケットをいっぱいにする可能性は非常に高くなります。これは、さまざまなバケットにデータを入れることができないためです。より多くのバケットが満杯ではない、より大きなハッシュマップを持つ方がはるかに良いです。この定数は、ハッシュマップが非常に小さい場合、基本的にはバケットをツリーにしないことを示しています。代わりに、最初にサイズを大きくする必要があります。


パフォーマンスの向上に関する質問に答えるために、最悪のケースを改善するためにこれらの最適化が追加されました。私は推測しているだけですが、hashCode関数があまり良くなかった場合、これらの最適化のためにおそらく顕著なパフォーマンスの向上が見られるでしょう。


3
不均一な分布は、必ずしもハッシュ関数が貧弱であることを示すものではありません。たとえばString、一部のデータ型は、intハッシュコードよりもはるかに大きな値空間を持っているため、衝突は避けられません。今度は、実際Stringのsのように、均等な分布が得られるかどうかに関係なく、マップに入れる実際の値に依存します。悪い分布は単に不運の結果である可能性があります。
ホルガー

3
+1、このツリーアプローチが緩和する特定のシナリオは、ハッシュ衝突DOS攻撃であることを付け加えたいと思います。java.lang.Stringは決定論的で暗号化されていhashCodeないため、攻撃者は衝突するハッシュコードを使用して簡単に異なる文字列を作成できます。この最適化の前は、HashMap操作がO(n)時間に低下する可能性がありましたが、今では、O(log(n))に低下するだけです。
MikeFHay 2017年

1
+1、if the objects implement that interface, else the identity hash code.私はこの他の部分を探していました。
Number945

1
@NateGlennオーバーライドしない場合のデフォルトのハッシュコード
Michael

「この定数は、ハッシュマップが非常に小さい場合、基本的にはバケットをツリーに作成しないことを示しています。代わりに、最初にサイズを大きくする必要があります。」のためにMIN_TREEIFY_CAPACITY。それは、「8(TREEIFY_THRESHOLD)のキーがすでに含まれているバケットにハッシュされるキーを挿入すると、64(MIN_TREEIFY_CAPACITY)のキーがに既にある場合HashMap、そのバケットのリンクリストはバランスツリーに変換されます。」
anir

16

もっと簡単に(もっと簡単に)+もう少し詳しく説明します。

これらのプロパティは、直接移行する前に理解しておくと非常にクールな内部の多くの要素に依存します。

TREEIFY_THRESHOLD- > 単一のバケットがこれに達すると(そして総数がを超えるとMIN_TREEIFY_CAPACITY)、完全にバランスのとれた赤/黒のツリーノードに変換されます。どうして?検索速度のため。別の方法で考えてみてください。

Integer.MAX_VALUEエントリを含むバケット/ビン内のエントリを検索するには、最大で32ステップかかります。

次のトピックの紹介。ビン/バケットの数が常に2の累乗であるのはなぜですか?少なくとも2つの理由:モジュロ演算より高速で、負の数のモジュロは負になります。また、エントリを「負の」バケットに入れることはできません。

 int arrayIndex = hashCode % buckets; // will be negative

 buckets[arrayIndex] = Entry; // obviously will fail

代わりに、モジュロの代わりに使用される素敵なトリックがあります:

 (n - 1) & hash // n is the number of bins, hash - is the hash function of the key

これは意味的にはモジュロ演算と同じです。下位ビットを保持します。これを行うと、興味深い結果が得られます。

Map<String, String> map = new HashMap<>();

上記の場合、エントリがどこに行くかの決定は、ハッシュコードの最後の4ビットのみに基づい行われます。

ここで、バケットの乗算が始まります。特定の条件下では(正確な詳細を説明するのに長い時間がかかります)、バケットのサイズが2倍になります。どうして?バケットのサイズが2倍になると、もう1つビットが出てきます。

つまり、16個のバケットがあります。ハッシュコードの最後の4ビットで、エントリの行き先が決まります。バケットを2倍にします。32バケット-最後の5ビットで、エントリの移動先を決定します。

そのため、このプロセスは再ハッシュと呼ばれます。これは遅くなるかもしれません。これは、HashMapが「ジョーク」されているため(高速化、高速化、高速化、低速化)です。他の実装があります-検索無停止ハッシュマップ ...

これで、UNTREEIFY_THRESHOLDが再ハッシュ後に有効になります。その時点で、一部のエントリはこのビンから他のビンに移動する可能性があり((n-1)&hash計算にビットが1つ追加されるため、他のバケットに移動する可能性があります)、これに到達する可能性がありますUNTREEIFY_THRESHOLD。この時点で、それはのようにビンを保つために完済しませんred-black tree nodeが、として、LinkedListのような代わりに、

 entry.next.next....

MIN_TREEIFY_CAPACITYは、特定のバケットがツリーに変換される前のバケットの最小数です。


10

TreeNodeの単一のビンに属するエントリを格納する別の方法HashMapです。以前の実装では、ビンのエントリはリンクリストに格納されていました。Java 8では、ビンのエントリ数がしきい値(TREEIFY_THRESHOLD)を超えた場合、それらは元のリンクリストではなくツリー構造で保存されます。これは最適化です。

実装から:

/*
 * Implementation notes.
 *
 * This map usually acts as a binned (bucketed) hash table, but
 * when bins get too large, they are transformed into bins of
 * TreeNodes, each structured similarly to those in
 * java.util.TreeMap. Most methods try to use normal bins, but
 * relay to TreeNode methods when applicable (simply by checking
 * instanceof a node).  Bins of TreeNodes may be traversed and
 * used like any others, but additionally support faster lookup
 * when overpopulated. However, since the vast majority of bins in
 * normal use are not overpopulated, checking for existence of
 * tree bins may be delayed in the course of table methods.

ない正確に真。それらがパスし、TREEIFY_THRESHOLD かつビンの総数が少なくともである場合MIN_TREEIFY_CAPACITY。私はそれを私の答えでカバーしようとしました...
ユージーン

3

あなたはそれを視覚化する必要があります:hashCode()関数のみが常に同じ値を返すようにオーバーライドされたクラスキーがあると言います

public class Key implements Comparable<Key>{

  private String name;

  public Key (String name){
    this.name = name;
  }

  @Override
  public int hashCode(){
    return 1;
  }

  public String keyName(){
    return this.name;
  }

  public int compareTo(Key key){
    //returns a +ve or -ve integer 
  }

}

次に、どこか別の場所で、9つのエントリをHashMapに挿入します。すべてのキーはこのクラスのインスタンスです。例えば

Map<Key, String> map = new HashMap<>();

    Key key1 = new Key("key1");
    map.put(key1, "one");

    Key key2 = new Key("key2");
    map.put(key2, "two");
    Key key3 = new Key("key3");
    map.put(key3, "three");
    Key key4 = new Key("key4");
    map.put(key4, "four");
    Key key5 = new Key("key5");
    map.put(key5, "five");
    Key key6 = new Key("key6");
    map.put(key6, "six");
    Key key7 = new Key("key7");
    map.put(key7, "seven");
    Key key8 = new Key("key8");
    map.put(key8, "eight");

//Since hascode is same, all entries will land into same bucket, lets call it bucket 1. upto here all entries in bucket 1 will be arranged in LinkedList structure e.g. key1 -> key2-> key3 -> ...so on. but when I insert one more entry 

    Key key9 = new Key("key9");
    map.put(key9, "nine");

  threshold value of 8 will be reached and it will rearrange bucket1 entires into Tree (red-black) structure, replacing old linked list. e.g.

                  key1
                 /    \
               key2   key3
              /   \   /  \

ツリーの走査は、LinkedList {O(n)}よりも{O(log n)}の方が高速であり、nが大きくなるほど、その差は大きくなります。


すべて同じであるハッシュコードと、順序付けに役立たないequalsメソッド以外のキーを比較する方法がないため、効率的なツリーを構築できない可能性があります。
user253751

@immibisそれらのハッシュコードは必ずしも同じではありません。それらはかなり異なる可能性があります。クラスがそれを実装する場合、さらにcompareTofromを使用しComparableます。identityHashCodeそれが使用する別のメカニズムです。
マイケル

@Michaelこの例では、すべてのハッシュコードは必ずしも同じであり、クラスはComparableを実装していません。identityHashCodeは正しいノードを見つける価値がありません。
user253751

@immibisああそうです、私はそれをすくい取りましたが、あなたは正しいです。ように、Key実装されていないComparableidentityHashCode使用されます:)
マイケル・

@EmonMishra残念ながら、単に視覚的に見るだけでは十分ではありません。私はそれを私の答えでカバーしようとしました。
ユージーン

2

HashMap実装の変更は、JEP-180で追加されました。目的は次のとおりです。

リンクリストではなく平衡型ツリーを使用してマップエントリを格納することにより、高ハッシュ衝突条件下でのjava.util.HashMapのパフォーマンスを向上させます。LinkedHashMapクラスに同じ改善を実装する

ただし、純粋なパフォーマンスだけが向上するわけではありません。また、ハッシュマップを使用してユーザー入力を格納する場合は、HashDoS攻撃防止し ます。これは、バケットにデータを格納するために使用される赤黒木が、O(log n)に最悪の場合の挿入複雑さを持っているためです。ツリーは、特定の基準が満たされた後に使用されますユージーンの回答を参照してください。


-1

ハッシュマップの内部実装を理解するには、ハッシュを理解する必要があります。最も単純な形式のハッシュは、プロパティに数式/アルゴリズムを適用した後、変数/オブジェクトに一意のコードを割り当てる方法です。

真のハッシュ関数はこのルールに従う必要があります-

「ハッシュ関数は、関数が同じまたは等しいオブジェクトに適用されるたびに、同じハッシュコードを返す必要があります。つまり、2つの等しいオブジェクトが同じハッシュコードを一貫して生成する必要があります。」


これは質問の答えにはなりません。
スティーブンC
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.