web-dev-qa-db-ja.com

Java HashMapは、同じハッシュコードを持つさまざまなオブジェクトをどのように処理しますか?

私の理解によれば、私は思います:

  1. 2つのオブジェクトが同じハッシュコードを持つことは完全に合法です。
  2. 2つのオブジェクトが等しい場合(equals()メソッドを使用)、それらは同じハッシュコードを持ちます。
  3. 2つのオブジェクトが等しくない場合、それらは同じハッシュコードを持つことはできません。

私は正しいですか?

正しい場合は、次の質問があります。HashMapは内部的にオブジェクトのハッシュコードを使用しています。それで、2つのオブジェクトが同じハッシュコードを持つことができるならば、HashMapはそれがどのキーを使うかをどうやって追跡できますか?

誰かがHashMapがオブジェクトのハッシュコードを内部的に使用する方法を説明できますか?

200
akshay

ハッシュマップは次のように機能します(これは少し簡略化されていますが、基本的なメカニズムを示しています)。

キーと値のペアを格納するために使用する「バケット」がいくつかあります。各バケットには一意の番号があります。これがバケットを識別します。キーと値のペアをマップに入れると、ハッシュマップはそのキーのハッシュコードを調べて、識別子がそのキーのハッシュコードであるバケットにそのペアを格納します。たとえば、キーのハッシュコードは235 - >ペアはバケット番号235に格納されます(1つのバケットに複数のキーと値のペアを格納できることに注意してください)。

ハッシュマップの中で値を検索するとき、それにキーを与えることで、あなたが与えたキーのハッシュコードを最初に調べます。ハッシュマップは対応するバケットを調べ、equals()と比較して、指定したキーとバケット内のすべてのペアのキーを比較します。

これで、マップ内のキーと値のペアを調べるのに非常に効率的であることがわかります。キーのハッシュコードによって、ハッシュマップはすぐにどのバケットを見るべきかを知っているので、そのバケットの内容に対してテストするだけで済みます。

上記のメカニズムを見ると、キーのhashCode()メソッドとequals()メソッドに必要な要件もわかります。

  • 2つのキーが同じ場合(それらを比較するとequals()trueを返します)、それらのhashCode()メソッドは同じ番号を返さなければなりません。キーがこれに違反している場合、等しいキーが異なるバケットに格納される可能性があり、ハッシュマップはキーと値のペアを見つけることができません(同じバケットを検索するため)。

  • 2つのキーが異なる場合は、それらのハッシュコードが同じかどうかは関係ありません。ハッシュコードが同じ場合、それらは同じバケットに格納されます。この場合、ハッシュマップはそれらを区別するためにequals()を使用します。

319
Jesper

あなたの3番目の主張は間違っています。

2つの異なるオブジェクトが同じハッシュコードを持つことは完全に合法です。指定されたキーを持つ可能なエントリをすばやく見つけることができるように、HashMapによって「初回通過フィルタ」として使用されます。同じハッシュコードを持つキーは、指定されたキーと等しいかどうかがテストされます。

2つの異なるオブジェクトが同じハッシュコードを持つことができないという要件は望ましくありません。32 可能なオブジェクト(また、他のクラスが同じハッシュを生成する可能性があるため、異なる型がオブジェクトのフィールドを使用してハッシュコードを生成することすらできないことも意味します。)

84
Jon Skeet

HashMap structure diagram

HashMapEntryオブジェクトの配列です。

HashMapを単なるオブジェクトの配列と見なします。

このObjectが何であるかを見てください。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        final int hash;
… 
}

Entryオブジェクトは、キーと値のペアを表します。バケットに複数のnextがある場合、フィールドEntryは別のEntryオブジェクトを参照します。

2つの異なるオブジェクトのハッシュコードが同じであることが時々起こるかもしれません。この場合、2つのオブジェクトが1つのバケットに保存され、リンクリストとして表示されます。エントリポイントは、最近追加されたオブジェクトです。このオブジェクトは、nextフィールドを持つ別のオブジェクトを参照します。最後のエントリはnullを参照しています。

