情報科学屋さんを目指す人のメモ

方法・手順・解説を書き残すブログ。私と同じことを繰り返さずに済むように。

Java:hashCodeを実装するときに迂闊にsuper.hashCodeを利用してはいけないというお話

Java (37)

super.hashCodeの注意点についての日本語情報が見当たらなかったので、メモしておきます。

詳しい人が途中まで読んで「そんだけか」とがっかりしないために結論を言ってしまうと、「super.hashCodeがObject#hashCodeだった場合、インスタンスごとに違う値が返って来ちゃって、その値を利用して実装したhashCode実装は、インスタンスごとに違う値を返してしまう」ということです。

こんなことが起こった

HashSetにオブジェクトをaddしたのち、equalsメソッド的に等しいはずのオブジェクトでcontainsを実行してみたところ、なぜか「false」が返ってきました。

equalsの実装は間違ってなさそう…

equalsの実装は間違ってなさそうでした。そこで、よく見るためにLine Breakpointを仕込んでみると、そもそも呼び出されていないことに気が付きました。

HashSetはhashCodeを使う

HashSetは、containsの内部でequalsを用いて比較する前に、とりあえずhashCodeメソッドの戻りとを利用して、等しい可能性があるかをチェックします。hashCodeの値が違っていれば、そもそもequalsを使うまでもなく「等しくない」と判断します

そう、今回は、equalsでは等しいオブジェクトでも、hashCodeの値が食い違っていたのが、containsでfalseが返ってしまう原因でした。

hashCodeの間違い

ここからが本題です。

なぜhashCodeが間違っていたのかを説明するために、間違いのあるhashCodeの実装を、簡単に1行にして書いてみます。

@Override
public int hashCode() {
	return 31 * super.hashCode() + this.name.hashCode();
}

superクラスのhashCodeを利用しつつ、クラスの持つ唯一のフィールドであるname:StringのhashCodeの値を加えていました。

こうすることで、「このオブジェクトが等しいときは、親クラスでhashCodeが等しくて、かつthis.nameのhashCodeが等しいだろう」という考えを表現していました。しかし、これが間違いでした。

ObjectのhashCode実装

実は、このクラスの親クラスはObjectでした。つまり、特に親クラスを指定していなかった状態です。こういうクラスはたくさんあると思います(全ての親クラスがequals・hashCodeを実装していない場合も含む)。

つまり、先ほどの「super.hashCode()」は、「ObjectクラスのhashCode」が呼び出されることになります。

ObjectクラスのhashCodeはJNIを使って実装されておりObject.javaには「public native int hashCode();」としか書かれていないのですが、docコメントにはこう書かれています。

As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.)

Objectのequalsがオブジェクトのアドレス比較(インスタンス一致チェック、==)であることから推測はつくのですが、Object#hashCode()の戻り値は、オブジェクトのアドレスを利用して生成されるため、インスタンスが違えば、hashCode()の値も違うのです。

うかつにsuper.hashCodeを使うと

つまり、うかつにsuper.hashCodeを使ってhashCodeを実装してしまい、なおかつ親クラスがObjectクラスだった場合、(たとえequals的に一致していたとしても、)インスタンスが同じでないと、別のhash値を返してしまうのです。

equalsとhashCode実装の基本

Effective Java の序盤にもあるとおり、equals的に等しいオブジェクトは、等しいhashCodeを返すというルールを守る必要があります。今回はこの原則を守れていなかったわけですが、なんとなくオーバーライドした親クラスのメソッドを呼び出してあげようとして、なんとなくhashCodeメソッドを使ってしまうと、その原則をうっかり破ってしまいかねないというのが今回のポイントです。Javaにそこそこ慣れているほど、このミスはうっかりやってしまいそうな気がします。

参考

コメント(0)

新しいコメントを投稿