JavaでequalsメソッドをoverrideするときはhashCodeメソッドもoverrideする
equals()
とhashCode()
についての話です。
Object
クラスにはequals()
とhashCode()
が定義されています。javaの全てのクラスはObject
クラスを継承しているので、この2つのメソッドは暗黙的に定義されています。
equals()
はインスタンスが同一のものかを検証する関数で、hashCode()
はそのインスタンスのハッシュ値を返すメソッドです。
インスタンスが同一のものかを検証するメソッド
String
クラスの場合、equals()
は文字列同士が一致するかどうかにつかわれるように、equals()
はインスタンスの内部のデータメンバー(=アトリビュート)値が等しいかどうかを検証する場合にも利用します。
そのため、自作クラスを作り、そのインスタンスについて、内部の値を比較する場合、equals()
メソッドをoverrideします。このとき、hashCode()
メソッドもかならずoverrideする必要があります。
この辺の話はEffective Java(Joshua Bloch著)に記述されている内容です。
You must override hashCode() in every class that overrides equals(). Failure to do so will result in a violation of the general contract for Object.hashCode(), which will prevent your class from functioning properly in conjunction with all hash-based collections, including HashMap, HashSet, and Hashtable.
equals()
メソッドをoverrideする一方、hashCode()
メソッドをオーバーライドしない場合、ハッシュベースのCollectionが適切に動かなくなるそうです。
これはHashMap
やHashSet
が、Collection内部で値を格納する際に、hashCode()
の値を使っているからです。そしてCollection内部で格納位置を特定するのにもhashCode()
は再利用されます。
上記Collectionでは、ハッシュを使ったデータの取得は以下の2つの過程からなります。
-
-
hashCode()
を使って正しい格納位置をみつける
-
-
- 検索の際は**
equals()
を使って**、どこにあるかを探索する
- 検索の際は**
我々プログラマが、equals()
で同一させるときには、このデータメンバーとこのデータメンバーが同じであれば、同一のインスタンスであるとみなそうと恣意的にデータメンバーを選ぶわけです。そのときhashCode()
も選択したデータメンバーもそれに合わせて同じ値を返さなければなりません
そのとき
hashCode()
も選択したデータメンバーもそれに合わせて同じ値を返さなければなりません。(難しい言葉で書くと、Javaの実装上hashCodeがこうなければならない契約と言うものがあって、その契約のうちequals consistency
を違反することになします。)
こればみたされない場合上記の過程1で、同一のデータとしてみなし、扱いたいデータが別の格納場所に保存されてしまいます。
stackoverflowに貼り付けてあったコードを参考に実装の例をみてみます。
以下のEmployee
クラスは、名前nameと年齢ageをプロパティ(=データメンバー)として持ちます。
そして、名前と年齢が等しいEmployeeインスタンス同士は、同一のインスタンスであるとみなしたい。
そのため、equals()
をoverrideして実装する必要があります。
public class Employee {
String name;
int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof Employee))
return false;
Employee employee = (Employee) obj;
return employee.getAge() == this.getAge()
&& employee.getName() == this.getName();
}
// commented
@Override
public int hashCode() {
int result=17;
result=31*result+age;
result=31*result+(name!=null ? name.hashCode() : 0);
return result;
}
}
このクラスをメイン関数より呼び出し、振る舞いを確認します。
impot java.util.HashSet;
public class ClientTest {
public static void main(String[] args) {
Employee employee = new Employee("rajeev", 24);
Employee employee1 = new Employee("rajeev", 24);
HashSet<Employee> employees = new HashSet<Employee>();
employees.add(employee);
System.out.println(employees.contains(employee1));
System.out.println("employee.hashCode(): " + employee.hashCode()
+ " employee1.hashCode():" + employee1.hashCode());
}
}
hashCode()
が適切にoverrideされている場合には結果は以下のようになり、employee
とemployee1
は同一のインスタンスとして見做されてました。そして二つのインスタンスのhashCodeは同じ値を返します。
true
employee.hashCode(): -938387308 employee1.hashCode():-938387308
一方、hashCode()
がoverrideされてない場合、以下のように同一のインスタンスとみなしたいemployee
とemployee2
がHashSet内部の別々の場所に格納されてしまったようです。当然hashCodeも異なります。
false
employee.hashCode(): 321755204 employee2.hashCode():375890482
ref
equals()のContract
- reflexive: an object must equal itself
- symmetric: x.equals(y) must return the same result as y.equals(x)
- transitive: if x.equals(y) and y.equals(z) then also x.equals(z)
- consistent: the value of equals() should change only if a property that is contained in equals() changes (no randomness allowed)
reflexiveとsymmetricとtransitiveを満たすと言うのは、離散数学の集合で出てくるequivalence relationと同じ性質ですね。 https://en.wikipedia.org/wiki/Equivalence_relation
hashCode()のContract
- internal consistency: the value of hashCode() may only change if a - property that is in equals() changes
- equals consistency: objects that are equal to each other must return the same hashCode
- collisions: unequal objects may have the same hashCode
Discussion