JavaでequalsとhashCodeをオーバーライドするときに考慮すべき問題は何ですか?


回答:


1439

理論(言語弁護士および数学的に傾倒している人向け):

equals()javadoc)は同値関係を定義する必要があります(再帰的対称的推移でなければなりません)。さらに、一貫性がなければなりません(オブジェクトが変更されていない場合は、同じ値を返し続ける必要があります)。さらに、o.equals(null)常にfalseを返す必要があります。

hashCode()javadoc)も一貫している必要があります(オブジェクトがに関して変更されていない場合equals()、同じ値を返し続ける必要があります)。

2つの方法の関係は次のとおりです。

いつでもa.equals(b)a.hashCode()と同じでなければなりませんb.hashCode()

実際には:

一方をオーバーライドする場合は、もう一方をオーバーライドする必要があります。

計算equals()に使用するのと同じフィールドセットを使用して計算しhashCode()ます。

Apache Commons Langライブラリーの優れたヘルパークラスであるEqualsBuilderおよびHashCodeBuilderを使用してください。例:

public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
       if (!(obj instanceof Person))
            return false;
        if (obj == this)
            return true;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

また覚えておいてください:

HashSetLinkedHashSetHashMapHashtableWeakHashMapなどのハッシュベースのコレクションまたはマップを使用する場合は、オブジェクトがコレクション内にある間、コレクションに入れたキーオブジェクトのhashCode()が変更されないようにしてください。これを確実にする完全な方法は、キーを不変にすることです。これには他の利点もあります


12
appendSuper()に関する追加のポイント:スーパークラスの等価動作を継承する場合にのみ、hashCode()およびequals()で使用する必要があります。たとえば、Objectから直接派生した場合、デフォルトではすべてのObjectが異なるため、意味がありません。
Antti Kissaniemi、2009年

312
Eclipseで2つのメソッド(ソース> hashCode()およびequals()を生成)を生成できます。
RokStrniša

27
同じことはNetbeansのも同様である。developmentality.wordpress.com/2010/08/24/...
seinecle

6
@Darthenius Eclipseで生成されたequalsはgetClass()を使用するため、場合によっては問題が発生する可能性があります(効果的なJavaアイテム8を参照)
AndroidGecko

7
instanceof最初のオペランドがnullの場合にfalse を返すという事実を考えると、最初のnullチェックは必要ありません(再び有効なJava)。
izaban

295

HibernateのようなObject-Relationship Mapper(ORM)を使用して永続化されているクラスを扱っている場合、これがすでに不当に複雑であると思わなかった場合は、注目に値するいくつかの問題があります。

遅延読み込みされたオブジェクトはサブクラスです

オブジェクトがORMを使用して永続化されている場合、多くの場合、動的プロキシを処理して、データストアからオブジェクトが早くロードされないようにします。これらのプロキシは、独自のクラスのサブクラスとして実装されます。これは、this.getClass() == o.getClass()が戻ることを意味しますfalse。例えば:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

ORMを扱っている場合、o instanceof Person正しく動作するのは使用することだけです。

遅延読み込みされたオブジェクトにはnullフィールドがあります

ORMは通常、ゲッターを使用して、遅延ロードされたオブジェクトを強制的にロードします。これは、ロードが強制されて"John Doe"が返された場合でも、遅延ロードされた場合であることperson.nameを意味します。私の経験では、これはとでより頻繁に発生します。nullpersonperson.getName()hashCode()equals()

ORMを扱う場合は、常にゲッターを使用し、とでフィールド参照を使用しないようにしhashCode()てくださいequals()

オブジェクトを保存すると状態が変わります

永続オブジェクトは、idフィールドを使用してオブジェクトのキーを保持することがよくあります。このフィールドは、オブジェクトが最初に保存されるときに自動的に更新されます。でidフィールドを使用しないでくださいhashCode()。ただし、で使用できますequals()

私がよく使うパターンは

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

しかし:あなたは含めることはできませんgetId()の中でhashCode()。その場合、オブジェクトが永続化されると、そのオブジェクトがhashCode変更されます。オブジェクトが内にある場合は、HashSet二度と「見つかりません」。

私のPerson例では、おそらくgetName()for hashCodegetId()plus getName()(単なるパラノイアのために)を使用しequals()ます。の「衝突」のリスクがある場合は問題ありhashCode()ませんが、の場合は問題ありませんequals()

