読者です 読者をやめる 読者になる 読者になる

ひよっこPGのブログ

主に、技術メモや英語たまにギター関連のことも書いているブログです。

JavaでObjectクラスのequalsメソッドをオーバーライドするメリット

すべてのjavaクラスは、equalsメソッドを持っています。
そのequalsメソッドをオーバーライド(上書き)するメリットを自分なりに説明してみます。

説明に使うクラス

/**
 * 人間、一人を表すクラス
 */
class Person {
  /** 名前 */
  private String name;
  /** 年齢 */
  private int age;
	
  /** コンストラクタ */
  public Person(String name, int age) {
    this.name = name;
    this.age  = age;
  }
  // ...各フィールドのgetter/setter
}

オーバーライドしなかった時のデフォルト実装

equalsメソッドをオーバーライドしなかった場合、Objectクラスのequalsメソッドが使われます。

/**
 * Objectクラスのequalsメソッド
 */
public boolean equals(Object obj) {
  return (this == obj);
}

この実装は、単にオブジェクトの参照先(メモリのアドレス値)が同一かのみを判断していて、自分自身のみと等しくなります。
下記が小さなサンプルコードです。
フィールドの値が全て一致していても新しくインスタンスを作れば、equalsメソッドの結果はfalseになります。
オブジェクトの参照先が変わるからです。

Person person1 = new Person("ほげさん", 20);
Person person2 = person1;
Person person3 = new Person("ほげさん", 20);

person1.equals(person2);
//=> true
person1.equals(person3);
//=> false
イメージが分かりやすい参考サイト

JavaにおけるequalsとhashCode - 同一性と同値性の違い

equalsメソッドをオーバーライドするメリット

一つ前で、デフォルト(初期)のequalsメソッドを説明しました。
ではメリットを説明する前に Personクラスのequalsメソッドを名前が一致すれば 同じオブジェクトだ! のように変更しましょう。
イメージとして、10人〜20人規模の名簿を管理しているとして、
その中に同姓同名が存在する可能性は低いので名前が同じであれば同じ人間であると設計しましょう という意味です。
ちなみに、hashCodeメソッドもオーバーライドしていますが、もう少し下の方で説明しています。

class Person {
  // .....

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (!(obj instanceof Person))
      return false;
    Person otherPerson = (Person) obj;
    // nameの値として nullを許容しています。
    return code == null ? otherPerson.getName() == null : code.equals(otherPerson.getName());
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
  }
}

メリットを説明しますね。
仮にList<Person> personListがあるとします。
その中から、名前 = "ほげさん"というPersonが存在するかチェックしたい時に、あなたならどうしますか?
単純に考えると、Listをループして一つずつチェックし一致したら trueを返すと書くかもしれません。
でも、このループ処理は equalsメソッドをオーバーライドしていれば実装する手間が省けます。

List<Person> personList = new ArrayList<>();
personList.add(new Person("テスト001さん", 30));
personList.add(new Person("テスト002さん", 30));
personList.add(new Person("テスト003さん", 30));
personList.add(new Person("テスト004さん", 30));
personList.add(new Person("ほげさん", 20));

// equalsメソッドをオーバーライドしなかった場合
boolean existFlg = false;
for(Person person : personList) {
	if("ほげさん".equals(person.getName())) {
		existFlg = true;
		break;
	}
}		

// equalsメソッドをオーバーライドした場合
Person hogePerson = new Person();
hogePerson.setName("ほげさん");
boolean existFlg = personList.contains(hogePerson);
//=> true

equalsメソッドをオーバーライドして、ArrayListのcontainsメソッドを使うと、存在チェック処理を簡単に書ける。
containsメソッド内でも、同じようにループ処理を行っているんですが、オブジェクトの一致判定にequalsメソッドを使っています。
なので、euqlasメソッドを自分が想定する条件になるようオーバーライドすれば、containsメソッドが利用可能になる。

仮に、オーバーライドせずにcontainsメソッドを使用すると、自分自身のみと等しいことになるので、フィールドが一致していても無視されます。

