Javaのequalsメソッドのオーバーライド

職場で、「なんでequalsメソッドのオーバーライドをする必要があるのか?」ときかれたので、メモをつくっておこう。

必須じゃないのはそうなんだが、自分の経験では、equalsメソッドのオーバーライドをしないと、特に、DTOをListなどコレクション系のクラスで処理したいとき困ってしまう。

equalsメソッドの復習

Javaでは、クラスを書くと、デフォルトでjava.lang.Objectクラスを継承することになる。このObjectクラスには、equalsというメソッドがある。そういうわけで、Javaで書いたクラスはequalsメソッドを自動的に継承することになる。

equalsメソッドは、あるオブジェクトともうひとつべつのオブジェクトが等しいかどうかを判定する。Objectクラスのequalsの挙動は「参照が等しいかどうか」でオブジェクトが等しいかどうかを判断する。挙動を変えたい場合は、equalsメソッドをオーバーライドする。

Javadocを読むとなんだか難しく書いてある。
Object (Java Platform SE 6)
equalsメソッドをオーバーライドするときは、hashCodeメソッドも合わせてオーバーライドというのが注意点か。hashCodeメソッドも意識しておかないと、java.util.HashSetとか、Set系のコレクションにDTOをつめこんだとき、やっぱり「あれ?」って挙動になる。

ついでに、Comparable (Java Platform SE 6)メソッドを実装するときも注意なんだよな。

Javaプログラマーの間では、有名な「Effective Javahttp://www.amazon.co.jp/Effective-Java-%E7%AC%AC2%E7%89%88-Joshua-Bloch/dp/489471499Xにもequalsのオーバーライドについて説明されている。

equalsメソッドをオーバーライドしてなくて「あれっ?」とおもう例

自分の経験では、equalsのオーバーライドが必要とおもったのは、
Listの処理をしているときだ。

package note.java;

import static junit.framework.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

public class EqualsTest {

    @Test
    public void testDefaultEquals() {

        TestBeanA a0 = new TestBeanA("name-a0", Integer.valueOf(0));
        TestBeanA a1 = new TestBeanA("name-a1", Integer.valueOf(1));
        TestBeanA a2 = new TestBeanA("name-a2", Integer.valueOf(2));

        //フィールドがa1と同じになるようにする。
        TestBeanA target = new TestBeanA("name-a1", Integer.valueOf(1));

        List<TestBeanA> list = new ArrayList<TestBeanA>();
        list.add(a0);
        list.add(a1);
        list.add(a2);

        //a1とtargetは、フィールドが同じだからきっと、
        //containsはtrueになるはずと思っていると、
        //このassertは期待通りいかない。
        //assertTrue(list.contains(target));
        assertFalse(list.contains(target));
        //このassertは期待通りいく。
        assertTrue(list.contains(a1));

        //あれ?っとおもって、ArrayList#containsのソースコードを見て、
        //TestBeanA#equalsを実行していることがわかって、その挙動を確認してみる。

        //a1とtargetは、フィールドが同じだからきっと、
        //trueになるはずと思っていると、
        //このassertは期待通りいかない。
        //assertTrue(target.equals(a1));
        assertFalse(target.equals(a1));
        //このassertは期待通りいく。
        assertTrue(a1.equals(a1));

        //なぜか?
        //それは、自分の直感と
        //TestBeanA#equals、すなわち、
        //Object#equalsの挙動がちがうから。
    }

}
package note.java;

public class TestBeanA {

    private String name;

    private Integer number;

    public TestBeanA(String name, Integer number) {
        super();
        this.name = name;
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return "TestBeanA [name=" + name + ", number=" + number + "]";
    }

    //java.lang.Object#equals(java.lang.Object)
    //をOverrideしていない。

}

java.lang.Object#equals(java.lang.Object)メソッドは、参照が等しければtrue, そうでなければfalseを返すというもので、実際のプログラムで、equalsを使おうとするとつかいにくい。Javaの理解がすすまないうちは、参照とかインスタンスとかいわれても、イメージがわかないので、なおさら途方にくれる。

equalsメソッドをオーバーライドしてみる。

java.lang.Object#equals(java.lang.Object)メソッドは、等しいかどうかの判定を、浅い範囲でおこなっている。等しいかどうかの判定を、もっと踏み込んで、オブジェクトのフィールドまで見て、深い範囲でおこないたい。そこで、equalsメソッドをオーバーライドする。浅いとか深いとかちょっと語弊があるかもしれないが。

下のTestBeanBでは、Eclipseソースコードの自動生成機能をつかった。

equalsメソッドのJavadocによれば、equalsメソッドをオーバーライドしたときは、hashCodeメソッドも妥当なオーバーライドをしたほうがよい。

余談だが、toStringメソッドもデバッグ用に便利なので、オーバーライドしておくことが多い。

package note.java;

import static junit.framework.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

public class EqualsTest {

