equals
andをオーバーライドする際に考慮しなければならない問題/落とし穴は何hashCode
ですか?
11 に答える
理論(言語弁護士と数学に傾倒する人向け):
equals()
( javadoc ) は同値関係を定義する必要があります (再帰的、対称的、および推移的である必要があります)。さらに、一貫性がなければなりません(オブジェクトが変更されていない場合は、同じ値を返し続ける必要があります)。さらに、o.equals(null)
常に false を返す必要があります。
hashCode()
( javadoc ) も一貫している必要があります(オブジェクトが に関して変更されていない場合equals()
、同じ値を返し続ける必要があります)。
2 つの方法の関係は次のとおりです。
When
a.equals(b)
, thena.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();
}
}
また覚えておいてください:
HashSet、LinkedHashSet、HashMap、Hashtable、またはWeakHashMapなどのハッシュベースのCollectionまたはMapを使用する場合は、オブジェクトがコレクション内にある間、コレクションに入れるキー オブジェクトの hashCode() が決して変更されないようにしてください。これを確実にする確実な方法は、キーを不変にすることです。これには、他の利点もあります。
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
正しく動作するのは using だけです。
遅延ロードされたオブジェクトには null フィールドがあります
通常、ORM は getter を使用して、遅延ロードされたオブジェクトを強制的にロードします。これは、ロードを強制して "John Doe" を返したとしても、遅延ロードさperson.name
れることを意味します。私の経験では、これは と でより頻繁に発生します。null
person
person.getName()
hashCode()
equals()
ORM を扱っている場合は、必ず getter を使用し、 および でフィールド参照を使用しないで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()
forhashCode
とgetId()
plus getName()
(パラノイアのためだけに) for を使用しequals()
ます。の「衝突」のリスクがあっても問題ありませんが、 のhashCode()
場合は決して問題ありませんequals()
。
hashCode()
から変更されないプロパティのサブセットを使用する必要があります。equals()
についての説明obj.getClass() != getClass()
。
このステートメントは、equals()
継承が友好的でない結果です。JLS (Java 言語仕様) では、 if A.equals(B) == true
thenB.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 if 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()
ます。
継承しやすい実装については、Tal Cohen のソリューション、How Do I Correctly Implement the equals() Method? を参照してください。
概要:
Joshua Bloch は著書『Effective Java Programming Language Guide』 (Addison-Wesley、2001 年) で、「インスタンス化可能なクラスを拡張し、等号契約を維持しながら側面を追加する方法はまったくない」と主張しています。タルは同意しません。
彼の解決策は、別の非対称のブラインド Equals() を両方の方法で呼び出して 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 Substitution Principleを満たすには、 equals() が継承階層全体で機能する必要があることに注意してください。
Still amazed that none recommended the guava library for this.
//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());
}
スーパークラスには、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
楽しんで!@.@
メンバーの等価性をチェックする前にクラスの等価性をチェックする方法はいくつかありますが、適切な状況ではどちらも役立つと思います。
- 演算子を使用し
instanceof
ます。 - を使用し
this.getClass().equals(that.getClass())
ます。
#1 は、final
equals の実装で使用するか、equals のアルゴリズムを規定するインターフェイスを実装するときに使用します (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;
}
}
イコールについては、アンジェリカ・ランガーの「シークレット・オブ・イコール」をご覧ください。私はそれが大好きです。彼女はまた、 JavaのGenericsに関する優れたFAQでもあります。彼女の他の記事をここで表示します(「コアJava」までスクロールダウンします)。ここでは、パート2と「混合型の比較」についても説明しています。それらを読んで楽しんでください!
論理的には次のとおりです。
a.getClass().equals(b.getClass()) && a.equals(b)
⇒a.hashCode() == b.hashCode()
しかし、その逆ではありません!
私が見つけた落とし穴の 1 つは、2 つのオブジェクトが互いへの参照を含む場合です (1 つの例は、親ですべての子を取得するための便利なメソッドを使用した親子関係です)。
たとえば、Hibernate マッピングを行う場合、これらの種類のことはかなり一般的です。
関係の両端を hashCode または equals テストに含めると、StackOverflowException で終了する再帰ループに入る可能性があります。
最も簡単な解決策は、getChildren コレクションをメソッドに含めないことです。