Javaにおけるequalsメソッドの実装と比較ロジック

クラス階層の最上位にあるObjectクラス

JavaのすべてのクラスはObjectクラスを継承しています。Objectクラスにはequalsメソッドが定義されており、デフォルトの実装では2つのオブジェクトの参照が同一であるかどうかを判定します。これは"=="演算子と同等の動作をします。

Javaの標準ライブラリには、equalsメソッドをオーバーライドしたクラスがあります。これらのクラスでは、参照の比較ではなく、オーバーライドされたルールに基づいて比較が行われます。

例えば、Stringクラスはequalsメソッドをオーバーライドしており、2つの文字列オブジェクトの文字シーケンスが同一であるかどうかを判定します。文字列が同一であればtrueを返し、そうでなければfalseを返します。

カスタムクラスはObjectクラスのequalsメソッドを継承するため、デフォルトではオブジェクトの参照を比較します。

カスタムクラスで2つのオブジェクトの内容が等しいかどうかを比較するには、equalsメソッドをオーバーライドする必要があります。

オーバーライドの例

@Override
public boolean equals(Object other) {
    if (other != null && other.getClass() == this.getClass()) {   //instanceofの代わりにgetClass()を使用することで、サブクラスと親クラスの比較による非対称性を防ぐ
         User user = (User) other;
         if (user.getUsername() == null || username == null) {
             return false;
         }else{
             return username.equals(user.getUsername());
         }
     }
     return false;
}

オーバーライドの要件は以下の通りです

以下の内容はhttps://www.cnblogs.com/1693977889zz/p/7089320.htmlから転載・改変したものです

なぜequals()メソッドをオーバーライドする必要があるのか?

2つのオブジェクトが論理的に等しいかどうかを判定するためです。例えば、クラスのメンバ変数に基づいてクラスのインスタンスが等しいかどうかを判定する場合、Objectクラスから継承したequalsメソッドでは、2つの参照変数が同じオブジェクトかどうかしか判定できません。そのため、equals()メソッドをオーバーライドする必要があります。

重複を許さないコレクションに要素を追加する際、コレクションにはオブジェクトが格納されることが多く、既存のオブジェクトがコレクション内に存在するかどうかを判定する必要があります。この場合、equalsメソッドをオーバーライドする必要があります。

equals()メソッドをどのようにオーバーライドするか?

equalsメソッドをオーバーライドする要件:

  1. 反射性(Reflexivity):nullでない任意の参照xに対して、x.equals(x)はtrueを返すべきです。

  2. 対称性(Symmetry):任意の参照xとyに対して、x.equals(y)がtrueを返す場合、y.equals(x)もtrueを返すべきです。

  3. 推移性(Transitivity):任意の参照x、y、zに対して、x.equals(y)がtrueを返し、y.equals(z)がtrueを返す場合、x.equals(z)もtrueを返すべきです。

  4. 一貫性(Consistency):xとyが参照するオブジェクトに変更がない場合、x.equals(y)を何度呼び出しても同じ結果を返すべきです。

  5. 非null性(Non-nullity):nullでない任意の参照xに対して、x.equals(null)はfalseを返すべきです。

1. 反射性の原則

JavaBeanでは、実際のビジネス要件に基づいて2つのオブジェクトが等しいかどうかを判定するためにequalsメソッドをオーバーライドすることがよくあります。例えば、personクラスを書き、名前に基づいて2つのpersonクラスのインスタンスオブジェクトが等しいかどうかを判定します。コードは以下の通りです:

public class Person {
    private String name;

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

    public String getName() {
        return name;
    }

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

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            Person person = (Person) obj;
            return name.equalsIgnoreCase(person.getName().trim());
        }
        return false;
    }

    public static void main(String[] args) {
        Person p1 = new Person("張三");
        Person p2 = new Person("張三    ");
        List<Person> list = new ArrayList<Person>();
        list.add(p1);
        list.add(p2);
        System.out.println("張三を含むか:" + list.contains(p1));
        System.out.println("張三を含むか:" + list.contains(p2));
    }
}

