banner
lMingyul

lMingyul

记录穿过自己的万物
jike

この2つのオブジェクトは同じですか

ビジネスコードを書くとき、私たちはしばしば 2 つの Java オブジェクトが同じかどうかを判断する必要があります。オブジェクトが等しいかどうかを判断する一般的な方法は、==、equals、hashcode の 3 つのメソッドを使用することです。本記事では、これら 3 つの使い方を明らかにしようとしています。

関係演算子 ==#

== を使用して 2 つのオブジェクトが等しいかどうかを判断します。これは、これら 2 つのオブジェクトのアドレスが等しいかどうかを判断します。

サンプルコード:

public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        Person tom = new Person();
        Person bob = jack;

        System.out.println("jack == tom ? " + (jack == tom));
        System.out.println("jack == bob ? " + (jack == bob));
      
        System.out.println("jack アドレス: " + jack);
        System.out.println("tom アドレス: " + tom);
        System.out.println("bob アドレス: " + bob);
    }
}

出力結果:

jack == tom ? false
jack == bob ? true
  
jack アドレス: com.mingyu.javalearn.Person@1b2c6ec2
tom アドレス: com.mingyu.javalearn.Person@4edde6e5
bob アドレス: com.mingyu.javalearn.Person@1b2c6ec2

jack と tom は同じ Person を new で作成していますが、新しく作成された Person のオブジェクトアドレスは異なります。したがって、jack は tom と等しくありません。印刷されたアドレスの結果から、jack と bob のオブジェクトアドレスは同じであることがわかります。したがって、彼らの比較結果は true です。


equals() メソッド#

Java のすべてのクラスは Object という親クラスを持ち、各クラスはこのクラスのメソッドを継承します。これには equals() メソッドも含まれます。

equals () の本質#

親クラスのメソッドを継承した後、子クラスは親クラスの同名メソッドをオーバーライドできます。equals() メソッドをオーバーライドしていない場合、equals メソッドを使用して 2 つのオブジェクトが等しいかどうかを判断することは、上記で述べた == 演算子を使用して判断するのと同じ結果になります。判断されるのは、これら 2 つのオブジェクトのアドレスが等しいかどうかです。

サンプルコード:

public class Test {

    public static void main(String[] args) {
        Person jack = new Person();
        Person tom = new Person();
        Person bob = jack;

        System.out.println("jack == tom ? " + (jack.equals(tom)));
        System.out.println("jack == bob ? " + (jack.equals(bob)));
    }
}

出力結果:

jack == tom ? false
jack == bob ? true

実際、equals () メソッドの底層ソースコードを見ると、実際には == を使用して 2 つのオブジェクトを判断しています。

public boolean equals(Object obj) {
      return (this == obj);
}

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

時には、2 つのオブジェクトのアドレスが同じかどうかを比較するだけでなく、2 つのオブジェクトの内容が同じかどうかを判断する必要があります。この場合、equals メソッドをオーバーライドする必要があります。

以下のように equals をオーバーライドできます。

@Override
public boolean equals(Object o) {
    // 同じオブジェクトの参照かどうかをチェックし、そうであれば true を返す
    if (this == o) {
      return true;
    }
    // 同じ型かどうかをチェックし、そうでなければ false を返す
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    // Object オブジェクトをキャスト
    Person person = (Person) o;
    // オブジェクトの内容が等しいかどうかを判断します。ここでは name の内容を判断します。
    return Objects.equals(name, person.name);
}

このようにオブジェクトの判断が異なります。今回はオブジェクトの内容が同じかどうかを判断します。

public class Test {

    public static void main(String[] args) {
        Person jack = new Person("jack");
        Person tom = new Person("jack");

        System.out.println("jack == tom ? " + (jack.equals(tom)));
        System.out.println("jack アドレス: " + jack);
        System.out.println("tom アドレス: " + tom);
    }
}

出力結果:

jack == tom ? true
jack アドレス: com.mingyu.javalearn.Person@1b2c6ec2
tom アドレス: com.mingyu.javalearn.Person@4edde6e5

jack オブジェクトインスタンスと tom オブジェクトインスタンスはオブジェクトアドレスが異なりますが、equals () メソッドをオーバーライドしたため、比較の重点がオブジェクトの内容に置かれたため、比較結果は true です。

他の equals () オーバーライドの書き方#

多くの他のパッケージも equals() をオーバーライドしています。

Apache Commons Lang フレームワーク#

