web-dev-qa-db-ja.com

Java内のString.hashCode()になぜ多くの競合があるのですか?

なぜString.hashcode()には非常に多くの競合があるのですか?

私はjdk1.6のString.hashCode()を読んでいます、以下はコードです

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

非常に多くの競合があるため、これは非常に混乱します。一意である必要はありませんが(equals()を使用することもできます)、競合が少ないということは、リンクリストのエントリにアクセスしなくてもパフォーマンスが向上することを意味します。

2つの文字があるとすると、方程式の下に一致する2つの文字列が見つかれば、同じhashcode()になります。

a * 31 +b = c * 31 +d

(a-c) * 31 = d-bは、a-c = 1およびd-b = 31を作成するのが簡単な例であると結論付けるのは簡単です。だから私は簡単なテストのために以下のコードを書いた

public void testHash() {
    System.out.println("A:" + (int)'A');
    System.out.println("B:" + (int)'B');
    System.out.println("a:" + (int)'a');

    System.out.println("Aa".hashCode() + "," + "BB".hashCode());
    System.out.println("Ba".hashCode() + "," + "CB".hashCode());
    System.out.println("Ca".hashCode() + "," + "DB".hashCode());
    System.out.println("Da".hashCode() + "," + "EB".hashCode());        
}

結果の下に出力されます。つまり、すべての文字列が同じhashcode()を持ち、ループで簡単に実行できます。

A:65 
B:66
a:97
2112,2112
2143,2143
2174,2174
2205,2205

さらに悪いことに、アルゴリズムに従って、最初の2文字がa2を生成し、2番目の2文字がb2を生成するとします。ハッシュコードは依然としてa2 * 31^2 + b2であるため、a2とb2が2つの文字列間で等しい場合、hashcode()の競合によりさらに多くの文字列が取得されます。そのような例は「AaAa」、「BBBB」などです。次に、6文字、8文字にします......

ほとんどの場合、ASCIIテーブルの文字をハッシュマップまたはハッシュテーブルで使用される文字列で使用すると仮定すると、ここで選択した素数31は明らかに小さすぎます。

1つの簡単な修正は、この競合を回避できるより大きな素数(幸いなことに、257は素数)を使用することです。もちろん、大きすぎる数を選択すると、文字列が非常に長い場合に返されるint値がオーバーフローしますが、ほとんどの場合、キーとして使用される文字列はそれほど大きくないと思いますか?もちろん、これを回避するために長い値を返すこともできます。

以下は、この問題を解決するのに効果的な、値の下に出力されるコードを実行することでこのような競合を簡単に解決できる、betterhash()の私の変更バージョンです。

16802,17028
17059,17285
17316,17542
17573,17799

しかし、なぜjdkはそれを修正しないのですか?どうも。

@Test
public void testBetterhash() {
    System.out.println(betterHash("Aa") + "," + betterHash("BB"));      
    System.out.println(betterHash("Ba") + "," + betterHash("CB"));
    System.out.println(betterHash("Ca") + "," + betterHash("DB"));
    System.out.println(betterHash("Da") + "," + betterHash("EB"));
}

public static int betterHash(String s) {
    int h = 0;
    int len = s.length();

    for (int i = 0; i < len; i++) {
        h = 257*h + s.charAt(i);
    }
    return h;
}
29
hetaoblog

私は58,000の英語の単語(すべて here )をハッシュしました。すべて小文字で、最初の文字を大文字にしました。衝突した数を知っていますか? 2つ:「兄弟」と「テヘラン」(「テヘラン」の代替スペル)。

あなたと同じように、私は可能な文字列のサブドメイン(おそらく私の場合は1つ)を取得して、hashCodeの衝突率を分析しました。可能性のある文字列の任意のサブドメインが私のものよりも最適化するためのより良い選択であると誰が言っているのですか?

このクラスを作成した人々は、ユーザーが文字列をキーとして使用するサブドメインを予測できない(したがって最適化できない)ことを知っている必要があります。したがって、彼らは文字列の全体ドメインに均等に分散するハッシュ関数を選択しました。

