たなかのJava日記

どんなことをやったか(学んだか)、どこで詰まったか(わからなかったか)、どこで工夫したかの記録です。

【Java】インスタンスの等価判定(前編)

【今回学ぶこと】
・2つの変数に入っているインスタンスを比較して、等価であるかを正しく判定できるようになること
・等価と等値の違いを理解すること

【等値とは】
完全同一の存在であることです。
つまり同じアドレスであることです。

package jp.co.mtanaka;

public class Sample1 {
    public static void main(String[] args) {
        Person a = new Person("かまいたち", "濱家");
        Person b = a;
        System.out.println(b == a);
    }
}


注目するところは、aとbの2つのアドレスです。
このとき、2つのオブジェクトが同じインスタンス(アドレス)を指していればtrue,
異なるインスタンスであればfalseになります。
内容的に同じ値が入っていたとしてもインスタンスが異なればfalseとなります。

【等価とは】
2つの変数に入っているもの(インスタンス)を比較して「同じ内容」であることです。
同じアドレスを指していなくても良いです。

例えば、以下だと2つのインスタンスaとbの
combination=かまいたち、name=濱家
なので同じ内容と判断できます。
aとbは同じアドレスでなくてもよいです。

Person a = new Person("かまいたち", "濱家"); //combination=かまいたち、name=濱家
Person b = new Person("かまいたち", "濱家"); //combination=かまいたち、name=濱家

【等価判定を行うためには①】
全てのクラスがObjectクラスから継承して備えるequals()を使用します。
equals()メソッドは2つの変数に入っているもの(インスタンス)を比較して「同じ内容」であることを判定するメソッドです。

では、先ほどの例で実施してみます。
aとbのインスタンスはともに「combination=かまいたち、name=濱家」です。
でも、なぜか実施結果はfalseになります。

想定通りの結果にはなりませんでした・・・

package jp.co.mtanaka;

public class Sample1 {
    public static void main(String[] args) {
        Person a = new Person("かまいたち", "濱家");
        Person b = new Person("かまいたち", "濱家");
        System.out.println(a.equals(b)); // 実施結果はfalse
    }
}

【等価判定を行うためには②】
==を用いた等"値"判定はすぐに行えました。
これはJVMからしたら、同じアドレスかどうかを単純に比較すればよいからです。

一方、equals()を用いる等価判定は簡単ではありません。
「2つのインスタンスの何をもって同一と見なすか」という基準はクラスによって違うことがあるためです。
例えば、コンビ名の「かまいたち」と「カマイタチ」を同一の内容と判断する必要があるかもしれません。
その判断基準を持っているのは、私たち開発者であってJVMではないのです。

先ほど、同じ内容であってもeqaulsメソッドを使用した場合はfalseになりましたが、
実は、Objectクラスに宣言されているequals()の中身は「ただの等値判定」ロジックになっているためです。
要は同じアドレスかどうかを判定しているのです。

これは、equals()が呼び出されたらJVMは何かしらの結果を返さなければなりません。
ですが、「2つのインスタンスの何をもって同一とみなすか」という基準はクラスによって違うことがあるため、
「等値であれば等価である」、もう少し砕いた言い方だと、
「2つの変数のインスタンスが同じアドレスであれば同じ内容」であると言わざるを得ない苦肉の策とのことです。
※諸説あり

【equals()を使用して正しく等値判定を行うためには】
繰り返しになりますが、「何をもってインスタンス同士を等価(同じ内容)と見なしてよいか」を判断するのはそのクラスの開発者だけです。
toString()同様、自分でクラスを開発したらそのクラスのequals()をオーバーライドする必要があります。
これにより、私たちが適切と考える等価判定アルゴリズムJVMに伝えることができます。

equals()の中身に記述する処理内容は様々ですが、定石とされる書き方を以下に記します。

package jp.co.mtanaka;

public class Sample1 {
    public static void main(String[] args) {
        Person a = new Person("かまいたち", "濱家");
        Person b = new Person("かまいたち", "濱家");
        Person c = b;
        System.out.println(b == a);     //等値判定、同じアドレスか⇒false
        System.out.println(c == b);     //等値判定、同じアドレスか⇒true
        System.out.println(a.equals(b));//等価判定、同じ内容(コンビ名と名前)のインスタンスか⇒true
        System.out.println(a);          //お笑い芸人(コンビ名=かまいたち/名前=濱家さん)
    }
}
package jp.co.mtanaka;

public class Person {
    private String combination;
    private String name;

    public Person(String combination, String name) {
        this.combination = combination;
        this.name = name;
    }

    // equals()をオーバーライドして、2つのインスタンスの何をもって同一と見なすかを決めて判定する
    @Override
    public boolean equals(Object obj) {
        /*
         自分自身が引数として渡されたら無条件でtrueを返す
         この場合の自分自身(this)とはPerson aのインスタンスを示す
         参考ページ:https://oshiete.goo.ne.jp/qa/6405286.html
        */
        if (this == obj) {
            return true;
        }

        // nullが引数で渡されたらfalseを返す
        if (obj == null) {
            return false;
        }

        // obj instanceof Personで左辺は右辺の型と同じかを判断
        // !で左辺は右辺と違う型だったらfalseを返す
        if(!(obj instanceof Person)) {
            return false;
        }

        // 次の処理に備えて比較ができるように適切にキャストする
        Person p = (Person) obj;

        //この場合のthisはPerson aのインスタンスを示す
        // aとbのインスタンスのコンビ名が等価(同じ内容)で無ければfalseを返す
        // Stringクラスのequalsメソッドで文字列の値が等しいか判定しています
        if (!this.combination.equals(p.combination)) {
            return false;
        }

        // aとbのインスタンスのnameが等価(同じ内容)で無ければfalseを返す
        // Stringクラスのequalsメソッドで文字列の値が等しいか判定しています
        if (!this.name.equals(p.name)) {
            return false;
        }
        return true;
    }

    // toString()をオーバーライドして、適切な文字列表現を返すように上書き
    @Override
    public String toString() {
        return "お笑い芸人(コンビ名=" + this.combination + "/名前=" + this.name + "さん)";
    }
}


【まとめ】
・等値判定について
==を用いれば、等値判定はすぐに行うことができます。
これはJVMからしたら、同じアドレスかどうかを単純に比較すればよいからです。

・等価判定について
「何をもってインスタンス同士を等価(同じ内容)と見なしてよいか」を判断するのはそのクラスの開発者だけです。
そのため、toString()同様、自分でクラスを開発したらそのクラスのequals()をオーバーライドする必要があります。
これにより、私たちが適切と考える等価判定アルゴリズムJVMに伝えることができます。
また、オーバーライドには定石があるので、調べながら対応できれば良い。
ちなみに「instanceofは左辺値がnullなら常にfalseなので、その前のif文は省略することも可能です」