JavaRush /Java Blog /Random-JA /Equals と hashCode コントラクト、またはそれが何であるか
Aleksandr Zimin
レベル 1
Санкт-Петербург

Equals と hashCode コントラクト、またはそれが何であるか

Random-JA グループに公開済み
もちろん、Java プログラマの大多数は、メソッドが互いに密接に関連しているequalsこと、およびクラス内でこれらのメソッドの両方を一貫してオーバーライドすることが推奨されることを知っています。hashCodeなぜそうなるのか、そしてこのルールを破った場合にどのような悲しい結果が生じる可能性があるのか​​を知っている人はわずかに少ないです。これらの方法の概念を検討し、その目的を繰り返し、なぜそれらが非常に結びついているのかを理解することを提案します。この記事も、クラスのロードに関する前回の記事と同様、最終的に問題の詳細をすべて明らかにし、サードパーティのソースには戻らないようにするために、自分自身のために書きました。したがって、どこかにギャップがある場合はそれを排除する必要があるため、建設的な批判は喜んで行います。残念ながら、記事はかなり長くなってしまいました。

オーバーライドルールに等しい

Java では、同じ起源の 2 つのオブジェクトが論理的に等しいequals()という事実を確認または否定するメソッドが必要です。つまり、2 つのオブジェクトを比較するとき、プログラマはそれらの重要なフィールドが同等かどうかを理解する必要があります。このメソッドは論理的等価性を意味するため、すべてのフィールドが同一である必要はありません。ただし、この方法を特に使用する必要がない場合もあります。よく言われるように、特定のメカニズムを使用して問題を回避する最も簡単な方法は、そのメカニズムを使用しないことです。また、契約を破ると、他のオブジェクトや構造があなたのオブジェクトとどのように相互作用するかを理解する制御を失うことにも注意する必要があります。そして、後でエラーの原因を見つけることは非常に困難になります。 equals()equals

このメソッドをオーバーライドしない場合

  • クラスの各インスタンスが一意である場合。
  • これは、データを操作するように設計されているのではなく、特定の動作を提供するクラスに多くの場合当てはまります。たとえば、 class などですThread。彼らにとってはequals、クラスによって提供されるメソッドの実装でObject十分です。別の例は enum クラス ( Enum) です。
  • 実際には、クラスはそのインスタンスの等価性を判断する必要はありません。
  • たとえば、クラスの場合、java.util.Randomクラスのインスタンスを相互に比較して、同じ乱数シーケンスを返すことができるかどうかを判断する必要はまったくありません。単純に、このクラスの性質がそのような動作を暗示していないからです。
  • 拡張しようとしているクラスに独自のメソッド実装がすでにありequals、この実装の動作が適切な場合。
  • たとえば、クラスSet、の場合ListMap実装はそれぞれ、およびequalsにあります。 AbstractSetAbstractListAbstractMap
  • equals最後に、クラスのスコープがprivateorでありpackage-private、このメソッドが決して呼び出されないことが確実な場合は、オーバーライドする必要はありません。

イコール契約

メソッドをオーバーライドする場合、equals開発者は Java 言語仕様で定義されている基本規則に従う必要があります。
  • 反射性
  • 与えられた値に対してx、式はx.equals(x)返さなければなりませんtrue
    与えられた- そのような意味x != null
  • 対称
  • 与えられた値xおよびに対してy、が返される場合にのみx.equals(y)返される必要があります。 truey.equals(x)true
  • 推移性
  • 与えられた値に対してx、戻り値と戻り値の場合はy値を返さなければなりません。 zx.equals(y)truey.equals(z)truex.equals(z)true
  • 一貫性
  • 2 つのオブジェクトの比較に使用されるフィールドが呼び出し間で変更されていなければ、x繰り返しy呼び出しx.equals(y)によってこのメソッドへの前回の呼び出しの値が返されます。
  • 比較ヌル
  • 与えられた値に対して、x呼び出しはx.equals(null)返さなければなりませんfalse

契約違反に等しい

