web-dev-qa-db-ja.com

hashCode()がJavaの異なるオブジェクトに対して同じ値を返すことができるのはなぜですか?

私が読んでいる本からの引用 Head First Java

重要なのは、hashCode()メソッドで使用される「ハッシュアルゴリズム」が複数のオブジェクトに対して同じ値を返す可能性があるため、必ずしもオブジェクトが等しいことを保証しなくてもハッシュコードが同じになる可能性があるということです。

hashCode()メソッドが異なるオブジェクトに対して同じ値を返すのはなぜですか?それは問題を引き起こしませんか?

16
Eugene

hashingオブジェクトは、「まったく同じインスタンスで再現できる適切で説明的な値(数値)を見つけることを意味します何度も何度も」。 JavaのObject.hashCode()のハッシュコードはint型であるため、_2^32_の異なる値しか持つことができません。そのため、2つの異なるオブジェクトが同じhashCodeを生成する場合、ハッシュアルゴリズムに応じて、いわゆる「衝突」が発生します。

hashCode()は主にequals()と一緒に使用されるため、通常、これによって問題が発生することはありません。たとえば、HashMapはそのキーに対してhashCode()を呼び出し、キーがすでにHashMapに含まれているかどうかを確認します。 HashMapがハッシュコードを見つけられない場合は、キーがまだHashMapに含まれていないことは明らかです。ただし、その場合は、equals()を使用して、同じハッシュコードを持つすべてのキーを再確認する必要があります。

つまり.

_A.hashCode() == B.hashCode() // does not necessarily mean
A.equals(B)
_

だが

_A.equals(B) // means
A.hashCode() == B.hashCode()
_

equals()hashCode()が正しく実装されている場合。

一般的なhashCodeコントラクトのより正確な説明については、 Javadoc を参照してください。

33
Lukas Eder

可能なハッシュコードは40億をわずかに超えています(intの範囲)が、作成するために選択できるオブジェクトの数ははるかに多くなります。したがって、一部のオブジェクトは、 鳩の巣原理 によって同じハッシュコードを共有する必要があります。

たとえば、A〜Zの10文字を含む可能な文字列の数は26 ** 10で、これは141167095653376です。これらの文字列すべてに一意のハッシュコードを割り当てることはできません。また、重要ではありません。ハッシュコードは一意である必要はありません。実際のデータに対して衝突が多すぎないようにする必要があります。

26
Mark Byers

ハッシュテーブルの考え方は、辞書と呼ばれるデータ構造を効率的に実現できるようにすることです。ディクショナリはキー/値ストアです。つまり、特定のオブジェクトを特定のキーの下に格納し、後で同じキーを使用してそれらを再度取得できるようにする必要があります。

値にアクセスする最も効率的な方法の1つは、値を配列に格納することです。たとえば、次のように、キーに整数を使用し、値に文字列を使用する辞書を実現できます。

_String[] dictionary = new String[DICT_SIZE];
dictionary[15] = "Hello";
dictionary[121] = "world";

System.out.println(dictionary[15]); // prints "Hello"
_

残念ながら、このアプローチはあまり一般的ではありません。配列のインデックスは整数値である必要がありますが、理想的には、整数だけでなく、任意の種類のオブジェクトをキーに使用できるようにする必要があります。

ここで、この点を解決する方法は、任意のオブジェクトを整数値にマッピングする方法を用意することです。整数値は、配列のキーとして使用できます。 Javaでは、それがhashCode()が行うことです。だから今、私たちは文字列->文字列辞書を実装しようとすることができます:

_String[] dictionary = new String[DICT_SIZE];
// "a" -> "Hello"
dictionary["a".hashCode()] = "Hello";

// "b" -> "world"
dictionary["b".hashCode()] = "world";

System.out.println(dictionary["b".hashCode()]); // prints world
_