listに生成されたpersonオブジェクトが含まれているはずで、結果はtrueになるはずですが、実際の結果は:ここでは文字列の空白を考慮し、前後の空白を除去しています。

張三を含むか:true

張三を含むか:false

なぜ2番目はfalseになるのでしょうか?

原因は、listが要素を含むかどうかをチェックする際に、オブジェクトのequalsメソッドを呼び出して判定している点です。つまり、contains(p2)が渡されると、p2.equals(p1)、p2.equals(p2)が順に実行され、どちらかがtrueを返せば結果はtrueになります。しかし、ここでp2.equals(p2)はfalseを返していますか?文字列の前後をトリムしたため、p2.equals(p2)の比較は実際には「張三 」.equals(「張三」)となり、片方に空白があり、もう片方に空白がないためエラーが発生しています。

これはequalsの反射性の原則に違反しています:nullでない任意の参照xに対して、x.equals(x)はtrueを返すべきです。

ここではtrimメソッドを削除するだけで解決します。

2. 対称性の原則

上記の例では十分ではありません。null値を渡すとどうなるでしょうか?以下の文を追加します:Person p2 = new Person(null);

結果:

張三を含むか:true
Exception in thread "main" java.lang.NullPointerException//ヌルポインタ例外

原因は、p2.equals(p1)を実行する際に、p2のnameがnull値であるため、name.equalsIgnoreCase()メソッドを呼び出すとヌルポインタ例外が発生します。

これはequalsメソッドをオーバーライドする際に、対称性の原則に従っていないためです:任意の参照x,yの場合、x.equals(y)がtrueを返す場合、y.equals(x)もtrueを返すべきです。

equalsメソッド内にnull値かどうかの判定を追加するべきです:

@Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            Person person = (Person) obj;
            if (person.getName() == null || name == null) {
                return false;
            }else{
                return name.equalsIgnoreCase(person.getName());
            }
        }
        return false;
    }

3. 推移性の原則

EmployeeクラスがPersonクラスを継承しているとします:

public class Employee extends Person{
    private int id;
  
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public Employee(String name, int id) {
        super(name);
        this.id = id;
    }
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof Employee){
            Employee e = (Employee)obj;
            return super.equals(obj) && e.getId() == id;
        }
        return super.equals(obj);
    }
  
    public static void main(String[] args){
        Employee e1 = new Employee("張三", 12);
        Employee e2 = new Employee("張三", 123);
        Person p1 = new Person("張三");
  
        System.out.println(p1.equals(e1));
        System.out.println(p1.equals(e2));
        System.out.println(e1.equals(e2));
    }
}

名前とIDが両方とも同じ場合にのみ同じ従業員とし、同名同姓の避けます。mainでは、2人の従業員と1人の一般市民を定義しました。名前は同じでも、明らかに同じ人ではありません。実行結果は3つともfalseになるはずです。しかし:

true

true

false

p1はe1に等しく、e2にも等しいのに、e1とe2は等しくないのでしょうか?

p1.equals(e1)は親クラスのequalsメソッドを呼び出して判定しており、instanceofキーワードを使用してe1がpersonのインスタンスであるかをチェックしています。employeeとpersonは継承関係にあるため、結果はtrueになります。しかし、逆の場合は成立せず、e1やe2はp1に等しくありません。これは対称性の原則に違反する典型的な例です。

e1がe2に等しくないのはなぜですか?

e1.equals(e2)はEmployeeのequalsメソッドを呼び出しており、名前が同じであることだけでなく、従業員番号が同じであることも判定します。両方の従業員番号が異なるため、等しくないのは正しいです。しかし、p1がe1に等しく、e2にも等しいのに、e1とe2が等しくないという矛盾があります。これは、等式が推移性を持たないためで、equalsの推移性の原則に違反しています:インスタンスオブジェクトx、y、zの場合、x.equals(y)がtrueを返し、y.equals(z)がtrueを返す場合、x.equals(z)もtrueを返すべきです。

上記の状況が発生するのは、親クラスがinstanceofキーワードを使用して特定のクラスまたはそのサブクラスのインスタンスであるかを判定するためです。これはサブクラスが「抜け穴」を見つけやすい状況を生み出します。