hashCode() 変更されないプロパティのサブセットを使用する必要があります equals()


2
@Johannes Brodwall:わかりませんSaving an object will change it's statehashCode戻る必要intがあるので、どのように使用しgetName()ますか?あなたのための例を与えることができますhashCode
jimmybondy

@jimmybondy:getNameは、使用可能なhashCodeを持つStringオブジェクトを返します
mateusz.fiolka

85

についての説明obj.getClass() != getClass()

このステートメントは、equals()継承が非友好的であることの結果です。JLS(Java言語仕様)であればあることを指定しA.equals(B) == true、その後B.equals(A)も返さなければなりませんtrue。このステートメントを省略すると、オーバーライドするequals()(およびその動作を変更する)クラスを継承するクラスは、この仕様に違反します。

ステートメントが省略された場合の次の例を考えてみます。

    class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }    

やってnew A(1).equals(new A(1))も、new B(1,1).equals(new B(1,1))それが必要として、真配るなります。

これはすべて非常によく見えますが、両方のクラスを使用しようとするとどうなるかを見てください。

A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

明らかに、これは間違っています。

対称状態を確保したい場合。a = b(b = aの場合)とLiskov置換原理super.equals(other)は、Bインスタンスの場合だけでなく、たとえば後でチェックしAます。

if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other)); 
if (other instanceof A) return super.equals(other); 
   else return false;

どちらが出力されます:

a.equals(b) == true;
b.equals(a) == true;

ここで、aがの参照でない場合、それはBクラスの参照である可能性がありますA(拡張するため)。この場合は、も呼び出しsuper.equals() ます


2
この方法でイコールを対称にすることができます(スーパークラスオブジェクトとサブクラスオブジェクトを比較する場合は、常にサブクラスのイコールを使用してください)if(obj.getClass()!= this.getClass()&& obj.getClass()。isInstance(this) )return obj.equals(this);
pihentagy 2010

5
@pihentagy-実装クラスがequalsメソッドをオーバーライドしないと、stackoverflowが発生します。楽しくない。
Ran Biron

2
スタックオーバーフローは発生しません。equalsメソッドがオーバーライドされていない場合は、同じコードを再度呼び出しますが、再帰の条件は常にfalseになります。
Jacob Raihle 2013

@pihentagy:2つの異なる派生クラスがある場合、それはどのように動作しますか?場合ThingWithOptionSetAに等しくすることができるThingすべての余分なオプションはデフォルト値があり、同様にのためにすることを提供ThingWithOptionSetBするために、それは可能なはずThingWithOptionSetAに等しいとされるようにThingWithOptionSetB両方のオブジェクトのすべての非塩基特性は、そのデフォルト値に一致する場合にのみ、しかし、どうやってテストするかわかりません。
スーパーキャット2013

7
これの問題は、それが推移性を壊すことです。あなたは、追加した場合B b2 = new B(1,99)、その後、b.equals(a) == true及びa.equals(b2) == trueけどb.equals(b2) == false
ニックグリム2015年

46

継承に適した実装については、Tal Cohenのソリューションをチェックしてください。equals()メソッドを正しく実装するにはどうすればよいですか?

概要:

Joshua Blochは、彼の著書「Effective Java Programming Language Guide(Addison-Wesley、2001)」で、「インスタンス化可能なクラスを拡張してアスペクトを追加しながらアスペクトを追加する方法はない」と述べています。タルはそう思わない。

彼の解決策は、別の非対称なblindlyEquals()を両方の方法で呼び出すことによってequals()を実装することです。blindlyEquals()はサブクラスによってオーバーライドされ、equals()は継承され、オーバーライドされることはありません。

例:

class Point {
    private int x;
    private int y;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return (p.x == this.x && p.y == this.y);
    }
    public boolean equals(Object o) {
        return (this.blindlyEquals(o) && o.blindlyEquals(this));
    }
}

class ColorPoint extends Point {
    private Color c;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return (super.blindlyEquals(cp) && 
        cp.color == this.color);
    }
}

Liskov置換原則が満たされる場合、equals()は継承階層全体で機能する必要があることに注意してください。


10
ここで説明されているcanEqualメソッドを見てください。同じ原理で両方のソリューションが機能しますが、canEqualを使用すると、同じフィールドを2回比較することはできません(上記、px == this.xは両方向でテストされます):artima.com /lejava/articles/equality.html
Blaisorblade