@Override
public boolean equals(Object o) {
    // 同じオブジェクトの参照かどうかをチェックし、そうであれば true を返す
    if (this == o) {
      return true;
    }
    // 同じ型かどうかをチェックし、そうでなければ false を返す
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    // Object オブジェクトをキャスト
    Person person = (Person) o;
    // オブジェクトの内容が等しいかどうかを判断します。ここでは name の内容を判断します。
    return new EqualsBuilder().append(name, person.name).isEquals();
}

equals と == の違い#

  • 基本型に対して、== は 2 つの値が等しいかどうかを判断します。基本型には equals () メソッドはありません。
  • 参照型に対して、== は 2 つの変数が同じオブジェクトを参照しているかどうかを判断しますが、equals () はオーバーライドされていない場合、== と同じで、オーバーライドされた場合は参照しているオブジェクトの内容が同じかどうかを判断します。

hashCode() メソッド#

hashCode も Object クラスで定義されたメソッドで、2 つのオブジェクトが等しいかどうかを比較することもできます。このメソッドの戻り値は、オブジェクトのハッシュ値を呼び出すもので、このハッシュ値の型は int です。

hashCode () の実装#

Object クラスのソースコードを見ると、hashCode () メソッドには具体的な実装がないことがわかります。これはネイティブメソッドで、C 言語で実装されています。このメソッドが返すハッシュ値は、オブジェクトのメモリアドレスを整数に変換することによって得られます。

public native int hashCode();

なぜ hashCode () メソッドが必要なのか#

上記で述べた equals メソッドはオブジェクト間の等しさを判断できますが、なぜ hashCode メソッドが必要なのでしょうか?

ソースコードのコメントにはこの理由が記載されています:

  • このメソッドをサポートすることで、ハッシュテーブルが恩恵を受けることができます。例えば、java.util.HashMap が提供するハッシュテーブルです。

私たちは HashSet や HashMap などのコレクションクラスが要素をコレクションに追加する際に、常に操作を行うことを知っています:現在追加しようとしているオブジェクトが現在のコレクションにすでに格納されているかどうかを判断します。このとき、オブジェクト間の比較が関与します。

HashSet と HashMap はどちらも hashCode () メソッドを使用してオブジェクトが格納される位置を計算します。したがって、これらのコレクションクラスにオブジェクトを追加する前に、格納されるキーの hashCode 値を求める必要があります。

以下は HashMap ソースコードにおけるキーの hashCode 値を求めるメソッドです。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

なぜ hashCode メソッドを使用するのか、equals メソッドを使用しないのか?

理由は、equals メソッドの呼び出しはより時間がかかるからです。

以下は実験です。

public class Test {

    public static void main(String[] args) {
        // 開始時間
        long stime = System.currentTimeMillis();

        // 実行時間を計算
        Person jack = new Person();
        Person tom = new Person();
        System.out.println("jack == tom ? " + (jack.equals(tom)));
        
        // 終了時間
        long etime = System.currentTimeMillis();
        System.out.printf("equals() 実行時間:%d ミリ秒.", (etime - stime));

        System.out.println();
        System.out.println("========== 明らかな区切り線 =========");
        
        // 開始時間
        stime = System.currentTimeMillis();

        // 実行時間を計算
        Person mingyu = new Person();
        Person bob = new Person();
        System.out.println("mingyu == bob ? " + (mingyu.hashCode() == bob.hashCode()));

        // 終了時間
        etime = System.currentTimeMillis();
        System.out.printf("hashCode() 実行時間:%d ミリ秒.", (etime - stime));
    }
}

出力結果:

jack == tom ? true
equals() 実行時間:3 ミリ秒.
========== 明らかな区切り線 =========
mingyu == bob ? false
hashCode() 実行時間:0 ミリ秒.

出力結果から、hashCode メソッドの呼び出しはほとんど時間がかからないことがわかります。なぜなら、本質的には 2 つの int 型値を比較しているからです。したがって、hashCode メソッドを使用して 2 つのオブジェクトを比較するのは非常に速いです。

なぜ equals () メソッドが必要なのか#

hashCode メソッドがこれほど速いので、なぜ equals メソッドが必要なのでしょうか?それは、hashCode メソッドには限界があるからです。

限界:2 つのオブジェクトの hashCode 値が等しいからといって、2 つのオブジェクトが等しいとは限りません

これは、hashCode 値がハッシュ関数によって計算されるためです。一般的な計算プロセスは、配列の長さに対してモジュロを取ることです。この配列はメモリ配列であったり、集合配列であったりします。長さが有限であるため、毎回計算されるハッシュ値が異なることを避けることは難しく、"ハッシュ衝突" が発生する可能性があります。悪化したハッシュアルゴリズムは衝突を引き起こしやすくなります。

