Help us understand the problem. What is going on with this article?

【Java】equals()メソッド実装に「同一性」のチェックは不要なのか?

はじめに

ご覧いただきありがとうございます.Javaのequals()メソッドについて,同一の場合即座にtrueを返す以下の実装の有り無しで実行時間を比較してみました.

// equals()の冒頭で同一性チェック
if (this == obj){
    return true;
}

いきさつ(長くてすみません)

私は現在SIerの新卒SEということで研修を受けています.Java未経験者前提の内容であるため少し退屈ではありますが楽しくやっております.
(少しでも隙間時間に自分のための勉強もしなければならないという危機感を持っていたりもしますが...)

さて,いま研修では開発演習というものを行っています.作成済みの外部設計書をもとに,単体,結合テスト,内部設計書(コンポーネント仕様書)作成,コーディングを行うといったプロジェクト体験のようなものです.

内部設計ではすでに決められたフィールド名メソッド名をもとに,コーディングできるようメソッドのフローチャートを作成します.
とあるDTOの仕様書に取り掛かるとそこには
「equals()メソッドをオーバーライドしてください」
とありました.あ,これ進●ゼミでやったところだ.

equals()メソッドのオーバーライド

equals()メソッドはObjectクラスで以下のように定義されています.

    // Java 11のソースコードより抜粋
    public boolean equals(Object obj) {
        return (this == obj);
    }

これは単に同一性を返すのみなので自作クラスでは適切に実装する必要があります.この場合の同値性は仕様で決まるので,例えば自作クラスPersonではageフィールドが異なってもnameさえ同値なら同値判定することも(やろうと思えば)自由です.
キャプチャ.PNG

そしてここからがこの記事の本題です.
equals()メソッドではまず最初に同一性をチェックします.

// equals()の冒頭で同一性チェック(再掲)
if (this == obj){
    return true;
}

つまり,a.equals(a)の場合即座にtrueを返します.これは必須ではないのですが,パフォーマンスに影響するので実装するのが通例だとEclipseの自動生成が言っています.これが無いと,aのフィールドがaのフィールドと等しいかを判定する必要があるわけですね.

この処理は何?

equals()メソッドのフローチャートをせっせせっせと作りPM役の講師の方に提出すると,該当の同一性チェック処理の部分にこんなコメントを頂きました.
「この処理は何?」
equals()を実装する際は当然含める処理だと思っていたのでつっこまれるとは思っていませんでした.つっこむにしても,同一性チェックという概念自体は知っていて「今回は小規模なのでここまで厳密な処理は要らないかな」という指摘なら腑に落ちるのですが,「こんな処理は知らない,現場でも見たことがないし返す結果は同じだから意味ないよ」とのことでした.
もちろん私はつい最近まで学生であり現場など全く知らないので,もしかすると同一性チェックなんてこの世に存在しないのでは・・・?と疑心暗鬼になったとともに,これじゃ単に冗長なロジックを書いたただの馬鹿扱いされてしまうと思い,この記事を書いてみました.
とりあえず実行時間によるメリットを実感できれば私の勝ちとします(強引).

環境

Windows10
Eclipse

方法

以下のPerson.javaを使います.本来はObjectクラスのequals()メソッドをオーバーライドしなければならないのですが,比較のために同一性チェックがあるequalsWithSelfCheck()と,同一性チェックが無いequalsWithoutSelfCheck()メソッドを作成して比較しました.どちらもEclipse自動生成のequals()メソッドをもとにしています.

Person.java
public class Person {

    private String name;
    private int age;

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

    public boolean equalsWithoutSelfCheck(Object obj) {
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    public boolean equalsWithSelfCheck(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Person other = (Person) obj;
        if (age != other.age)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }
}

また,以下のMainで条件を同じにして実行時間の差を出力します.10回繰り返して計測しました.

Main.java
public class Main {

    public static void main(String[] args) {
        ArrayList<Person> list = new ArrayList<>();
        Person person = new Person("Taro", 20);

        //全部同一のpersonが入っているリスト
        for(int i = 0; i < 10000000; i++) {
            list.add(person);
        }

        for (int i = 0; i < 10; i++) {
            long time1 = 0;
            long time2 = 0;

            for (Person p : list) {
                long start = System.nanoTime();
                person.equalsWithoutSelfCheck(p);
                long end = System.nanoTime();
                time1 = time1 + end - start;
            }

            for (Person p : list) {
                long start = System.nanoTime();
                person.equalsWithSelfCheck(p);
                long end = System.nanoTime();
                time2 = time2 + end - start;
            }

            System.out.println("実行時間差:" + (time1 - time2) * 0.000001 + "ミリ秒");
        }
    }
}


結果

実行時間差:7.8233999999999995ミリ秒
実行時間差:5.2443ミリ秒
実行時間差:3.8985ミリ秒
実行時間差:4.9727ミリ秒
実行時間差:5.5971ミリ秒
実行時間差:2.7468ミリ秒
実行時間差:10.9687ミリ秒
実行時間差:5.1853ミリ秒
実行時間差:5.4607ミリ秒
実行時間差:3.6744ミリ秒

いずれも同一性チェックを含めたメソッドのほうが速い結果となりました.若干ばらつきはあります.
実行順が原因の可能性もあるため順序を入れ替えて試しましたが,いずれも実行時間差がマイナスになり,同じ結論になりました.

ちなみに

以下のようにリストの中身が同値ではあるが同一でないケースも試してみました.

        //全要素が同値(同一ではない)リスト
        for(int i = 0; i < 10000000; i++) {
            list.add(new Person("Taro", 20));
        }

その結果,以下のように平均した実行時間差はありませんでした.

実行時間差:3.5603ミリ秒
実行時間差:-0.223ミリ秒
実行時間差:1.0935ミリ秒
実行時間差:0.5618ミリ秒
実行時間差:-0.006999999999999999ミリ秒
実行時間差:-1.4681ミリ秒
実行時間差:-0.8628ミリ秒
実行時間差:1.5103ミリ秒
実行時間差:-1.9932999999999998ミリ秒
実行時間差:0.3394ミリ秒

このことから,equals()メソッドにおける同一性チェックはパフォーマンスに十分寄与することが確認できました.

結論

equals()メソッドにおける同一性チェックはパフォーマンスに十分寄与することが確認でき,有用な実装だということがわかりました.(私の勝ち(?))

さいごに

最後までお読みいただきありがとうございました.何かございましたらぜひご指摘,ご指導よろしくお願いいたします.

macaroni10y
社会人です
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away