2
いずれにしても、これは良い考えではないと思います。Equalsコントラクトを不必要に混乱させます-2つのポイントパラメータaとbをとる人は、a.getX()== b.getX()とa.getY()== b.getYの可能性を意識する必要があります()はtrueにできますが、a.equals(b)とb.equals(a)はどちらもfalseになります(ColorPointが1つだけの場合)。
ケビン

基本的に、これはに似if (this.getClass() != o.getClass()) return falseていますが、派生クラスが等号を変更する必要がある場合にのみfalseを返すという点で柔軟性があります。そうですか?
Aleksandr Dubinsky 2016

31

まだ誰もがこのためにグアバライブラリを推奨していないことにまだ驚いています。

 //Sample taken from a current working project of mine just to illustrate the idea

    @Override
    public int hashCode(){
        return Objects.hashCode(this.getDate(), this.datePattern);
    }

    @Override
    public boolean equals(Object obj){
        if ( ! obj instanceof DateAndPattern ) {
            return false;
        }
        return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
                && Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
    }

23
java.util.Objects.hash()およびjava.util.Objects.equals()はJava 7(2011年にリリース)の一部であるため、Guavaは必要ありません。
ハーマン2013

1
もちろん、ただし、OracleはJava 6のパブリックアップデートを提供していないため、これを回避する必要があります(2013年2月以降そうです)。
ハーマン2013

6
あなたのthisthis.getDate()の手段(乱雑以外)何も
スティーブ・クオ