デフォルトのコンストラクタでHashMapを作成した場合

HashMap hashMap = new HashMap();

アレイはサイズ16、デフォルトは0.75のロードバランスで作成されます。

新しいキーと値のペアを追加する

  1. キーのハッシュコードを計算する
  2. 要素を配置する位置hash % (arrayLength-1)を計算します(バケット番号)
  3. HashMapにすでに保存されているキーを使用して値を追加しようとすると、値は上書きされます。
  4. そうでなければ、要素がバケットに追加されます。

バケットにすでに少なくとも1つの要素がある場合は、新しい要素が追加されてバケットの最初の位置に配置されます。そのnextフィールドは、古い要素を参照しています。

削除

  1. 与えられたキーのハッシュコードを計算する
  2. バケット番号を計算するhash % (arrayLength-1)
  3. バケット内の最初のEntryオブジェクトへの参照を取得し、equalsメソッドを使用して、指定されたバケット内のすべてのエントリを繰り返します。やがて正しいEntryが見つかるでしょう。目的の要素が見つからない場合はnullを返します
64
Sergii Shevchyk

あなたは http://javarevisited.blogspot.com/2011/02/how-hashmap-works-in-Java.html で優れた情報を見つけることができます

要約する:

HashMapはハッシュの原理に基づいて動作します

put(key、value):HashMapはキーと値の両方のオブジェクトをMap.Entryとして保存します。ハッシュマップはバケットを取得するためにハッシュコード(key)を適用します。衝突があると、HashMapはLinkedListを使用してオブジェクトを格納します。

get(key):HashMapはKey Objectのハッシュコードを使用してバケットの場所を見つけ、次にkeys.equals()メソッドを呼び出してLinkedList内の正しいノードを識別して戻ります。 Java HashMap内のそのキーに関連付けられた値オブジェクト。

34
Abhijit Gaikwad

Java 8バージョンの場合のHashMapのメカニズムの大まかな説明は、次のとおりです(Java 6とは若干異なる場合があります)


データ構造

  • ハッシュテーブル
    ハッシュ値はkeyのhash()によって計算され、ハッシュテーブルのどのバケットを与えられたキーに使うかを決定します。
  • リンクリスト(単独)
    バケット内の要素数が少ない場合は、片方向リンクリストが使用されます。
  • 赤黒木
    バケット内の要素数が多い場合は、赤黒の木が使用されます。

クラス(内部)

  • Map.Entry
    マップ内の単一のエンティティ、キー/値エンティティを表します。
  • HashMap.Node
    ノードのリンクリストバージョン。

    それは表すことができます:

    • ハッシュバケット.
      それはハッシュ特性を持っているからです。
    • 単独リンクリスト内のノード(したがってリンクリストの先頭)
  • HashMap.TreeNode
    ノードのツリーバージョン。

フィールド(内部)

  • Node[] table
    バケットテーブル(リンクリストの先頭)。
    バケットに要素が含まれていない場合、それはnullです。したがって、参照用のスペースだけを取ります。
  • Set<Map.Entry> entrySetエンティティのセット。
  • int size
    エンティティの数.
  • float loadFactor
    サイズ変更する前に、ハッシュテーブルが一杯になることを許可することを示します。
  • int threshold
    サイズ変更する次のサイズ。
    式:threshold = capacity * loadFactor

メソッド(内部)

  • int hash(key)
    キーでハッシュを計算します。
  • ハッシュをバケットにマッピングする方法
    以下の論理を使用してください。

    static int hashToBucket(int tableSize, int hash) {
        return (tableSize - 1) & hash;
    }
    

定員について

ハッシュテーブルでは、容量はバケット数を意味し、それはtable.lengthから取得できます。
thresholdおよびloadFactorを介して計算することもできます。したがって、クラスフィールドとして定義する必要はありません。

