JavaのhashCode()関数とequals()関数のいくつかのピットを浅く分析します

20448 ワード

テキストリンク:https://dzone.com/articles/working-with-hashcode-and-equals-in-java
**注:**この文書はhttps://dzone.com/articles/working-with-hashcode-and-equals-in-java デフォルトではjava.lang.Objectクラスには、equals()とhashCode()の2つの非常に重要な関数があります.2つのオブジェクトを比較する必要がある場合に使用されます.特に、大規模なプロジェクトでは、複数のクラス間のインタラクションが必要な場合、この2つの方法がより重要になります.この論文では,この2つの関数間の関係,彼らのデフォルトの実装方法,および開発者がこの2つの方法を再ロードしなければならない場合について検討する.
JDKでの定義およびデフォルトの実装
  • equals(Object obj):このメソッドはjava.lang.Objectによって提供され、現在のオブジェクトがパラメータオブジェクトと等しいかどうかを判断する役割を果たします.JDKでは、デフォルトでは、メモリアドレスに基づいて2つのオブジェクトが等しいかどうかを判断します.メモリアドレスが同じであれば等しいです.そうでなければ等しくありません.
  • hashCode():このメソッドはjava.lang.Objectによって提供され、このメソッドはオブジェクトのメモリアドレスの整数表現形式を返します.デフォルトでは、メソッドはランダムな整数を返し、各オブジェクトのhashCode()の戻り値は一意です.各オブジェクトにとって、メソッドの戻り値は変わらないが、逆に、この関数を複数回呼び出すたびに、呼び出される戻り値が異なる可能性がある.

  • equals()関数とhashCode()関数の間の規則
    JDKが提供するデフォルトのequals()関数とhashCode()関数の実装は、ビジネスニーズを満たすことができないことが多い.特に、大規模なアプリケーションでは、いくつかのシナリオが発生すると、2つのオブジェクトを等しいと見なすことができる.一部のシナリオでは、メモリアドレスに依存するのではなく、カスタムビジネスロジックに依存するように、開発者はequals()関数とhashCode()関数を再ロードします.
    Java開発文書によれば,判定などのメカニズムを完全に有効にするためには,開発者はequals()関数とhashCode()関数を同時にリロードすべきであり,すなわちequals()関数を実現するだけでは不十分である.
    equals(Object obj)関数が2つのオブジェクトが等しいと判断した場合、この2つのオブジェクトのhashCode()関数は同じ戻り値を持つ必要があります.
    In the following sections, we provide several examples that show the importance of overriding both methods and the drawbacks of overriding equals() without hashcode().
    次の章では、equals()関数とhashCode()関数を同時にリロードすることの重要性を示すとともに、hashCode()関数をリロードせずにequals()関数のみをリロードする欠陥を示す例を示します.
    ケース
    Studentクラスを定義します.
    package com.programmer.gate.beans;
    
    public class Student {
    
        private int id;
        private String name;
        
        public Student(int id, String name) {
            this.name = name;
            this.id = id;
        }
        
        public int getId() {
            return id;
        }
        
        public void setId(int id) {
            this.id = id;
        }
        
        public String getName() {
            return name;
        }
        
        public void setName(String name) {
            this.name = name;
        }
    }
    

    テストを容易にするために、2つのStudioクラスのインスタンス(この2つのインスタンスには同じ属性がある)が私たちが想像していたように等しいかどうかを確認するために、プライマリクラスHashCodeEqualsを定義します.
    public class HashcodeEquals {
    
        public static void main(String[] args) {
            Student alex1 = new Student(1, "Alex");
            Student alex2 = new Student(1, "Alex");
            
            System.out.println("alex1 hashcode = " + alex1.hashCode());
            System.out.println("alex2 hashcode = " + alex2.hashCode());
            System.out.println("Checking equality between alex1 and alex2 = " + alex1.equals(alex2));
        }
    }
    

    出力:
    alex1 hashcode = 1852704110
    alex2 hashcode = 2032578917
    Checking equality between alex1 and alex2 = false
    

    同じプロパティを持つ2つのStudioクラスのオブジェクトを定義しましたが、異なるメモリ領域に格納されます.したがって、デフォルトのequals()関数を使用して2つのオブジェクトが等しいかどうかを判断するとfalseが返されます.hashCode()関数もそうです.インスタンスごとに一意のランダムコードを生成します.
    リロードequals()関数
    実際のビジネスでは、同じIDを持つ2人の学生を等しいと見なしていると仮定します.そのため、equals()関数を次のように再ロードする必要があります.
    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (!(obj instanceof Student))
            return false;
        if (obj == this)
            return true;
        return this.getId() == ((Student) obj).getId();
    }
    

    上記の実装では、2つのStudioオブジェクトが同じメモリに格納されている場合、または同じIDを持っている場合、この2人の学生は等しいと言います.HashcodeEqualsを実行すると、次の出力が得られます.
    alex1 hashcode = 2032578917
    alex2 hashcode = 1531485190
    Checking equality between alex1 and alex2 = true
    

    ご覧のように、独自のビジネスニーズに基づいてequals()関数を再ロードすると、Javaは2つのStudioオブジェクトを比較する際にオブジェクトのID属性を考慮させられます.
    ArrayListのequals()
    equals()関数の非常に広範な使用方法は、ArrayListのオブジェクトを定義し、要素がStudentオブジェクトであり、指定されたStudentオブジェクトを見つけることです.この目的を達成するために、テストクラスのコードを次のように変更します.
    public class HashcodeEquals {
    
        public static void main(String[] args) {
            Student alex = new Student(1, "Alex");
            
            List < Student > studentsLst = new ArrayList < Student > ();
            studentsLst.add(alex);
            
            System.out.println("Arraylist size = " + studentsLst.size());
            System.out.println("Arraylist contains Alex = " + studentsLst.contains(new Student(1, "Alex")));
        }
    }
    

    上記のテストコードを実行すると、次の出力が得られます.
    Arraylist size = 1
    Arraylist contains Alex = true
    

    hashCode()関数の再ロード
    equals()関数を再ロードし,両オブジェクトのhashcodeが異なるとしても,我々が望む結果を得た.では、hashCode()関数を再ロードする必要はありますか?
    HashSetのequals()
    Let’s consider a new test scenario.We want to store all the students in a HashSet,so we update HashcodeEquals as the following:新しいテストシーンを考えてみましょう.私たちは1つのHashSetにすべての学生オブジェクトを格納したいので、HashcodeEqualsのコードを更新しました.
    public class HashcodeEquals {
    
        public static void main(String[] args) {
            Student alex1 = new Student(1, "Alex");
            Student alex2 = new Student(1, "Alex");
            
            HashSet < Student > students = new HashSet < Student > ();
            students.add(alex1);
            students.add(alex2);
            
            System.out.println("HashSet size = " + students.size());
            System.out.println("HashSet contains Alex = " + students.contains(new Student(1, "Alex")));
        }
    }
    

    上記のテストコードを実行すると、次の出力が得られます.
    HashSet size = 2
    HashSet contains Alex = false
    

    待って!equals関数を再ロードしたのではないでしょうか.また、alex 1とalex 2が等しいことを検証しました.そして、HashSetに格納されているのは互いに異なる要素であることを知っています.なぜHashSetはalex 1とalex 2が異なると思っているのでしょうか.
    HashSetは内部の要素をメモリバケツに格納し、各バケツはhashcodeに接続されています.students.add(alex1)を呼び出すと、Javaはalex 1をバケツに格納し、このバケツをalex 1.hashCode()の戻り値に関連付けます.次に、HashSetにオブジェクトを挿入し、そのオブジェクトのhashcodeがalex 1と同じであれば、そのオブジェクトはalex 1を置き換えます.しかしながら、alex 2のhashcodeはalex 1のhashcodeとは異なるため、alex 2は全く異なるオブジェクトと見なされ、別のバケツに格納される.
    HashSetが内部で要素を検索すると、まずこの要素のhashcodeが生成され、その後、このhashcodeに対応するバケツが検索されます.
    これはhashCode()関数の再ロードの重要性を示しているので、StudentクラスのhashCode()関数を再ロードし、StudentのIDを返して、同じIDを持つ2つの学生オブジェクトが同じバケツに格納されます.
    @Override
    public int hashCode() {
        return id;
    }
    

    テストコードを実行すると、次の出力が得られます.
    HashSet size = 1
    HashSet contains Alex = true
    

    hashCode()関数の不思議なところを見たでしょう.この2つの要素は等しいと見なされ、同じメモリバケツに保存されているので、contains()関数を呼び出し、同じhashcodeを持つオブジェクトパラメータを渡すと、HashSetはこの要素を見つけることができます.
    同様に、hashcodeは、HashMap、HashTable、およびハッシュメカニズムを使用して要素を格納するすべてのデータ構造にも適用される.
    まとめ
    徹底的に動作する判定などのメカニズムを実現するためには,equals()関数を重荷するたびにhashCode()関数を重荷する.以下のいくつかの提案に従う限り、カスタマイズされた判断などのメカニズムではBugは永遠に現れません.
  • 2 2 2つのオブジェクトが等しい場合、同じhashcodeを持つ必要があります.
  • 両方のオブジェクトが同じhashcodeを持っている場合、それらが等しいことを意味しません.
  • equals()関数のみを再ロードすると、ハッシュのデータ構造でビジネスが失効します.たとえば、HashSet、HashMap、HashTableなどです.
  • hashCode()関数のみをリロードすると、Javaは2つのオブジェクトを比較するときにメモリアドレスを無視しません.