しかし、ねえ、キーとして使用したいオブジェクトがあるが、そのhashCodeメソッドが_DICT_SIZE_以上の値を返す場合はどうなるでしょうか。次に、ArrayIndexOutOfBoundsExceptionが発生しますが、これは望ましくありません。だから、できるだけ大きくしましょう。

_public static final int DICT_SIZE = Integer.MAX_VALUE // Ooops!
_

しかし、それは、たとえ少数のアイテムだけを格納するつもりであっても、配列に膨大な量のメモリを割り当てる必要があることを意味します。したがって、それが最善の解決策になることはありません。実際、私たちはもっとうまくやることができます。任意の_DICT_SIZE_に対して任意の整数を_[0, DICT_SIZE[_の範囲にマップする関数hがあると仮定します。次に、キーオブジェクトのhashCode()メソッドが返すものにhを適用するだけで、基になる配列の境界にとどまることができます。

_public static int h(int value, int DICT_SIZE) {
    // returns an integer >= 0 and < DICT_SIZE for every value.
}
_

その関数はハッシュ関数と呼ばれます。これで、辞書の実装を適応させて、ArrayIndexOutOfBoundsExceptionを回避できます。

_// "a" -> "Hello"
dictionary[h("a".hashCode(), DICT_SIZE)] = "Hello"

// "b" -> "world"
dictionary[h("b".hashCode(), DICT_SIZE)] = "world"
_

しかし、それは別の問題を引き起こします。hが2つの異なるキーインデックスを同じ値にマップするとどうなるでしょうか。例えば:

_int keyA = h("a".hashCode(), DICT_SIZE);
int keyB = h("b".hashCode(), DICT_SIZE);
_

keyAkeyBに同じ値が生成される可能性があり、その場合、誤って配列の値を上書きしてしまいます。

_// "a" -> "Hello"
dictionary[keyA] = "Hello";

// "b" -> "world"
dictionary[keyB] = "world"; // DAMN! This overwrites "Hello"!!

System.out.println(dictionary[keyA]); // prints "world"
_

そうですね、そう言えば、これが決して起こらないような方法でhを実装することを確認する必要があります。残念ながら、これは一般的には不可能です。次のコードについて考えてみます。

_for (int i = 0; i <= DICT_SIZE; i++) {
    dictionary[h(i, DICT_SIZE)] = "dummy";
}
_

このループは、_DICT_SIZE + 1_値(常に同じ値、実際には文字列「ダミー」)を辞書に格納します。うーん、でも配列は_DICT_SIZE_の異なるエントリしか保存できません!つまり、hを使用すると、(少なくとも)1つのエントリが上書きされます。つまり、hは2つの異なるキーを同じ値にマップします!これらの「衝突」は避けられません。n羽の鳩がn-1羽の鳩の穴に入ろうとすると、少なくとも2羽は同じ穴に入らなければなりません。

しかし、私たちにできることは、配列が同じインデックスの下に複数の値を格納できるように実装を拡張することです。これは、リストを使用して簡単に実行できます。したがって、使用する代わりに:

_String[] dictionary = new String[DICT_SIZE];
_

私達は書く:

_List<String>[] dictionary = new List<String>[DICT_SIZE];
_

(補足:Javaはジェネリック型の配列の作成を許可しないため、上記の行はコンパイルされませんが、アイデアは得られます)。

これにより、辞書へのアクセスが次のように変更されます。

_// "a" -> "Hello"
dictionary[h("a".hashCode(), DICT_SIZE)].add("Hello");

// "b" -> "world"
dictionary[h("b".hashCode(), DICT_SIZE)].add("world");
_

ハッシュ関数hがすべてのキーに対して異なる値を返す場合、これにより、それぞれ1つの要素のみを持つリストが作成され、要素の取得は非常に簡単です。

_System.out.println(dictionary[h("a".hashCode(), DICT_SIZE)].get(0)); // "Hello"
_

しかし、一般的にhが異なるキーを同じ整数にマップすることがあることはすでに知っています。このような場合、リストには複数の値が含まれます。検索するには、リスト全体を調べて「正しい」値を見つける必要がありますが、どのように認識しますか?

まあ、値だけを保存する代わりに、常に完全な(key、value)ペアをリストに保存することができます。次に、ルックアップは2つのステップで実行されます。

  1. ハッシュ関数を適用して、配列から正しいリストを取得します。
  2. 取得したリストに格納されているすべてのペアを反復処理します。目的のキーを持つペアが見つかった場合は、ペアから値を返します。

現在、追加と取得は非常に複雑になっているため、これらの操作に対して別々のメソッドを扱うのは無作法ではありません。

_List<Pair<String,String>>[] dictionary = List<Pair<String,String>>[DICT_SIZE];

public void put(String key, String value) {
    int hashCode = key.hashCode();
    int arrayIndex = h(hashCode, DICT_SIZE);

    List<Pair<String,String>> listAtIndex = dictionary[arrayIndex];
    if (listAtIndex == null) {
        listAtIndex = new LinkedList<Pair<Integer,String>>();
        dictionary[arrayIndex] = listAtIndex;
    }

    for (Pair<String,String> previouslyAdded : listAtIndex) {
        if (previouslyAdded.getValue().equals(value)) {
            return; // the value is already in the dictionary;
        }
    }

    listAtIndex.add(new Pair<String,String>(key, value));
}

public String get(String key) {
    int hashCode = key.hashCode();
    int arrayIndex = h(hashCode, DICT_SIZE);

    List<Pair<String,String>> listAtIndex = dictionary[arrayIndex];
    if (listAtIndex != null) {
        for (Pair<String,String> previouslyAdded : listAtIndex) {
            if (previouslyAdded.getKey().equals(key)) {
                return previouslyAdded.getValue(); // entry found!
            }
        }
    }

    // entry not found
    return null;
}
_

したがって、このアプローチが機能するためには、実際には2つの比較操作が必要です。配列内のリストを見つけるhashCodeメソッドです(これは、hashCode()hの両方が高速である場合に高速に機能します)リストを調べるときに必要なequalsメソッド。

これはハッシュの一般的な考え方であり、_Java.util.Map._からputおよびgetメソッドを認識します。もちろん、上記の実装は単純化しすぎていますが、の要点を説明する必要があります。それをすべて。

当然、このアプローチは文字列に限定されず、メソッドhashCode()およびequalsはトップレベルクラスJava.lang.Objectのメンバーであり、すべての種類のオブジェクトに対して機能します。他のクラスはそのクラスを継承します。

ご覧のとおり、2つの異なるオブジェクトがhashCode()メソッドで同じ値を返すかどうかは実際には問題ではありません。上記のアプローチは常に機能します!ただし、hによって生成されるハッシュ衝突の可能性を低くするために、異なる値を返すことが望ましいです。これらは一般に100%回避できないことがわかりましたが、衝突が少なければ少ないほど、ハッシュテーブルはより効率的になります。最悪の場合、すべてのキーが同じ配列インデックスにマップされます。その場合、すべてのペアが1つのリストに格納され、値を見つけると、ハッシュテーブルのサイズに比例したコストの操作になります。

16
Thomas

HashCode()値を使用すると、オブジェクトが格納されているハッシュテーブルバケットへのアドレスとしてハッシュコードを使用することにより、オブジェクトをすばやく見つけることができます。

複数のオブジェクトがhashCode()から同じ値を返す場合、それらは同じバケットに格納されることを意味します。多くのオブジェクトが同じバケットに格納されている場合、特定のオブジェクトを検索するには、平均してより多くの比較操作が必要になることを意味します。

代わりに、equals()を使用して2つのオブジェクトを比較し、それらが意味的に等しいかどうかを確認します。

2
JLund

私が理解しているように、ハッシュコードメソッドの作業は、要素をハッシュするためのバケットを作成することです。これにより、取得が高速化されます。各オブジェクトが同じ値を返す場合、ハッシュを実行する必要はありません。

0
Vishwanath