Java equals()およびhashCode()

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

クラスObjectequals()のデフォルトの実装では、同等性はオブジェクトIDと同じであるとされています。 また、incomeexpensesは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()メソッドの実装が満たさなければならないコントラクトを定義します。 Most of the criteria are common sense.equals()メソッドは次のようにする必要があります。

  • reflexive:オブジェクトはそれ自体と等しくなければなりません

  • symmetricx.equals(y) must return the same result as y.equals(x)

  • transitivex.equals(y)およびy.equals(z)の場合、x.equals(z)

  • consistentequals()の値は、equals()に含まれるプロパティが変更された場合にのみ変更されます(ランダム性は許可されません)

Java SE Docs for the Object classで正確な基準を調べることができます。

2.3. 継承によるequals()対称性の違反

equals()の基準がそのような常識である場合、どうすればそれを違反できますか? さて、violations happen most often, if we extend a class that has overridden 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()のオーバーライドは正しいように見えます。 また、MoneyMoney、またはVoucherVoucherを比較する限り、両方のequals()メソッドは正しく動作します。 But what happens, if we compare these two objects?

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()の対称性を構成で修正

この落とし穴を回避するには、 favor composition over inheritance.を実行する必要があります

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()

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

詳細については、guide to hashCode()をご覧ください。

3.1. hashCode()契約

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

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

  • internal consistencyhashCode()の値は、equals()にあるプロパティが変更された場合にのみ変更できます

  • equals consistency:*互いに等しいオブジェクトは、同じhashCodeを返す必要があります*

  • collisionsunequal objects may have the same hashCode

3.2. hashCode()equals()の整合性に違反する

hashCodeメソッドコントラクトの2番目の基準は、重要な結果をもたらします。If we override equals(), we must also override hashCode().これは、equals()メソッドとhashCode()メソッドのコントラクトに関して最も広範囲に及ぶ違反です。

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

class Team {

    String city;
    String department;

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

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

ここで、都市「ニューヨーク」と部門「マーケティング」の両方を持つ2つのTeamオブジェクトを作成すると、それらは等しくなります、but they will return different hashCodes.

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

しかし、なぜTeamクラスのコントラクト違反が問題になるのでしょうか。 まあ、いくつかのハッシュベースのコレクションが関係するとき、問題は始まります。 TeamクラスをHashMapのキーとして使用してみましょう。

Map 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」を返すと予想されます。 But with the current code, it doesn’t.

TeamクラスのインスタンスをHashMapキーとして使用する場合は、hashCode()メソッドをオーバーライドして、コントラクトに準拠するようにする必要があります。Equal objects return the same 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()をいつオーバーライドしますか?

Generally, we want to override either both of them or neither of them.セクション3で、このルールを無視した場合の望ましくない結果を確認しました。

ドメインドリブンデザインは、状況から離れるべき状況を判断するのに役立ちます。 エンティティクラスの場合-固有のIDを持つオブジェクトの場合-デフォルトの実装が適切です。

ただし、for value objects, we usually prefer equality based on their properties。 したがって、equals()hashCode()をオーバーライドする必要があります。 セクション2のMoneyクラスを思い出してください。2つの別々のインスタンスであっても、55米ドルは55米ドルに相当します。

5. 実装ヘルパー

通常、これらのメソッドの実装を手作業で作成することはありません。 ご覧のとおり、落とし穴はかなりあります。

一般的な方法の1つは、let our IDEequals()メソッドとhashCode()メソッドを生成することです。

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

Project Lombokは、@EqualsAndHashCode注釈も提供します。 Note again how equals() and hashCode() “go together” and even have a common annotation.

6. 契約の確認

実装がJavaSEコントラクトに準拠しているかどうか、およびいくつかのベストプラクティスにも準拠しているかどうかを確認する場合は、we can use the EqualsVerifier library.

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


    nl.jqno.equalsverifier
    equalsverifier
    3.0.3
    test

Teamクラスがequals()およびhashCode()コントラクトに従っていることを確認しましょう。

@Test
public void equalsHashCodeContracts() {
    EqualsVerifier.forClass(Team.class).verify();
}

EqualsVerifierequals()メソッドとhashCode()メソッドの両方をテストすることに注意してください。

EqualsVerifier is much stricter than the Java SE contract.たとえば、メソッドがNullPointerException.をスローできないようにします。また、両方のメソッドまたはクラス自体がfinalであることを強制します。

the default configuration of EqualsVerifier allows only immutable fieldsであることを理解することが重要です。 これは、Java SE契約で許可されているものよりも厳密なチェックです。 これは、ドメイン駆動設計の推奨事項に従って、値オブジェクトを不変にします。

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

7. 結論

この記事では、equals()およびhashCode()コントラクトについて説明しました。 覚えておくべきこと:

  • equals()をオーバーライドする場合は、常にhashCode()をオーバーライドします

  • 値オブジェクトのequals()hashCode() をオーバーライド

  • equals()hashCode()をオーバーライドした拡張クラスのトラップに注意してください

  • equals()およびhashCode()メソッドの生成には、IDEまたはサードパーティライブラリの使用を検討してください

  • EqualsVerifierを使用して実装をテストすることを検討してください

最後に、すべてのコード例はover on GitHubで見つけることができます。