equalsに続き、hashCodeをオーバーライドする場合に守るべき内容について、かなり詳細に記載しています。
記載内容
hashCodeの一般契約
まず、equalsをオーバーライドしているすべてのクラスで、hashCodeをオーバーライドしなければならない。
そうしないと、一般契約を破ることになる。
(一般契約についての説明はObject.hashCode()の通りなのでここでは割愛)
equalsをオーバーライドして、hashCodeをオーバーライドしていない場合、以下の一般契約を破ることになる。
equals(Object)メソッドに従って2つのオブジェクトが等しい場合は、
2つの各オブジェクトに対するhashCodeメソッドの呼出しによって同じ整数の結果が生成される必要があります。
このようなオブジェクトを、HashMapのキーに使用すると、意図せぬ動作になることがある。
実装方法
基本的なhashCodeの実装は以下の様になる。
(PhoneNumberはshortのareaCode、prefix、lineNumをメンバーに持つ)
@Override
public boolean hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
パフォーマンスが重要でない状況であれば、以下の実装でも良い。
@Override
public boolean hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
ただし、この書き方では、可変長引数のための配列生成、基本データ型が含まれる場合はボクシングとアンボクシングが発生するため、
パフォーマンスは良くない。
もし、クラスが不変で、ハッシュコードを計算するコストが高い場合、以下の様にキャッシュすることが出来る。
(スレッドセーフであるように実装する必要が有る)
private int hashCode;
@Override
public boolean hashCode() {
int result = hashcode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
hashCode = result;
}
return result;
}
上記のコードは、ハッシュコードの計算結果とhashCodeフィールドの初期値(上記の場合0)が一致する可能性が低いことが前提になっている。
注意点
ハッシュコードの計算に使用するフィールド
パフォーマンスを向上するために、ハッシュコードの計算から意味のあるフィールドを除外してはならない。
その結果、hashcodeメソッドは速くなるかもしれないが、ハッシュ値の品質がハッシュテーブルで使用できないほど
下がるかもしれない。
例として、Java2より前のStringのhashcodeは、先頭16文字だけを1文字おきに使用してハッシュ値を計算していたため、
URLの様に階層化された大きな文字列をキーにした場合、ハッシュテーブルの検索速度がO(n^2)になっていた
hashCodeが返す値をドキュメント化しない
hashCodekが返す値の詳細な仕様に関するドキュメントを提供するべきではない。
記載した場合、クライアントはその値に依存した処理を記載する可能性がある。
ドキュメントを提供しなければ、値の変更に対する柔軟性を得ることが出来る。
考察
equalsに続いて長い項目ですが、こちらも記載されている内容はhashCodeに関する基本的な内容であり、
特に一般契約については理解しておく必要が有ります。
実装方法による性能の差異
3つの実装方針が記載されていますが、特に気になるのは、
自分で計算するものと、Objects.hashを使用したものとの性能差だと思います。
そこで、以下のクラスA、BのhashCodeをそれぞれの実装方法で実装し、A.hashCodeの速度を比較してみました。
class A {
int n;
String s;
B object;
}
class B {
int n;
String s;
}
結果は、4倍程度の性能差はありましたが、
100万回実行して14ミリ秒と50ミリ秒という数値(CPUは3.5GHz)でしたので、微妙な感はあります。
実処理に組み込んだ場合にGCにどの程度影響するのか等も考慮すると、
それなりに影響するのかもしれませんが、コードの読みやすさとの天秤を考えると悩ましいです。
このレベルの性能差が気になるのであれば、3番目のキャッシュする方式の方が良さそうに思います。
(同じオブジェクトに繰り返しhashCodeを呼ぶ場合に限りますが)
実装方針
equalsでも触れましたが、IntelliJ、Eclipse等のIDEで、equalsメソッドとhashcodeメソッドをセットで生成してくれますので、
これに従うのが良いと思いますし、
もし、何らかの理由で特殊な比較を行う場合は、その旨をコメントで記載するべきです。
やはりRecordの正式導入が待ち遠しいです。