Java equals()およびhashCode()
1概要
このチュートリアルでは、密接に属する2つのメソッド、
equals()
と
hashCode()
を紹介します。私たちはお互いの関係、それらをどうやって正しく上書きするのか、そしてなぜ両方を上書きするのか、どちらも上書きしないのかに焦点を当てます。
2
equals()
Object
クラスは、
equals()
メソッドと
hashCode()
メソッドの両方を定義します。つまり、これら2つのメソッドは、作成するものも含め、すべてのJavaクラスで暗黙的に定義されます。
class Money {
int amount;
String currencyCode;
}
Money income = new Money(55, "USD");
Money expenses = new Money(55, "USD");
boolean balanced = income.equals(expenses)
income.equals(費用)
が
true
を返すことを期待します。しかし、現在の形の
Money
クラスでは、それは成功しません。
-
クラス
Object
の中の
equals()
のデフォルト実装は、同等性はオブジェクトの同一性と同じであると言います。そして
income
と
expenses
は2つの異なるインスタンスです。
2.1.
equals()
をオーバーライドする
オブジェクトのアイデンティティだけでなく、2つの関連するプロパティの値も考慮しないように、
equals()
メソッドをオーバーライドします。
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Money))
return false;
Money other = (Money)o;
boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
|| (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
return this.amount == other.amount && currencyCodeEquals;
}
2.2.
equals()
契約
Java SEは、
equals()
メソッドの実装が満たすべき規約を定義しています。
ほとんどの基準は常識です
equals()
メソッドは次のようになります。
-
__反射的:オブジェクトはそれ自身と同じでなければならない
-
symmetric
:**
x.equals(y)
は、以下と同じ結果を返さなければなりません。
y.equals(x)
transitive
:if
x.equals(y)
と
y.equals(z)
の場合
x.equals(z)
**
consistent
:
equals()
の値はプロパティが変更された場合にのみ変更される
それは
equals()
changesに含まれています(ランダム性は許されません)
Object
クラスのhttps://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Object.html[Java SE Docsで正確な基準を調べることができます。]。
2.3. 継承による
equals()
対称性への違反
equals()
の基準がそのような常識である場合、どうしたらそれをまったく違反できますか?
equals()
をオーバーライドしたクラスを拡張すると、違反が最も頻繁に発生します。**
Money
クラスを拡張する
Voucher
クラスを考えてみましょう。
class WrongVoucher extends Money {
private String store;
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof WrongVoucher))
return false;
WrongVoucher other = (WrongVoucher)o;
boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
|| (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
boolean storeEquals = (this.store == null && other.store == null)
|| (this.store != null && this.store.equals(other.store));
return this.amount == other.amount && currencyCodeEquals && storeEquals;
}
//other methods
}
一見すると、
Voucher
クラスとその
equals()
のオーバーライドは正しいようです。そして、
Money
を
Money
に、または
Voucher
を
Voucher
に比較する限り、両方の
equals()
メソッドは正しく動作します。
しかし、これら2つのオブジェクトを比較するとどうなりますか?
Money cash = new Money(42, "USD");
WrongVoucher voucher = new WrongVoucher(42, "USD", "Amazon");
voucher.equals(cash) => false//As expected.
cash.equals(voucher) => true//That's wrong.
これは
equals()
規約の対称性基準に違反しています。
2.4. コンポジションによる
equals()
対称性の修正
この落とし穴を避けるために、継承よりも合成を優先する必要があります。
Money
をサブクラス化する代わりに、
Money
プロパティを持つ
Voucher
クラスを作成しましょう。
class Voucher {
private Money value;
private String store;
Voucher(int amount, String currencyCode, String store) {
this.value = new Money(amount, currencyCode);
this.store = store;
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Voucher))
return false;
Voucher other = (Voucher) o;
boolean valueEquals = (this.value == null && other.value == null)
|| (this.value != null && this.value.equals(other.value));
boolean storeEquals = (this.store == null && other.store == null)
|| (this.store != null && this.store.equals(other.store));
return valueEquals && storeEquals;
}
//other methods
}
そして今、____equalsは契約の要求どおりに対称的に機能します。
3
ハッシュコード()
hashCode()
は、クラスの現在のインスタンスを表す整数を返します。この値はクラスの等価性の定義と一致して計算する必要があります。したがって、
equals()
** メソッドをオーバーライドする場合は、
hashCode()
もオーバーライドする必要があります。
詳細については、https://www.baeldung.com/java-hashcode[guide to
hashCode()
]を参照してください。
3.1.
hashCode()
契約
Java SEは、
hashCode()
メソッドの規約も定義しています。詳しく見てみると、
hashCode()
と
equals()
がどれほど密接に関連しているかがわかります。
-
hashCode()
の契約における3つの基準はすべて、何らかの形で
equals()
メソッドについて言及している -
内部の一貫性
:__hashCode()の値は、
equals()
changesに含まれるプロパティ
同等の一貫性
:
互いに等しいオブジェクト
同じハッシュコードを返す
collisions
:
等しくないオブジェクトは同じhashCodeを持つことができます
3.2.
hashCode()
と
equals()
の整合性違反
hashCodeメソッド規約の2番目の基準は重要な結果をもたらします。** equals()をオーバーライドする場合は、hashCode()もオーバーライドする必要があります。 ()__メソッド。
そのような例を見てみましょう:
class Team {
String city;
String department;
@Override
public final boolean equals(Object o) {
//implementation
}
}
Team
クラスは
equals()
のみをオーバーライドしますが、それでも
Object
クラスで定義されているように
hashCode()
のデフォルト実装を暗黙的に使用します。そして、これはクラスのインスタンスごとに異なる__hashCode()を返します。これは2番目の規則に違反します。
都市 “New York”と部門 “marketing”の両方を持つ2つの
Team
オブジェクトを作成した場合、それらは等しくなります** が、それらは異なるhashCodeを返します。
3.3. 一貫性のない
HashMap
キー
hashCode()
しかし、なぜ
Team
クラスの契約違反が問題なのですか?まあ、問題はハッシュベースのコレクションが関係しているときに始まります。
Team
クラスを
HashMap
のキーとして使用しましょう。
Map<Team,String> leaders = new HashMap<>();
leaders.put(new Team("New York", "development"), "Anne");
leaders.put(new Team("Boston", "development"), "Brian");
leaders.put(new Team("Boston", "marketing"), "Charlie");
Team myTeam = new Team("New York", "development");
String myTeamLeader = leaders.get(myTeam);
myTeamLeader
が“ Anne”を返すことを期待します。
しかし、現在のコードでは、そうではありません。
Team
クラスのインスタンスを
HashMap
キーとして使用したい場合は、
hashCode()
メソッドをオーバーライドしてコントラクトに準拠させる必要があります。
実装例を見てみましょう。
@Override
public final int hashCode() {
int result = 17;
if (city != null) {
result = 31 ** result + city.hashCode();
}
if (department != null) {
result = 31 ** result + department.hashCode();
}
return result;
}
この変更後、
leaders.get(myTeam)
は期待どおりに「Anne」を返します。
4いつ
equals()
と
hashCode()
をオーバーライドしますか?
-
一般的には、両方またはどちらか一方を上書きしたいです。
ドメイン駆動設計は、私たちがそれらを放置すべきときの状況を決定するのに役立ちます。エンティティクラスの場合 – 固有の識別情報を持つオブジェクトの場合 – デフォルトの実装はしばしば意味があります。
ただし、
値オブジェクトの場合は、通常、そのプロパティに基づいて等価性が優先されます
。したがって、
equals()
と
hashCode()
をオーバーライドする必要があります。 2章の
Money
クラスを覚えておいてください:55 USDは55 USDに等しい – たとえそれらが2つの別々の例であったとしても。
5実装ヘルパー
通常、これらのメソッドの実装は手作業では書かないでください。見てわかるように、かなりの数の落とし穴があります。
一般的な方法の1つは、https://www.baeldung.com/java-eclipse-equals-and-hashcode[IDEから]で
equals()
および
hashCode()
メソッドを生成することです。
Apache Commons Lang
およびhttps://www.baeldung.com/whats-new-in-guava-19[Google Guava]にはヘルパークラスがあります。両方の方法を書くのを簡単にするために。
プロジェクトロンボク
も
@ EqualsAndHashCode
アノテーションを提供しています。 **
equals()
と
hashCode()
がどのようにして共通の注釈を持っているのかをもう一度注意してください。
6. 契約の確認
私たちの実装がJava SEの契約といくつかのベストプラクティスにも従っているかどうかをチェックしたいのならば、私たちはEqualsVerifierライブラリを使うことができます。
EqualsVerifier
Mavenテスト依存関係を追加しましょう。
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
Team
クラスが
equals()
および
hashCode()
の規約に従っていることを確認しましょう。
@Test
public void equalsHashCodeContracts() {
EqualsVerifier.forClass(Team.class).verify();
}
EqualsVerifier
が
equals()
メソッドと
hashCode()
メソッドの両方をテストすることは注目に値します。
-
EqualsVerifier
は、Java SEの規約よりもはるかに厳密です** たとえば、このメソッドは__NullPointerExceptionをスローしないようにします。 -
デフォルトの
EqualsVerifier
の設定では不変フィールドのみが許可されることを理解することが重要です。これは、Java SEの契約で許可されているものよりも厳しいチェックです。これは、値オブジェクトを不変にするというドメイン駆動設計の推奨に準拠しています。
いくつかの組み込み制約が不要であるとわかった場合は、
EqualsVerifier
呼び出しに
suppress(Warning.SPECIFIC
WARNING)__を追加できます。
7. 結論
この記事では、
equals()
および
hashCode()
コントラクトについて説明しました。私たちは覚えておくべきです:
-
equals()
をオーバーライドする場合は、常に
hashCode()
をオーバーライドしてください。 -
値オブジェクトに対して
equals()
および
__hashCode()
__をオーバーライドする
オーバーライドされたクラスを拡張することのトラップに注意してください。
equals()
と
hashCode()
** IDEまたはサードパーティのライブラリを使用して、
equals()
メソッドと
hashCode()
メソッド
** 実装をテストするためにEqualsVerifierを使用することを検討してください。
最後に、すべてのコード例がhttps://github.com/eugenp/tutorials/tree/master/core-java-lang-oop[over on GitHub]にあります。