Java Collections Framework のクラスなどの多くのクラスは、メソッドの実装に依存しているequals()ため、これを無視しないでください。この方法の契約に違反すると、アプリケーションの不合理な動作につながる可能性があり、この場合、その理由を見つけるのは非常に困難になります。再帰性の原理によれば、すべてのオブジェクトはそれ自体と等価でなければなりません。この原則に違反すると、コレクションにオブジェクトを追加し、メソッドを使用してそれを検索するときに、contains()コレクションに追加したばかりのオブジェクトを見つけることができなくなります。対称条件は、比較される順序に関係なく、任意の 2 つのオブジェクトが等しい必要があることを示します。たとえば、文字列型のフィールドを 1 つだけ含むクラスがある場合、equalsこのフィールドをメソッド内の文字列と比較することは正しくありません。なぜなら 逆比較の場合、メソッドは常に value を返しますfalse
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
推移性の 条件から、3 つのオブジェクトのうちの 2 つが等しい場合、この場合は 3 つすべてが等しい必要があることがわかります。この原則は、意味のあるコンポーネントを追加して特定の基本クラスを拡張する必要がある場合に簡単に違反される可能性があります。たとえば、Point座標を持つクラスにxyそれを展開してポイントの色を追加する必要があります。ColorPointこれを行うには、適切なフィールドを持つクラスを宣言する必要がありますcolor。したがって、拡張クラスで親メソッドを呼び出し、親で座標とequalsのみが比較されると仮定すると、色は異なるが座標が同じ 2 つの点は等しいと見なされますが、これは正しくありません。この場合、派生クラスに色を区別できるように教える必要があります。これを行うには、2 つの方法を使用できます。しかし、対称性の法則と 2 番目の推移性の法則に違反します。 xy
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
この場合、呼び出しはpoint.equals(colorPoint)value を返しtrue、比較はcolorPoint.equals(point)を返しますfalse。「その」クラスのオブジェクトを期待します。したがって、対称性の法則が破られます。2 番目の方法では、点の色に関するデータがない場合、つまり クラス がある場合に「ブラインド」チェックを実行しますPoint。または、色に関する情報が利用可能な場合は色を確認します。つまり、 クラス のオブジェクトを比較しますColorPoint
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
ここでは推移性の 原則が次のように違反されます。次のオブジェクトの定義があるとします。
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
したがって、等号p1.equals(p2)が満たされていてもp2.equals(p3)p1.equals(p3)値が返されますfalse。同時に、私の意見では、2 番目の方法はあまり魅力的ではありません。場合によっては、アルゴリズムがブラインド化されて比較が完全に実行されず、ユーザーがそのことを知らない可能性があります。 ちょっとした詩 一般的に、私の理解では、この問題に対する具体的な解決策はありません。Kay Horstmann という名前の権威ある著者の意見では、演算子の使用をオブジェクトのクラスを返すinstanceofメソッド呼び出しにgetClass()置き換えることができ、オブジェクト自体の比較を開始する前に、それらが同じ型であることを確認することができます。 、そしてそれらの共通の起源の事実に注意を払いません。したがって、対称性と推移性規則が満たされます。しかし同時に、バリケードの向こう側には、同様に広範なサークルで尊敬されている別の作家、ジョシュア・ブロックが立っており、彼はこのアプローチがバルバラ・リスコフの置換原則に違反していると信じています。この原則は、「呼び出し元のコードは、基本クラスを意識せずにそのサブクラスと同じ方法で扱わなければならない」と述べています。そして、Horstmann が提案した解決策では、この原則は実装に依存するため、明らかに違反しています。要するに、事態は闇であることは明らかだ。また、Horstmann は彼のアプローチを適用するためのルールを明確にし、クラスを設計するときに戦略を決定する必要があること、および等価性テストがスーパークラスによってのみ実行される場合は、以下を実行することでこれを実行できることを平易な英語で書いていることにも注意してください。操作。それ以外の場合、チェックのセマンティクスが派生クラスに応じて変更され、メソッドの実装を階層の下に移動する必要がある場合は、メソッドを使用する必要があります。一方、Joshua Bloch は、継承を放棄し、クラスにクラスを含めて、特にその点に関する情報を取得するためのアクセス メソッドを提供することによってオブジェクト合成を使用することを提案しています。これにより、すべてのルールに違反することは避けられますが、私の意見では、コードを理解するのがより難しくなります。3 番目のオプションは、IDE を使用して、equals メソッドの自動生成を使用することです。ちなみに、Idea はホルストマン生成を再現しており、スーパークラスまたはその子孫でメソッドを実装するための戦略を選択できます。最後に、次の一貫性ルールは、オブジェクトが変更されない場合でも、オブジェクトを再度呼び出すと前と同じ値を返さなければならないことを示しています。最後のルールは、オブジェクトは と等しくないということです。ここではすべてが明らかです- これは不確実性ですが、オブジェクトは不確実性と等しいのでしょうか? それは明確ではありません。 instanceofgetClass()ColorPointPointasPoint()xyx.equals(y)nullnullfalse