ハッシュ衝突は、2 つのオブジェクトが等しいかどうかを比較する際に影響を与えます。つまり、異なる 2 つのオブジェクトのハッシュ値が同じである可能性もあります。

したがって、hashCode メソッドだけでは不十分で、equals メソッドを使用してさらに判断する必要があります。

HashSet が要素をコレクションに追加する過程では、これら 2 つのメソッドが同時に使用されます。

  • 要素を追加する手順:
    • オブジェクトの hashCode 値を計算して、オブジェクトが追加される位置を判断します。
    • 他のすでに追加されたオブジェクトの hashCode 値と比較します。
      • 同じ hashCode 値が存在しない場合、オブジェクトは現在のコレクションに存在しないことが証明されます。2 つの等しいオブジェクトの hashCode 値は必ず等しいです。
      • 同じ hashCode 値が存在する場合、equals メソッドを使用してさらに判断します。
    • オブジェクトが現在のコレクションに存在しない場合、オブジェクトをコレクションに格納します。

このプロセスは、最初に hashCode メソッドを使用して判断することで、すでにコレクションに存在する要素が equals メソッドを呼び出す回数を減らし、プログラムの実行速度を向上させます。

equals と hashCode の関係#

Effective Java』には次のように書かれています:

equals をオーバーライドする際は、必ず hashCode をオーバーライドすること

equals メソッドをオーバーライドしたクラスでは、必ず hashCode メソッドもオーバーライドしなければなりません。

もし equals のみをオーバーライドし、hashCode をオーバーライドしなかった場合、このクラスのオブジェクトがハッシュベースのコレクション(HashMap、HashSet、Hashtable を含む)に要素として追加されると問題が発生します。

サンプルコード:

public class Test {

    public static void main(String[] args) {

        Person person1 = new Person("jack");
        Person person2 = new Person("jack");
        HashSet<Person> set = new HashSet<>();
        set.add(person1);
        set.add(person2);

        System.out.println("person1 equals person2: " + person1.equals(person2));
        System.out.println("person1.hashCode == person2.hashCode: " + (person1.hashCode() == person2.hashCode()));
        System.out.println("person1 hashCode: " + person1.hashCode());
        System.out.println("person2 hashCode: " + person2.hashCode());

        System.out.println("set コレクションの要素数: " + set.size());
        for (Person person : set) {
            System.out.println("person: " + person.getName());
        }
    }
}

出力結果:

person1 equals person2: true
person1.hashCode == person2.hashCode: false
person1 hashCode: 455896770
person2 hashCode: 1323165413
set コレクションの要素数: 2
person: jack
person: jack

hashCode メソッドをオーバーライドしなかったため、オブジェクトを作成するたびに、Object クラスの hashCode メソッドが呼び出されます。このメソッドは異なるハッシュ値を生成します。

前述のように、HashSet はオブジェクトが現在のコレクションに存在するかどうかを判断する際にオブジェクトのハッシュ値を使用します。したがって、equals メソッドと hashCode の比較結果が異なるため、HashSet には私たちが重複していると考える 2 つのオブジェクトが存在することになります。

したがって、equals メソッドをオーバーライドする際には、必ず hashCode メソッドもオーバーライドする必要があります。

では、hashCode メソッドをどのようにオーバーライドすればよいのでしょうか?

  • オーバーライド時に遵守すべき原則:
    • オブジェクトの equals メソッドで比較に使用される情報が変更されていない場合、同じオブジェクトの hashCode メソッドを複数回呼び出すと、常に同じ値を返す必要があります。
    • 2 つのオブジェクトが equals メソッドで比較されて等しい場合、これら 2 つのオブジェクトの hashCode メソッドは同じ結果を返す必要があります。
public class Person {

    private String name;

    @Override
    public boolean equals(Object o) {
        // 同じオブジェクトの参照かどうかをチェックし、そうであれば true を返す
        if (this == o) {
            return true;
        }
        // 同じ型かどうかをチェックし、そうでなければ false を返す
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        // Object オブジェクトをキャスト
        Person person = (Person) o;
        // オブジェクトの内容が等しいかどうかを判断します。ここでは name の内容を判断します。
        return new EqualsBuilder().append(name, person.name).isEquals();
    }

    @Override
    public int hashCode() {
      // name フィールドのハッシュ値を返します
        return Objects.hash(name);
    }
}

出力結果:

person1 equals person2: true
person1.hashCode == person2.hashCode: true
person1 hashCode: 3254270
person2 hashCode: 3254270
set コレクションの要素数: 1
person: jack

オーバーライド後、2 つのオブジェクトの hashCode 値は同じになり、set コレクションの要素数も私たちが期待した結果:1 つの要素になります。


参考資料#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。