// equalsメソッドをオーバーライドしなかった場合
Person hogePerson = new Person();
hogePerson.setName("ほげさん");
boolean existFlg = personList.contains(hogePerson);
//=> false

同様に、ArrayListメソッド内で、equalsメソッドに依存しているメソッドがいくつかあります。
indexOf(Object obj)
 name値が"ほげさん"のインデックス番号(List内での順番,要素番号)を取得する。
remove(Object obj)
 name値が"ほげさん"で一番最初にヒットするPersonをListから削除する。

参考サイト

Java SE 7 - ArrayListメソッド一覧 - ArrayList (Java Platform SE 7)

説明した後に、一つ補足です。

List<String> strList = new ArrayList<>();
strList.add("aaa");
strList.add("bbb");
strList.add("ccc");
strList.add("hoge");

strList.contains("hoge");
//=> true

このように、ListにStringやInteger、Dateなど Javaから提供されているクラスを入れる場合は、すでにそのクラスでequalsメソッドをオーバーライドされているので、equalsメソッドのオーバーライドは不要です。
オーバーライドする必要があるのは、自作の値クラスをListに入れた時です(^-^)

equalsメソッドをオーバーライドする時に注意すること。

equalsメソッドをオーバーライドする際には、守らないといけないルールが5つある。
ざっと書くと

  • 反射性 (reflexive): null 以外の参照値 x について、x.equals(x) は true を返します。
  • 対称性 (symmetric): null 以外の参照値 x および y について、y.equals(x) が true を返す場合に限り、x.equals(y) は true を返します。
  • 推移性 (transitive): null 以外の参照値 x、y、および z について、x.equals(y) が true を返し、y.equals(z) が true を返す場合、x.equals(z) は true を返します。
  • 一貫性 (consistent): null 以外の参照値 x および y について、x.equals(y) の複数の呼び出しは、このオブジェクトに対する equals による比較で使われた情報が変更されていなければ、一貫して true を返すか、一貫して false を返します。
  • null 以外の参照値 x について、x.equals(null) は false を返します。

引用 - Object (Java Platform SE 7)

一つずつ、説明すると新たに記事を作成しないといけない内容なので、このルールについて詳しく知りたい方は

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)

この本の"項目8 equalsをオーバーライドする時は一般契約に従う"を参考にすると詳細に書いてあります。
さらに、equalsメソッドをオーバーライドする際は必ず、hashCodeメソッドもオーバーライドが必要です。
equalsメソッドがtrueなら、その比較した2つのオブジェクトが持つhashCodeメソッドの返り値が同じにならなければならない。
そうしないと HashMapなどを使った時に困るからです。
詳しくは、下記のサイトが分かりやすかったです^ー^b
http://education.yachinco.net/tips/java/01/4.html

でも、これは1から全て実装する時に意識する必要がありますが、自分が知っている限りだと自動生成したり、ライブラリに処理を任せたり出来ます。

eclipseの機能で自動生成

getter/setterを自動生成出来るようにequalsメソッドも自動生成出来ます。
f:id:buzzword111:20141109115243p:plain

Apache common langのEqualsBuilderを使う
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

@Override
public boolean equals(Object o) {
  return EqualsBuilder.reflectionEquals(this, o);
}
@Override
public int hashCode() {
  return HashCodeBuilder.reflectionHashCode(this);
}

まとめ

まとめると、コレクションクラス(List, Map, Setなど)に自作の値クラスを入れる場合は、
equalsメソッドをオーバーライドすると、コレクションクラスで提供されているメソッドを利用可能になる。
言い方を変えるなら、コレクションクラスにそのようなクラスを入れる場合は、equalsメソッドのオーバーライドが必須だと言えるでしょう。

ほかに、メリットがないか考えたのですがどうも思いつかず自分が知っている限りのことは、網羅して書いたつもりです。
これ以外で、こういうメリットもあるよ! というのがあれば教えていただければ嬉しいです!