1
「not instanceof」式には追加の括弧が必要です:if (!(otherObject instanceof DateAndPattern)) {。ヘルナンとスティーブクオに同意する(ただし、個人的な好みの問題です)が、それでも+1します。
Amos M. Carpenter

26

スーパークラスには、java.lang.Objectとして2つのメソッドがあります。それらをカスタムオブジェクトにオーバーライドする必要があります。

public boolean equals(Object obj)
public int hashCode()

等しいオブジェクトは等しい限り同じハッシュコードを生成する必要がありますが、等しくないオブジェクトは異なるハッシュコードを生成する必要はありません。

public class Test
{
    private int num;
    private String data;
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        // object must be Test at this point
        Test test = (Test)obj;
        return num == test.num &&
        (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + num;
        hash = 31 * hash + (null == data ? 0 : data.hashCode());
        return hash;
    }

    // other methods
}

さらに取得したい場合は、このリンクをhttp://www.javaranch.com/journal/2002/10/equalhash.htmlとして確認してください。

これは別の例です 。http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

楽しんで!@。@


申し訳ありませんが、hashCodeメソッドに関するこのステートメントを理解できません。equals()よりも多くの変数を使用することは不正です。しかし、より多くの変数を使用してコーディングすると、コードがコンパイルされます。なぜ合法ではないのですか?
Adryr83

19

メンバーの等価性をチェックする前にクラスの等価性をチェックする方法はいくつかありますが、どちらも適切な状況で役立ちます。

  1. instanceof演算子を使用します。
  2. を使用しthis.getClass().equals(that.getClass())ます。

finalイコールの実装で#1を使用するか、イコールのアルゴリズムを規定するインターフェースを実装するときに(java.utilコレクションインターフェースのように、実装しているインターフェースで確認するための正しい方法(obj instanceof Set))。equalsがオーバーライドされる可能性がある場合、対称性のプロパティが壊れるので、一般的にこれは悪い選択です。

オプション#2を使用すると、equalsをオーバーライドしたり対称性を壊したりすることなく、クラスを安全に拡張できます。

クラスもであるComparable場合、equalsおよびcompareToメソッドも一貫している必要があります。Comparableクラスのequalsメソッドのテンプレートは次のとおりです。

final class MyClass implements Comparable<MyClass>
{

  

  @Override
  public boolean equals(Object obj)
  {
    /* If compareTo and equals aren't final, we should check with getClass instead. */
    if (!(obj instanceof MyClass)) 
      return false;
    return compareTo((MyClass) obj) == 0;
  }

}

1
+1。getClass()もinstanceofも万能薬ではありません。これは両方にアプローチする方法の良い説明です。equals()を使用する代わりにthis.getClass()== that.getClass()を実行しない理由があるとは思わないでください。
ポールカントレル2013

これには1つの問題があります。アスペクトを追加せず、equalsメソッドをオーバーライドしない匿名クラスは、同等であるはずのgetClassチェックに失敗します。
steinybot 2015年

@Steiny異なるタイプのオブジェクトが同じであるべきかどうかは私には明らかではありません。インターフェースの異なる実装を共通の匿名クラスとして考えています。あなたの前提をサポートする例を挙げられますか?
エリクソン2015年

MyClass a = new MyClass(123); MyClass b = new MyClass(123){//いくつかのメソッドをオーバーライドする}; // this.getClass()。equals(that.getClass())を使用している場合、a.equals(b)はfalse
steinybot 2015

1
@Steinyそうですね。ほとんどの場合にそうであるように、特にメソッドが追加される代わりにオーバーライドされる場合。上記の私の例を考えてみましょう。そうではなくfinalcompareTo()メソッドがオーバーライドされてソート順が逆になる場合、サブクラスとスーパークラスのインスタンスは等しいと見なされるべきではありません。これらのオブジェクトをツリーで一緒に使用すると、instanceof実装に応じて「等しい」キーが見つからない場合があります。
エリクソン2015


11

equals()メソッドは、2つのオブジェクトの等価性を判断するために使用されます。

10のint値は常に10に等しいためです。ただし、このequals()メソッドは、2つのオブジェクトのほぼ同等です。オブジェクトと言うと、プロパティがあります。同等性を決定するために、これらの特性が考慮されます。等価性を判断するためにすべてのプロパティを考慮する必要はなく、クラス定義とコンテキストに関して決定する必要があります。次に、equals()メソッドをオーバーライドできます。

equals()メソッドをオーバーライドする場合は、常にhashCode()メソッドをオーバーライドする必要があります。そうでない場合、どうなりますか?アプリケーションでハッシュテーブルを使用すると、期待どおりに動作しません。格納された値の同等性を判断するためにhashCodeが使用されるため、キーに対応する正しい値は返されません。

指定されたデフォルトの実装は、ObjectクラスのhashCode()メソッドです。オブジェクトの内部アドレスを使用し、それを整数に変換して返します。

public class Tiger {
  private String color;
  private String stripePattern;
  private int height;

  @Override
  public boolean equals(Object object) {
    boolean result = false;
    if (object == null || object.getClass() != getClass()) {
      result = false;
    } else {
      Tiger tiger = (Tiger) object;
      if (this.color == tiger.getColor()
          && this.stripePattern == tiger.getStripePattern()) {
        result = true;
      }
    }
    return result;
  }

  // just omitted null checks
  @Override
  public int hashCode() {
    int hash = 3;
    hash = 7 * hash + this.color.hashCode();
    hash = 7 * hash + this.stripePattern.hashCode();
    return hash;
  }

  public static void main(String args[]) {
    Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
    Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
    Tiger siberianTiger = new Tiger("White", "Sparse", 4);
    System.out.println("bengalTiger1 and bengalTiger2: "
        + bengalTiger1.equals(bengalTiger2));
    System.out.println("bengalTiger1 and siberianTiger: "
        + bengalTiger1.equals(siberianTiger));

    System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
    System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
    System.out.println("siberianTiger hashCode: "
        + siberianTiger.hashCode());
  }

  public String getColor() {
    return color;
  }

  public String getStripePattern() {
    return stripePattern;
  }

  public Tiger(String color, String stripePattern, int height) {
    this.color = color;
    this.stripePattern = stripePattern;
    this.height = height;

  }
}

コード出力例:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: 1227465966

7

論理的には:

a.getClass().equals(b.getClass()) && a.equals(b)a.hashCode() == b.hashCode()

しかしその逆ではありません


6

私が見つけた1つの落とし穴は、2つのオブジェクトがお互いへの参照を含むところです(1つの例は、すべての子を取得するための親の便利なメソッドを持つ親/子関係です)。
この種のことは、たとえばHibernateマッピングを行う場合にはかなり一般的です。

関係の両端をhashCodeまたはequalsテストに含めると、StackOverflowExceptionで終了する再帰ループに入る可能性があります。
最も簡単な解決策は、メソッドにgetChildrenコレクションを含めないことです。


5
ここでの根底にある理論は、オブジェクトの属性集合体、および関連を区別することだと思います。asssociationsはで参加させないでくださいequals()。マッドサイエンティストが私の複製を作成した場合、私たちは同等です。しかし、同じ父親はいないでしょう。
Raedwald、2009
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.