解決策は簡単です。getClassを使用して型を判定します。Personクラスのequalsメソッドを以下のように修正します:

@Override
    public boolean equals(Object obj) {
        if (obj != null && obj.getClass() == this.getClass()) {
            Person person = (Person) obj;
            if (person.getName() == null || name == null) {
                return false;
            }else{
                return name.equalsIgnoreCase(person.getName());
            }
        }
        return false;
}

4. hashCodeメソッドのオーバーライドが必須

equalsメソッドをオーバーライドする場合は、必ずhashCodeメソッドもオーバーライドする必要があります。これはJavaプログラマーなら誰もが知っています。

その理由はHashMapの内部処理メカニズムが配列を使用してマップのエントリを保存しているためです。その鍵は、この配列のインデックス処理メカニズムです:

渡された要素のhashCodeメソッドの戻り値に基づいて配列のインデックスを決定します。その配列位置にすでにマップのエントリがあり、渡されたキーと等しい場合は何もしません。等しくない場合は上書きします。配列位置にエントリがない場合は挿入し、マップのエントリのリストに追加します。同様に、キーの存在確認もハッシュコードに基づいて位置を特定し、キー値を検索します。

では、オブジェクトのhashCodeメソッドは何を返すのでしょうか?

オブジェクトのハッシュコードであり、Objectクラスのネイティブメソッドによって生成され、各オブジェクトにハッシュコードがあることを保証します。

  1. equalsメソッドのオーバーライド例 部分コードはhttp://blog.csdn.net/wangloveall/article/details/7899948を参考にしています

equalsメソッドをオーバーライドする目的は、2つのオブジェクトの内容(内容には、例えば名前と年齢を同時に比較するなど、多くのものがあり、両方とも同じである場合に同じオブジェクトとする)が同じかどうかを判定することです。

equalsをオーバーライドしない場合、比較されるのはオブジェクトの参照が同じメモリアドレスを指しているかどうかです。オーバーライドした後の目的は、2つのオブジェクトの値が等しいかどうかを比較することです。特にequalsを使用して8つの基本データ型のラッパーオブジェクト(int、floatなど)やStringクラス(このクラスはequalsとhashcodeメソッドを既にオーバーライドしている)オブジェクトを比較する場合、デフォルトでは値が比較されます。その他のカスタムオブジェクトを比較する場合は、すべて参照アドレスが比較されます。

package com.example.demo;

class Account {
    private String username;
    private int years;
    
    public int getYears() {
        return years;
    }
    public void setYears(int years) {
        this.years = years;
    }
    public void setUsername(String username) {  
        this.username = username;  
    }
    public String getUsername() {  
        return username;  
    }
    public boolean equals(Object obj) {  
        if(this == obj) {  
            return true;  
        }  
        if(null == obj) {  
            return false;  
        }  
        if(this.getClass() != obj.getClass()) {  
            return false;  
        }  

        Account account = (Account) obj;  
        if(this.username.equals(account.username)&&this.years == account.years) {  
            return true;  
        }
        return false;  
    }  
    
}  

public class TestDemo {  
    public static void main(String[] args) {  
        Account accountA = new Account();  
        accountA.setUsername("田中");
        accountA.setYears(30);

        Account accountB = new Account();  
        accountB.setUsername("田中");
        accountB.setYears(30);

        Account accountC = new Account();  
        accountC.setUsername("佐藤");
        accountC.setYears(30);

        System.out.println("accountA equals accountB:" + accountA.equals(accountB));  
        System.out.println("accountA equals accountC:" + accountA.equals(accountC));
    }  
}  
accountA equals accountB:true
accountA equals accountC:false

Javaでは、なぜequalsメソッドをオーバーライドする際には、必然的にhashCodeメソッドもオーバーライドする必要があるのでしょうか?

理由は以下の通りです。equalsメソッドがオーバーライドされた場合、等しいオブジェクトは等しいハッシュコードを持つ必要があるという規約を維持するために、通常hashCodeメソッドもオーバーライドする必要があります。その規約は以下の通りです:

