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]にあります。