有効容量は、次の方法で取得できます。capacity()


オペレーション

  • キーでエンティティを見つけます。
    最初にハッシュ値でバケットを見つけ、次にリンクリストをループするか、ソートされたツリーを検索します。
  • キーでエンティティを追加します。
    まず、キーのハッシュ値に従ってバケットを見つけます。
    次に、次の値を見つけます。
    • 見つかった場合は、値を置き換えます。
    • それ以外の場合は、リンクリストの先頭に新しいノードを追加するか、ソートツリーに挿入します。
  • リサイズ
    thresholdに達すると、ハッシュテーブルの容量(table.length)を2倍にしてから、すべての要素に対して再ハッシュを実行してテーブルを再構築します。
    これはコストのかかる操作です。

パフォーマンス

  • ゲット&プット
    時間の複雑さはO(1)です。
    • バケットは配列インデックス、つまりO(1)を介してアクセスされます。
    • 各バケット内のリンクリストは長さが短いため、O(1)として表示される可能性があります。
    • ツリー数も制限されています。これは、要素数が増えると、容量が増えてハッシュが再ハッシュされるため、O(1)ではなくO(log N)と見なすことができるためです。
21
Eric Wang

ハッシュコードは、ハッシュマップのどのバケットをチェックするかを決定します。バケット内に複数のオブジェクトがある場合は、バケット内のどの項目が(equals()メソッドを使用して)目的の項目に一致するかを検索するために線形検索が行われます。

言い換えれば、もしあなたが完璧なハッシュコードを持っていてもハッシュマップアクセスは一定であれば、あなたはバケツを通して繰り返す必要は決してないでしょう。スペース所要量を削減します。あなたが最悪のハッシュコードを持っている(常に同じ数を返す)なら、あなたが欲しいものを得るためにあなたがマップ内のすべてのアイテムを通して検索しなければならないので(それらはすべて同じバケットにある).

ほとんどの場合、よく書かれたハッシュコードは完璧ではありませんが、ほぼ一定のアクセスを提供するのに十分なほどユニークです。

14
Pace

あなたはポイント3を間違えています。 2つのエントリは同じハッシュコードを持つことができますが、等しくなることはできません。 OpenJdkのHashMap.get の実装を見てください。ハッシュが等しいこととキーが等しいことを確認していることがわかります。ポイント3が真であれば、キーが等しいことを確認する必要はありません。前者がより効率的な比較であるため、ハッシュコードはキーの前に比較されます。

これについてもう少し詳しく知りたい場合は、ウィキペディアの Open Addressing collision resolution の記事を見てください。これは、OpenJdkの実装で使用されているメカニズムだと思います。そのメカニズムは、他の答えの1つが述べている「バケツ」アプローチとは微妙に異なります。

11
Leif Wickland
import Java.util.HashMap;

public class Students  {
    String name;
    int age;

    Students(String name, int age ){
        this.name = name;
        this.age=age;
    }

    @Override
    public int hashCode() {
        System.out.println("__hash__");
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        System.out.println("__eq__");
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Students other = (Students) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    public static void main(String[] args) {

        Students S1 = new Students("taj",22);
        Students S2 = new Students("taj",21);

        System.out.println(S1.hashCode());
        System.out.println(S2.hashCode());

        HashMap<Students,String > HM = new HashMap<Students,String > (); 
        HM.put(S1, "tajinder");
        HM.put(S2, "tajinder");
        System.out.println(HM.size());
    }
}

Output:

__ hash __

116232

__ hash __

116201

__ hash __

__ hash __

2

そのため、ここでは、オブジェクトS1とS2の両方のコンテンツが異なる場合、オーバーライドされたHashcodeメソッドが両方のオブジェクトに対して異なるHashcode(116232、11601)を生成することを確信しています。今は違うハッシュコードがあるので、EQUALSメソッドを呼ぶのも面倒ではありません。異なるハッシュコードは、オブジェクト内の異なるコンテンツを保証するからです。