(1) obj1.equals(obj2)がtrueの場合、obj1.hashCode() == obj2.hashCode()もtrueでなければならない

(2) obj1.hashCode() == obj2.hashCode()がfalseの場合、obj1.equals(obj2)はfalseでなければならない

hashCodeはハッシュデータの高速なアクセスに使用されます。HashSet/HashMap/Hashtableクラスを使用してデータを保存する際は、保存するオブジェクトのハッシュコード値に基づいて同じかどうかを判定します。

これにより、オブジェクトのequalsをオーバーライドした場合、オブジェクトのメンバ変数の値がすべて等しい場合にequalsがtrueを返すように設定しましたが、hashCodeをオーバーライドしない場合、新しいオブジェクトをnewで生成し、元のオブジェクト.equals(新しいオブジェクト)がtrueでも、両方のhashCodeは異なります。これにより、一貫性のない理解が生まれます。

  1. 以下の3つのプログラムを見てみましょう
package com.example.demo;

public class ComparisonTest {
    public static void main(String[] args) {
        int a = 10;
        int b = 10;
        System.out.print("基本型a==b:");
        System.out.println(a == b);
        System.out.println("-----");
        
        String s1 = "abc";
        String s2 = "abc";
        System.out.print("String型s1==s2:");
        System.out.println(s1 == s2);
        System.out.println("-----");
        
        String s3 = new String("abc");
        String s4 = new String("abc");//==はスタックのアドレスが同じかどうかを比較している
        System.out.print("String型new String() s3==s4:");
        System.out.println(s3 == s4);
        System.out.println(s1 == s3);
        System.out.println("-----");
        
        Integer i1 = 1;
        Integer i2 = 1;
        System.out.print("ラッパー型i1==i2:");
        System.out.println(i1 == i2);
        System.out.println("-----");
        
        Integer i3 = 128;
        Integer i4 = 128;//ここでfalseが出力されるのは、Integerが-128〜127の間でキャッシュされ、この範囲を超えるとキャッシュされないため
        System.out.print("ラッパー型i3==i4:");
        System.out.println(i3 == i4);
        System.out.println("-----");
        
        Integer i5 = new Integer("1");
        Integer i6 = new Integer("1");
        System.out.print("ラッパー型new Integer() i5==i6:");
        System.out.println(i5 == i6);//new Integer()を使用する場合はキャッシュされない
        System.out.println("-----");
        
        Data d1 = new Data(1);
        Data d2 = new Data(1);
        Data d3 = d2;
        System.out.print("通常参照型d1 == d2:");
        System.out.println(d1 == d2);
        System.out.println(d2 == d3);//オブジェクトを新規オブジェクトに代入するとアドレスも同じになる
        System.out.println("-----");
    }
}

class Data{
    int value;
    public Data(int value){
        this.value = value;
    }
}
基本型a==b:true
-----
String型s1==s2:true
-----
String型new String() s3==s4:false
false
-----
ラッパー型i1==i2:true
-----
ラッパー型i3==i4:false
-----
ラッパー型new Integer() i5==i6:false
-----
通常参照型d1 == d2:false
true
-----
package com.example.demo;

public class EqualsTest {

    public static void main(String[] args) {
        System.out.println("基本型にはequalsメソッドがない");
        System.out.println("-----");
        
        String s1 = "abc";
        String s2 = "abc";
        System.out.print("String型のequalsメソッド:");
        System.out.println(s1.equals(s2));
        System.out.println("-----");
        
        String s3 = new String("abc");
        String s4 = new String("abc");//equalsメソッドはヒープ内の値が同じかどうかを比較している
        System.out.print("String型new String()のequalsメソッド:");
        System.out.println(s3.equals(s4));
        System.out.println("-----");
        
        System.out.print("Stringの==代入とnew String()代入の比較:");
        System.out.println(s1.equals(s3));
        System.out.println("-----");
        
        Integer i1 = 1;
        Integer i2 = 1;
        System.out.print("ラッパークラスのequalsメソッド:");
        System.out.println(i1.equals(i2));
        System.out.println("-----");
        
        Integer i3 = new Integer(1);
        Integer i4 = new Integer(1);
        System.out.print("ラッパークラスnew Integer()のequalsメソッド:");
        System.out.println(i3.equals(i4));
        System.out.println("-----");
        
        System.out.print("Integerの==代入とnew Integer()代入の比較:");
        System.out.println(i1.equals(i3));
        System.out.println("-----");
    }

}
基本型にはequalsメソッドがない
-----
String型のequalsメソッド:true
-----
String型new String()のequalsメソッド:true
-----
Stringの==代入とnew String()代入の比較:true
-----
ラッパークラスのequalsメソッド:true
-----
ラッパークラスnew Integer()のequalsメソッド:true
-----
Integerの==代入とnew Integer()代入の比較:true
-----
package com.example.demo;

