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(expenses) true、を返すと予想されますが、現在の形式の Money クラスでは、返されません。

オブジェクトクラスのequals()のデフォルトの実装では、同等性はオブジェクトIDと同じであり、収入費用は次のようになります。 2つの異なるインスタンス。

2.1. equals()をオーバーライドする

equals()メソッドをオーバーライドして、オブジェクトIDだけでなく、関連する2つのプロパティの値も考慮されるようにします。

@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()メソッドは次のようにする必要があります。

  • reflexive :オブジェクトはそれ自体と等しくなければなりません
  • symmetric x.equals(y)は、y.equals(x)と同じ結果を返す必要があります
  • 推移的 x.equals(y)および y.equals(z)、の場合、 x.equals(z)
  • consistent equals()の値は、 equals()に含まれるプロパティが変更された場合にのみ変更されます(ランダム性は許可されません)

オブジェクトクラスJavaSEドキュメントで正確な基準を検索できます。

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()のオーバーライドは正しいように見えます。 また、 equals()メソッドは、MoneyMoneyまたはVoucherVoucherを比較する限り、正しく動作します。 。 しかし、これら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
}

これで、 = は、契約の要求に応じて対称的に機能します。

3. hashCode()

hashCode()は、クラスの現在のインスタンスを表す整数を返します。 この値は、クラスの等式の定義と一致して計算する必要があります。 したがって、 equals()メソッドをオーバーライドする場合は、 hashCode()もオーバーライドする必要があります。

詳細については、hashCode()ガイドをご覧ください。

3.1. hashCode()コントラクト

Java SEは、 hashCode()メソッドのコントラクトも定義します。 それをよく見ると、 hashCode() equals()がどれほど密接に関連しているかがわかります。

hashCode()コントラクトの3つの基準はすべて、 equals()メソッドに何らかの形で言及しています。

  • 内的整合性 hashCode()の値は、 equals()にあるプロパティが変更された場合にのみ変更される可能性があります
  • 等しい整合性互いに等しいオブジェクトは、同じhashCodeを返す必要があります
  • 衝突等しくないオブジェクトは同じhashCodeを持つ可能性があります

3.2. hashCode() equals()の整合性に違反しています

hashCodeメソッドコントラクトの2番目の基準は、重要な結果をもたらします。 equals()をオーバーライドする場合は、hashCode()もオーバーライドする必要があります。これは、 equals()に関して最も広範囲に及ぶ違反です。 およびhashCode()メソッドはコントラクトします。

そのような例を見てみましょう:

class Team {

    String city;
    String department;

    @Override
    public final boolean equals(Object o) {
        // implementation
    }
}

Team クラスは、 equals()のみをオーバーライドしますが、 Objectで定義されているhashCode()のデフォルト実装を暗黙的に使用します。クラス。 そして、これはクラスのインスタンスごとに異なる hashCode()を返します。 これは2番目のルールに違反しています。

ここで、2つの Team オブジェクトを作成すると、両方とも都市「ニューヨーク」と部門「マーケティング」で等しくなりますが、異なるハッシュコードが返されます。

3.3. HashMapキーに一貫性がないhashCode()

しかし、なぜチームクラスの契約違反が問題になるのでしょうか。 さて、問題はいくつかのハッシュベースのコレクションが関係しているときに始まります。 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()メソッドをオーバーライドして、コントラクトに準拠するようにする必要があります。 等しいオブジェクトは同じ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()をいつオーバーライドしますか?

一般に、両方をオーバーライドするか、どちらもオーバーライドしないようにします。このルールを無視すると、セクション3で望ましくない結果が発生することがわかりました。

ドメイン駆動設計は、状況をそのままにしておくべき状況を決定するのに役立ちます。 エンティティクラスの場合、固有のIDを持つオブジェクトの場合、デフォルトの実装が理にかなっていることがよくあります。

ただし、値オブジェクトの場合、通常、プロパティに基づいて同等性を優先します。 したがって、 equals()および hashCode()をオーバーライドする必要があります。 セクション2のMoneyクラスを覚えておいてください。2つの別々のインスタンスであっても、55USDは55USDに相当します。

5. 実装ヘルパー

通常、これらのメソッドの実装を手作業で作成することはありません。 これまで見てきたように、かなりの数の落とし穴があります。

一般的なオプションの1つは、 IDEequals()および hashCode()メソッドを生成させることです。

Apache CommonsLangGoogleGuava には、両方のメソッドの記述を簡素化するためのヘルパークラスがあります。

Project Lombok は、@EqualsAndHashCodeアノテーションも提供します。 equals()とhashCode()がどのように「一緒に」なり、共通の注釈を持っているかにもう一度注意してください。

6. 契約の確認

実装がJavaSEコントラクトに準拠しているかどうか、およびベストプラクティスにも準拠しているかどうかを確認する場合は、EqualsVerifierライブラリを使用できます。

EqualsVerifierMavenテストの依存関係を追加しましょう。

<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のデフォルト構成では、不変フィールドのみが許可されていることを理解することが重要です。 これは、JavaSE契約で許可されているものよりも厳密なチェックです。 値オブジェクトを不変にするためのドメイン駆動設計の推奨事項に準拠しています。

組み込みの制約の一部が不要であることがわかった場合は、 suppress(Warning.SPECIFIC_WARNING)EqualsVerifier呼び出しに追加できます。

7. 結論 

この記事では、 equals()および hashCode()コントラクトについて説明しました。 次のことを覚えておく必要があります。

  • equals()をオーバーライドする場合は、常に hashCode()をオーバーライドします
  • 値オブジェクトのequals()および hashCode()をオーバーライドします
  • equals()および hashCode()をオーバーライドした拡張クラスのトラップに注意してください
  • equals()および hashCode()メソッドを生成するために、IDEまたはサードパーティライブラリの使用を検討してください
  • EqualsVerifierを使用して実装をテストすることを検討してください

最後に、すべてのコード例はGitHubにあります。