興味があれば、これが私のコードです(Guavaを使用しています)。

    List<String> words = CharStreams.readLines(new InputStreamReader(StringHashTester.class.getResourceAsStream("corncob_lowercase.txt")));
    Multimap<Integer, String> wordMap = ArrayListMultimap.create();
    for (String Word : words) {
        wordMap.put(Word.hashCode(), Word);
        String capitalizedWord = Word.substring(0, 1).toUpperCase() + Word.substring(1);
        wordMap.put(capitalizedWord.hashCode(), capitalizedWord);
    }

    Map<Integer, Collection<String>> collisions = Maps.filterValues(wordMap.asMap(), new Predicate<Collection<String>>() {
        public boolean apply(Collection<String> strings) {
            return strings.size() > 1;
        }
    });

    System.out.println("Number of collisions: " + collisions.size());
    for (Collection<String> collision : collisions.values()) {
        System.out.println(collision);
    }

編集する

ちなみに、もし興味があるなら、ハッシュ関数を使った同じテストでString.hashCodeの1と比較して13の衝突がありました。

43
Mark Peters

申し訳ありませんが、このアイデアには冷たい水をかける必要があります。

  1. あなたの分析はあまりにも単純すぎます。あなたはあなたの主張を証明するために設計された文字列のサブセットを厳選したようです。これは、衝突の数がすべての文字列のドメイン全体で予想よりも(統計的に)多いことを示すものではありません。

  2. expectString.hashCodeが衝突のないようにすることは、彼らの正しい心の誰もしません。それは単にそれを念頭に置いて設計されていません。 (非常に衝突のないハッシュが必要な場合は、暗号ハッシュアルゴリズムを使用し、コストを支払います。)String.hashCode()は、すべての文字列のドメイン全体で合理的に適切に設計されています...および速い

  3. あなたがより強力なケースを述べることができると仮定すると、これはそれを述べる場所ではありません。あなたは、この問題を関係者(OracleのJavaエンジニアリングチーム)に提起する必要があります。

  4. Javaエンジニアリングチームは、そのような変更の利点と、それを実装するコスト、およびJavaの他のすべてのユーザーのを比較検討します。 最後のポイントは、おそらくこのアイデアを石で殺すのに十分です。


(「非常に衝突のないハッシュ」は、この回答の目的で私が空から引き出したアイデア/用語です。ただし、申し訳ありません。ただし、要点は、2つの文字列のハッシュコードの衝突の確率は、関連性の程度とは無関係であるべきだということです。たとえば、 "AA"と "bz"は同じ長さで関連していることは明らかです。明らかに、この考えにはもっと考えが必要です。そして、私が話している意味での "関連性"は、測定できない...のようなもの Kolmogorov Complexity 。)

13
Stephen C

ハッシュするとき、衝突は避けられません。 hashCode()メソッドは、同じハッシュコードを持つすべてのオブジェクトのバケットである配列へのインデックスとして使用される整数を返します。 equals(Object)メソッドは、ターゲットオブジェクトをバケット内の各オブジェクトと比較して、完全に一致するオブジェクトが存在する場合はそれを識別するために使用されます。

最終的には、hashCode()メソッドはfastであり、弱すぎない(つまり、衝突が多すぎる)必要があります。ここで、too weakはかなりあいまいなメトリックです。

9
maerics

かなり効率的ですが、シンプルでもあります。 6文字までのすべての可能な小文字(ASCII)ワードまたは6桁までのすべての数字には、一意のhashCode()があります。つまり、hashCodeは31進数のようなものです。大きな数を使用すると、独自の問題があります。すべてのASCII文字の先頭ビットが0であるため、257ファクターは8ビットごとに特にランダムではありません。より大きいファクターは、5桁と6桁/文字の単語のハッシュコードが重複する結果になります。

ハッシュアルゴリズムを変更できない場合、おそらく最も大きな問題は何ですか。どのようなアプローチをとっても、これは非常に悪い選択であり、ユースケースにとって最適ではない可能性があります。

おそらく最大の問題は、サービス拒否攻撃であり、通常は非常にまれな、病理学的なケースを引き起こします。たとえば、Webサーバーを攻撃する方法は、すべて同じハッシュコードを持つキーでキャッシュを埋めることです。毎回計算される0。これにより、HashMapがリンクリストに退化します。

これを回避する簡単な方法は、ハッシュアルゴリズムを不明にすることです。現状では、最善の方法はTreeMapを使用することです(これはカスタム比較をサポートしていますが、この場合はデフォルトで問題ありません)。

1
Peter Lawrey