等しいかを判断するための一般的なアルゴリズム

  1. オブジェクト参照thisとメソッドのパラメータが等しいかどうかを確認しますo
    if (this == o) return true;
  2. リンクが定義されているかどうかo、つまりリンクが定義されているかどうかを確認しますnull
    将来、オブジェクト タイプを比較するときに演算子が使用される場合は、このパラメータが返されるため、このinstanceof項目はスキップできます。falsenull instanceof Object
  3. 上記の説明と自分の直感に基づいて、演算子またはメソッドをthis使用してオブジェクト タイプを比較します。oinstanceofgetClass()
  4. メソッドがequalsサブクラスでオーバーライドされる場合は、必ず呼び出しを行ってください。super.equals(o)
  5. パラメータの型をo必要なクラスに変換します。
  6. すべての重要なオブジェクト フィールドの比較を実行します。
    • プリミティブ型 (floatとを除くdouble) の場合、演算子を使用==
    • 参照フィールドの場合は、そのメソッドを呼び出す必要がありますequals
    • 配列の場合は、循環反復またはメソッドを使用できます。Arrays.equals()
    • 型の場合はfloatdouble対応するラッパー クラスの比較メソッドを使用する必要がありますFloat.compare()Double.compare()
  7. 最後に、実装されたメソッドは対称ですか?という 3 つの質問に答えてください。推移的同意しますか? 他の 2 つの原則 (反射性確実性) は通常、自動的に実行されます。

HashCode オーバーライド ルール

ハッシュは、ある時点でのオブジェクトの状態を記述する、オブジェクトから生成される数値です。この数値は、Java では主に などのハッシュ テーブルで使用されますHashMap。この場合、オブジェクトに基づいて数値を取得するハッシュ関数は、ハッシュ テーブル全体で要素が比較的均等に分散されるように実装する必要があります。また、関数が異なるキーに対して同じ値を返す場合の衝突の可能性を最小限に抑えるためでもあります。

コントラクトハッシュコード

ハッシュ関数を実装するために、言語仕様では次のルールが定義されています。
  • 同じオブジェクトに対してメソッドをhashCode1 回以上呼び出すと、値の計算に関与するオブジェクトのフィールドが変更されていない限り、同じハッシュ値が返される必要があります。
  • 2 つのオブジェクトのメソッドを呼び出すと、hashCodeオブジェクトが等しい場合は常に同じ数値が返されます (equalsこれらのオブジェクトのメソッドを呼び出すと が返されますtrue)。
  • 2 つの等しくないオブジェクトに対してメソッドを呼び出すと、hashCode異なるハッシュ値が返される必要があります。この要件は必須ではありませんが、その実装はハッシュ テーブルのパフォーマンスにプラスの影響を与えることを考慮する必要があります。

equals メソッドと hashCode メソッドは一緒にオーバーライドする必要があります

