もう遠い昔の話だし、今さらどうでもいいことだけど、日本語資料が見当たらなかったので、まとめておく。
実は Java2 (1.2) 以降とそれより前で String.hashCode() のアルゴリズムが異なっている。
現在の hashCode
現在 (1.2 以降) のアルゴリズムは単純にすべての文字に適当な係数をかけて算出する。
public int hashCode()
- この文字列のハッシュコードを返します。
String
のハッシュコードは、次の方法で計算します。s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
int
算術を使います。s[i]
は文字列の i 番目の文字、n
は文字列の長さ、^
は、べき乗を示します。空の文字列のハッシュ値は 0 です。
public int hashCode() { int hashCode = 0; int limit = count + offset; for (int i = offset; i < limit; i++) hashCode = hashCode * 31 + value[i]; return hashCode; }
以前の hashCode
一方、昔 (1.1 以前) のアルゴリズムは JLS1 の 20.12.10 に言語仕様として、明記されているもので現在のものよりもちょっと複雑に見える。
20.12.10
public int
hashCode()
オブジェクトの長さに応じて,このString
オブジェクトのハッシュコードを次のいずれかの方法で計算し,その値を返す。ここで,n を文字の並びの長さ ([http://www.y-adagio.com/public/standards/tr_javalang/javalang.doc.htm#13985:title=20.12.11]) とし,[tex:c_i] をインデクス i の文字とする。
- の場合,ハッシュコードは,
int
演算を使用して で求められる。- の場合,ハッシュコードは,
int
演算を使用して で求められる。ここで,,また であり,文字列から 8 文字又は 9 文字だけをサンプリングする。
これを Java で書くと、以下のようになる。*2
public int hashCode() { int h = 0; int off = offset; char val[] = value; int len = count; if (len < 16) { for (int i = len ; i > 0; i--) { h = (h * 37) + val[off++]; } } else { // only sample some characters int skip = len / 8; for (int i = len ; i > 0; i -= skip, off += skip) { h = (h * 39) + val[off]; } } return h; }
変わった理由
以前のアルゴリズムでは Java のコードを見ればわかるとおり、長い (16 文字以上の) 文字列の場合にすべての文字をハッシュの計算に使用しないことで、衝突が発生しやすかったという問題があったため、現在のようにすべての文字を計算に使用するアルゴリズムに変わったとのこと。
以前のアルゴリズムで衝突する例は Bug ID: 1258091 String.hashCode() produces the same value for too many unequal strings. にある。以下に実際にやってみた例を示す。
public class OldStringHashCode { static public int getOldStringHashCode(String s) { int length = s.length(); int hash = 0; int offset = 0; if (length < 16) { for (int i = length; i > 0; i--) { hash = (hash * 37) + s.charAt(offset++); } } else { int skip = length / 8; for (int i = length; i > 0; i -= skip, offset += skip) { hash = (hash * 39) + s.charAt(offset); } } return hash; } static public void main(String[] args) { String s1 = "abcdefghijklmnopqrstuvwxyz"; String s2 = "a12d34g56j78m9!p@#s$%v^&y*A"; System.out.println("[" + s1 + "] = " + getOldStringHashCode(s1)); System.out.println("[" + s2 + "] = " + getOldStringHashCode(s2)); } }
% javac OldStringHashCode.java % java OldStringHashCode [abcdefghijklmnopqrstuvwxyz] = -1412339411 [a12d34g56j78m9!p@#s$%v^&y*A] = -1412339411 %
*1:http://www.docjar.com/html/api/java/lang/String.java.html からアルゴリズムを実装するコードのみを抜粋した。
*2:ftp://ftp.gwdg.de/pub/languages/java/linux/JDK-1.1.8/i386/v3/jdk118_v3-glibc-2.1.3.tar.bz2 に含まれる src.zip より抜粋した。