    public static void main(String[] args) {

        Students S1 = new Students("taj",21);
        Students S2 = new Students("taj",21);

        System.out.println(S1.hashCode());
        System.out.println(S2.hashCode());

        HashMap<Students,String > HM = new HashMap<Students,String > (); 
        HM.put(S1, "tajinder");
        HM.put(S2, "tajinder");
        System.out.println(HM.size());
    }
}

Now lets change out main method a little bit. Output after this change is 

__ hash __

116201

__ hash __

116201

__ hash __

__ hash __

__ eq __

1
We can clearly see that equal method is called. Here is print statement __eq__, since we have same hashcode, then content of objects MAY or MAY not be similar. So program internally  calls Equal method to verify this. 


Conclusion 
If hashcode is different , equal method will not get called. 
if hashcode is same, equal method will get called.

Thanks , hope it helps. 
5
Tajinder Singh

各Entryオブジェクトはキーと値のペアを表します。バケットに複数のエントリがある場合、フィールドnextは他のエントリオブジェクトを参照します。

2つの異なるオブジェクトのハッシュコードが同じであることが時々起こるかもしれません。この場合、2つのオブジェクトが1つのバケットに保存され、LinkedListとして表示されます。エントリポイントは、最近追加されたオブジェクトです。このオブジェクトは、次のフィールドを持つ他のオブジェクトを参照します。最後のエントリがnullを参照しています。デフォルトのコンストラクタでHashMapを作成するとき

配列はサイズ16、デフォルトは0.75のロードバランスで作成されます。

enter image description here

(ソース)

2
Premraj

ハッシュマップはハッシュの原理に基づいて動作します

HashMapのget(Key k)メソッドは、キーオブジェクトのhashCodeメソッドを呼び出し、返されたhashValueを独自の静的ハッシュ関数に適用して、キーと値がEntry(Map)と呼ばれるネストクラスの形式で格納されるバケット位置(バッキング配列)を探します。エントリ)。したがって、前の行から、キーと値の両方がEntryオブジェクトの形式としてバケットに格納されていると結論付けました。そのため、値だけがバケットに格納されていると考えることは正しくなく、インタビュアーに良い印象を与えることはありません。

  • HashMapオブジェクトのget(Key k)メソッドを呼び出すたびに。まず、keyがnullかどうかをチェックします。 HashMapにはnullキーが1つしかないことに注意してください。

Keyがnullの場合、nullキーは常にハッシュ0、つまりインデックス0にマッピングされます。

Keyがnullでない場合、keyオブジェクトのhashfunctionを呼び出します。上記のメソッドの4行目、つまりkey.hashCode()を参照してください。したがって、key.hashCode()がhashValueを返した後、4行目は次のようになります。

            int hash = hash(hashValue)

そして今、返されたhashValueをそれ自身のハッシュ関数に適用します。

Hash(hashValue)を使ってハッシュ値をもう一度計算しているのはなぜだろうか。答えは質の悪いハッシュ関数から守ることです。

これで、Entryオブジェクトが格納されているバケットの場所を見つけるために最終ハッシュ値が使用されます。エントリオブジェクトは、このようにバケットに格納します(ハッシュ、キー、値、バケットインデックス)。

1
user2706780

HashMapのしくみの詳細については説明しませんが、HashMapのしくみを現実と関連付けることで、HashMapのしくみを思い出すことができるように例を示します。

Key、Value、HashCode、そしてbucketがあります。

しばらくの間、それぞれを以下のように関連付けます。

  • バケツ - >社会
  • ハッシュコード - >社会の住所(常に一意)
  • 価値 - >社会の中の家
  • キー - >家の住所.

Map.get(key)を使う:

StevieはVIP社会の別荘に住んでいる彼の友人の(Josse)家に行きたがっています。それをJavaLovers Societyにしましょう。 Josseの住所は彼のSSNです(これは誰にとっても異なります)。 SSNに基づいて協会の名前を見つけるための索引が維持されています。このインデックスはHashCodeを見つけるためのアルゴリズムと考えることができます。

  • SSNソサエティの名前
  • 92313(Josse's) - Javaラバーズ
  • 13214 - AngularJSLovers
  • 98080 - Javaラバーズ
  • 53808 - バイオロバー

  1. このSSN(キー)は、最初に(インデックステーブルから)ハッシュコードを与えます。これは、Societyの名前に他なりません。
  2. 今、複数の家が同じ社会にあることができるので、HashCodeは一般的になることができます。
  3. 協会が2つの家に共通していると仮定して、私たちがどの家に行っているのかを特定するにはどうすればよいですか。

Map.put(key、Value)を使う

これはHashCodeを見つけることによってこの値に適した社会を見つけ、そして値が格納されます。

私はこれが助けになることを願っていますし、これは変更の余地があります。

1
Prashant K

2つのオブジェクトが等しいということは、それらが同じハッシュコードを持っていることを意味しますが、その逆はありません。

HashMapのJava 8アップデート -

あなたはあなたのコードの中でこの操作をします -

myHashmap.put("old","key-value-pair");
myHashMap.put("very-old","old-key-value-pair");

そのため、両方のキー"old""very-old"に対して返されたハッシュコードが同じであるとします。それではどうなるでしょう。

myHashMapはHashMapです。最初は容量を指定しなかったとします。 Javaあたりのデフォルト容量は16です。したがって、newキーワードを使用してハッシュマップを初期化するとすぐに、16個のバケットが作成されます。最初のステートメントを実行したときに

myHashmap.put("old","key-value-pair");

それから"old"のためのハッシュコードが計算されます、そして、ハッシュコードも非常に大きい整数であるかもしれないので、それで、Javaは内部的にこれをしました -

hash XOR hash >>> 16

これを大きくすると、0から15の間のインデックスが返されます。これで、キー値のペア"old""key-value-pair"は、Entryオブジェクトのキーと値のインスタンス変数に変換されます。そして、このエントリオブジェクトはバケットに格納されます。または、特定のインデックスにこのエントリオブジェクトが格納されると言えます。

FYIエントリは、これらのシグネチャ/定義を持つMapインタフェースMap.Entryのクラスです。

class Entry{
          final Key k;
          value v;
          final int hash;
          Entry next;
}

今度は次の文を実行するとき -

myHashmap.put("very-old","old-key-value-pair");

"very-old""old"と同じハッシュコードを与えるので、この新しいキーと値のペアは同じインデックスまたは同じバケットに送信されます。しかし、このバケットは空ではないため、Entryオブジェクトのnext変数を使用してこの新しいキーと値のペアを格納します。

これは、同じハッシュコードを持つすべてのオブジェクトのリンクリストとして格納されますが、TRIEFY_THRESHOLDは値6で指定されます。ルート。

1
anubhs

言われているように、絵は1000語の価値があります。私は言う:いくつかのコードは1000語以上です。これがHashMapのソースコードです。メソッドを取得:

/**
     * Implements Map.get and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

そのため、ハッシュが「バケット」を見つけるために使用され、最初の要素が常にそのバケットでチェックされることが明らかになります。そうでない場合は、キーのequalsを使用してリンクリスト内の実際の要素を見つけます。

put()メソッドを見てみましょう:

  /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

もう少し複雑ですが、新しい要素がハッシュに基づいて計算された位置のタブに配置されることが明らかになります。

ここでi = (n - 1) & hashここでiは新しい要素が置かれるインデックスです(またはそれは "バケツ"です)。 ntab配列( "バケット"の配列)のサイズです。

まず、その「バケツ」の最初の要素として入れることを試みます。すでに要素がある場合は、新しいノードをリストに追加します。

0
ACV