web-dev-qa-db-ja.com

HashMap Java 8実装

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

HashMapの実装(または、HashMapの拡張)と混同しています。私のクエリは次のとおりです。

まず

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

これらの定数が使用される理由と方法このための明確な例が必要です。これでパフォーマンスを向上させる方法

次に

JDKでHashMapのソースコードが表示される場合、次の静的内部クラスがあります。

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

どのように使用されますか? アルゴリズムの説明が欲しいだけです

80

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

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

HashMap

ハッシュコード関数が適切な場合、すべてのバケットがある程度均等に使用されるように、均等な分布を提供する必要があります。この場合、バケットはリンクリストを使用して値を保存します。

Linked Buckets

しかし、優れたハッシュ関数を実装するために人々に頼ることはできません。人々はしばしば貧弱なハッシュ関数を書き、それが不均一な分布をもたらします。また、入力に不運が生じる可能性もあります。

Bad hashmap

この分布が均一でないほど、O(1)操作から遠ざかり、O(n)操作に近づきます。

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

Tree Bucket

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

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

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

ハッシュマップのサイズが大きくなると、より多くのバケットを持つように自動的にサイズが変更されます。小さなハッシュマップがある場合、非常に多くのバケットを取得する可能性は非常に高くなります。いっぱいにならないバケットを多くして、より大きなハッシュマップを作成することをお勧めします。この定数は基本的に、ハッシュマップが非常に小さい場合はバケットをツリーにしないことを示します。代わりにサイズを大きくする必要があります。


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

207
Michael

もっとシンプルに(もっと簡単にできるように)+詳細を追加します。

これらのプロパティは、直接移動する前に、非常に理解しやすい内部の多くのものに依存しています。

TREEIFY_THRESHOLD->singleバケットがこれに達すると(そして合計数が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がfast、fast、fast、slooowのように「冗談を言っている」ので、それは(気にする人にとって)です。他の実装があります-検索一時停止なしハッシュマップ ...

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

 entry.next.next....

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

13
Eugene

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.
8
Eran

それを視覚化する必要があります: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が大きくなると、その差はより大きくなります。

3
rentedrainbow

HashMap実装の変更は、 JEP-180 で追加されました。目的は:

マップエントリを格納するためにリンクリストではなくバランスの取れたツリーを使用することにより、高ハッシュ衝突条件下でのJava.util.HashMapのパフォーマンスを改善します。 LinkedHashMapクラスで同じ改善を実装する

ただし、純粋なパフォーマンスだけがゲインではありません。また、preventHashDoS攻撃 、ユーザー入力の保存にハッシュマップが使用される場合、 赤黒ツリー はデータの保存に使用されるためO(log n)のバケットの挿入の複雑さは最悪です。ツリーは、特定の基準が満たされた後に使用されます- Eugeneの答えを参照

2
Anton Krosnev

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

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

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

0
Avinash