public class ObjectComparison {

    public static void main(String[] args) {
        Member m1 = new Member("鈴木", 25);
        Member m2 = new Member("鈴木", 25);
        Member m3 = new Member();
        Member m4 = new Member();
        Member m5 = m1;
        System.out.print("通常クラスオブジェクトの==デフォルトコンストラクタ以外:");
        System.out.println(m1 == m2);
        System.out.println(m1 == m5);
        System.out.println("-----");
        
        System.out.print("通常クラスオブジェクトのequalsデフォルトコンストラクタ以外:");
        System.out.println(m1.equals(m2));
        System.out.println(m1.equals(m5));
        System.out.println("-----");
        
        System.out.print("通常クラスオブジェクトの==デフォルトコンストラクタ:");
        System.out.println(m3 == m4);
        System.out.println("-----");
        
        System.out.print("通常クラスオブジェクトのequalsデフォルトコンストラクタ:");
        System.out.println(m3.equals(m4));
        System.out.println("-----");
        
        System.out.print("通常オブジェクトのプロパティをequalsで比較:");
        System.out.println(m1.getName().equals(m2.getName()));
        System.out.print("通常オブジェクトのプロパティを==で比較:");
        System.out.println(m1.getName() == m2.getName());
    }
}
class Member{
    public String name;
    public int age;
    public Member(){
        
    }
    public Member(String name, int age){
        this.name = name;
        this.age = age;
    }
    public void display(){
        System.out.println(this.name);
        System.out.println(this.age);
    }
}
通常クラスオブジェクトの==デフォルトコンストラクタ以外:false
true
-----
通常クラスオブジェクトのequalsデフォルトコンストラクタ以外:false
true
-----
通常クラスオブジェクトの==デフォルトコンストラクタ:false
-----
通常クラスオブジェクトのequalsデフォルトコンストラクタ:false
-----
通常オブジェクトのプロパティをequalsで比較:true
通常オブジェクトのプロパティを==で比較:true

上記の3つのプログラムから以下のことがわかります:

  1. ==について:単純な型(intなど)では、このメソッドを使用して比較できます。この型にはequalsメソッドがなく、intの値はスタックに存在し、==はスタックの内容が同じかどうかを比較します。String型では特別で、String="";のような方法で代入する場合、2つの同じ値を==で比較しても同じになります。しかしnew String()を使用して代入すると、同じになりません。これはString=""の場合、Javaがヒープ内に同じ値があるかどうかを確認し、同じ値がある場合は新規オブジェクトのアドレスも古いオブジェクトのアドレスと同じにするため、==の比較も同じになります。しかしnew String()を使用すると2つのスタックが確保されるため、==の比較は同じになりません。ラッパークラスの場合、Integer="";のように代入すると、-128〜127でキャッシュされるため、上記のプログラムをご覧ください。その他の場合はStringと同様です。

  2. equalsについて:String型またはラッパークラス(Integerなど)の場合、ヒープ内の値が比較されます。Integerにもキャッシュの概念はありません。通常のクラスの場合、equalsはメモリの先頭アドレスを比較します。これは==と同じで、両方が同じオブジェクトを指しているかどうかを比較します。詳細はプログラム3をご覧ください。

タグ: Java Objectクラス equalsメソッド オーバーライド 比較ロジック

6月25日 20:05 投稿