    @Test
    public void testOverridedEquals() {

        TestBeanB b0 = new TestBeanB("name-b0", Integer.valueOf(0));
        TestBeanB b1 = new TestBeanB("name-b1", Integer.valueOf(1));
        TestBeanB b2 = new TestBeanB("name-b2", Integer.valueOf(2));

        //フィールドがb1と同じになるようにする。
        TestBeanB target = new TestBeanB("name-b1", Integer.valueOf(1));

        List<TestBeanB> list = new ArrayList<TestBeanB>();
        list.add(b0);
        list.add(b1);
        list.add(b2);

        //このassertは期待通りいく。
        assertTrue(list.contains(target));

        //このassertは期待通りいく。
        assertTrue(target.equals(b1));

    }
}
package note.java;

public class TestBeanB {

    private String name;

    private Integer number;

    public TestBeanB(String name, Integer number) {
        super();
        this.name = name;
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return "TestBeanB [name=" + name + ", number=" + number + "]";
    }

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        TestBeanB other = (TestBeanB) obj;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (number == null) {
            if (other.number != null)
                return false;
        } else if (!number.equals(other.number))
            return false;
        return true;
    }


}

Apache Commons Langを使って、equalsメソッドをオーバーライドしてみる。

equalsメソッドをオーバーライドするために、
Apache Commons Lang
http://commons.apache.org/lang/api-release/org/apache/commons/lang/builder/package-summary.html
を使うというのも、ひとつの手だ。

equalsメソッドのオーバーライドのソースコードは、お決まりだけど、意外と正確に書くのがむずかしい。こいつをつかうと、それがかんたんになる。

具体的には、org.apache.commons.lang.builder.EqualsBuilderには、EqualsBuilderというクラスを使う。これのJavadocを読むと、内部的に、AccessibleObject.setAccessibleを使っていると書いてある。

package note.java;

import static junit.framework.Assert.assertTrue;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

public class EqualsTest {

    @Test
    public void testCommonsLangOverridedEquals() {

        TestBeanC c0 = new TestBeanC("name-c0", Integer.valueOf(0));
        TestBeanC c1 = new TestBeanC("name-c1", Integer.valueOf(1));
        TestBeanC c2 = new TestBeanC("name-c2", Integer.valueOf(2));

        //フィールドがc1と同じになるようにする。
        TestBeanC target = new TestBeanC("name-c1", Integer.valueOf(1));

        List<TestBeanC> list = new ArrayList<TestBeanC>();
        list.add(c0);
        list.add(c1);
        list.add(c2);

        //このassertは期待通りいく。
        assertTrue(list.contains(target));

        //このassertは期待通りいく。
        assertTrue(target.equals(c1));

    }

}
package note.java;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;

public class TestBeanC {

    private String name;

    private Integer number;

    public TestBeanC(String name, Integer number) {
        super();
        this.name = name;
        this.number = number;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getNumber() {
        return number;
    }

    public void setNumber(Integer number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

    @Override
    public boolean equals(Object obj) {
        return EqualsBuilder.reflectionEquals(this, obj);
    }

}

Javaソースコードを調べてみる。

C:\Program Files\Java\jdk1.6.0_22\src.zip
を展開するとJavaソースコード読める。

Eclipseだったら、Javaアプリの起動時に使用するJREとして、JDKを指定すれば、Javaそのもののソースコードは、Javaのエディタの「宣言を開く」で読めるようになるのかな?あるいは、ソースコードの添付をするとか。

コンパイルされて生成されたソースコードでなくて、本物のソースコードを開いていれば、ソースコードのコメントも書いてあるのがわかる。

Ecliseから以下のクラスのequalsメソッドのソースコードを「宣言を開く」で開いて読んで見ると、「なんだ。そういうこと?」ってのがわかる。

Objectクラスのequals

たとえば、Objectクラスのequalsメソッドを見てみると、
return (this == obj);文字通り、参照だけで等しいかどうかを判定している。

Stringクラスのequals

たとえば、Stringクラスのequalsメソッドを見てみると、やっぱりオーバーライドしてある。Stringは、内部的には、charの配列をもっていて、この配列の要素を、ひとつひとつ等しいか判定することで、String(文字列)が等しいかどうかの判定をしている。

Integerクラスのequals

たとえば、Integerクラスのequalsメソッドを見てみると、やっぱりオーバーライドしてある。Integerは、内部的には、intをもっていて、この値が等しいか判定することで、Integerが等しいかどうかの判定をしている。

ArrayListクラスのequals

たとえば、ArrayListクラスのequalsメソッドを見てみると、やっぱりオーバーライドしてある。ArrayListは、内部的には、Iteratorで、比較元と比較先の、ArrayListの要素を取り出して、さらに、取り出した要素のequalsメソッドを実行して、より深い範囲で、ArrayListが等しいかどうかの判定をしている。

これをみると、ArrayListなどの、コレクション系のクラスをまともに使おうとしたら、要素となるクラスはequalsメソッドをオーバーライドせざるを得ない。

「オーバーライドしなくてすんでいる」というひとは、Listをつかうのは、Stringのときだけとか、DTOからgetterでプロパティの値を取り出して、それがStringで、そのequalsを使っているという場合じゃなかろうか。