上記の規約に基づくと、コード内でメソッドをオーバーライドするときはequals、常にメソッドをオーバーライドする必要がありますhashCode。実際、クラスの 2 つのインスタンスは異なるメモリ領域にあるため異なるため、何らかの論理的基準に従って比較する必要があります。したがって、論理的に同等の 2 つのオブジェクトは同じハッシュ値を返さなければなりません。 これらのメソッドの 1 つだけがオーバーライドされた場合はどうなりますか?
  1. equalsはい・hashCodeいいえ

    equalsクラス内にメソッドを正しく定義し、hashCodeそのメソッドをクラス内にそのまま残すことにしたとしますObject。この場合、メソッドの観点からはequals2 つのオブジェクトは論理的に同等ですが、メソッドの観点からはhashCode共通点が何もありません。したがって、オブジェクトをハッシュ テーブルに配置すると、キーによってオブジェクトを取得できなくなるリスクがあります。
    たとえば、次のようになります。

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    明らかに、配置されているオブジェクトと検索されているオブジェクトは、論理的には同じですが、2 つの異なるオブジェクトです。しかし理由は 契約に違反したため、ハッシュ値が異なります。ハッシュ テーブルの奥のどこかでオブジェクトを失ったと言えます。

  2. hashCodeはい・equalsいいえ。

    メソッドをオーバーライドしhashCodeequalsクラスからメソッドの実装を継承するとどうなるでしょうかObject。ご存知のとおり、equalsデフォルトのメソッドはオブジェクトへのポインターを単純に比較し、それらが同じオブジェクトを参照しているかどうかを判断します。hashCodeすべての規範に従ってメソッドを記述した、つまり IDE を使用してメソッドを生成したと仮定しましょう。論理的に同一のオブジェクトに対して同じハッシュ値が返されます。そうすることで、2 つのオブジェクトを比較するためのメカニズムがすでに定義されていることは明らかです。

    したがって、理論上は前の段落の例を実行する必要があります。しかし、それでもハッシュ テーブルでオブジェクトを見つけることはできません。これに近づくことになりますが、少なくともオブジェクトが置かれるハッシュ テーブル バスケットが見つかるからです。

    ハッシュ テーブル内のオブジェクトを正常に検索するには、キーのハッシュ値を比較することに加えて、キーと検索されたオブジェクトの論理的同等性の判断も使用されます。つまり、equalsメソッドをオーバーライドせずに実行する方法はありません。

hashCodeを決定するための一般的なアルゴリズム

ここでは、あまり心配せず、お気に入りの IDE でメソッドを生成するべきだと思います。なぜなら、黄金比、つまり正規分布を求めてこれらすべてのビットを右と左にシフトするからです。これは完全に頑固な人向けです。個人的には、同じアイデアよりも優れたものをより速く実行できるかどうかは疑問です。

結論の代わりに

このように、 Java 言語ではメソッドが明確に定義された役割をequals果たし、2 つのオブジェクトの論理的等価特性を取得するように設計されていることがわかります。hashCodeこのメソッドの場合、equalsこれはオブジェクトの比較に直接関係し、hashCode間接的な場合は、たとえば、ハッシュ テーブルまたは同様のデータ構造内のオブジェクトのおおよその位置を決定する必要がある場合に、オブジェクトの検索速度が向上します。契約に加えて、オブジェクトの比較に関連する別の要件がありますequals。これは、とのインターフェイスメソッドhashCodeの一貫性です。この要件により、開発者は常に を返すことが義務付けられます。つまり、2 つのオブジェクトの論理比較はアプリケーション内のどこでも矛盾してはならず、常に一貫している必要があることがわかります。 compareToComparableequalsx.equals(y) == truex.compareTo(y) == 0

情報源

効果的な Java、第 2 版。ジョシュア・ブロック。 とても良い本の無料翻訳。 Java、プロフェッショナルのライブラリ。第 1 巻。基本。ケイ・ホーストマン。 理論を少し減らして実践を増やします。しかし、ブロックほどすべてが詳細に分析されているわけではありません。同じequals()に関するビューがありますが。 画像内のデータ構造。HashMap Java の HashMap デバイスに関する非常に役立つ記事。ソースを見る代わりに。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION