JavaでequalsメソッドをoverrideするときはhashCodeメソッドもoverrideする

5 min read読了の目安(約4600字

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が適切に動かなくなるそうです。

これはHashMapHashSetが、Collection内部で値を格納する際に、hashCode()の値を使っているからです。そしてCollection内部で格納位置を特定するのにもhashCode()は再利用されます。

上記Collectionでは、ハッシュを使ったデータの取得は以下の2つの過程からなります。

    1. hashCode()を使って正しい格納位置をみつける
    1. 検索の際は**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されている場合には結果は以下のようになり、employeeemployee1は同一のインスタンスとして見做されてました。そして二つのインスタンスのhashCodeは同じ値を返します。

true
employee.hashCode():  -938387308  employee1.hashCode():-938387308

一方、hashCode()がoverrideされてない場合、以下のように同一のインスタンスとみなしたいemployeeemployee2が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