CodeGym /Java Blog /ランダム /equals メソッドと hashCode メソッド: ベスト プラクティス
John Squirrels
レベル 41
San Francisco

equals メソッドと hashCode メソッド: ベスト プラクティス

ランダム グループに公開済み
やあ!equals()今日は、Java の 2 つの重要なメソッド、およびについて説明しますhashCode()。私たちが彼らに会うのは今回が初めてではありません。CodeGym コースは次の短いレッスンから始まりますequals()。忘れてしまった場合やまだ見たことがない場合は読んでください... equals メソッドと hashCode メソッド: ベスト プラクティス - 1今日のレッスンでは、次のことについて話します。これらの概念を詳しく説明します。信じてください、話したいことがあります! ==しかし、新しい話に移る前に、これまでに説明した内容を更新してみましょう :) 覚えているとおり、演算子は参照を比較するため、演算子を使用して 2 つのオブジェクトを比較することは通常悪い考えです==。最近のレッスンで使用した車の例を次に示します。

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
コンソール出力:

false
2 つの同一のオブジェクトを作成したようですCar。2 つの車オブジェクトの対応するフィールドの値は同じですが、比較の結果は依然として false です。理由はすでにわかっています。car1と のcar2参照は異なるメモリ アドレスを指しているため、それらは等しくありません。ただし、それでも比較したいのは 2 つの参照ではなく 2 つのオブジェクトです。オブジェクトを比較するための最良の解決策はequals()メソッドです。

等しい() メソッド

このメソッドは最初から作成するのではなく、オーバーライドすることを思い出してください。メソッドはクラスequals()内で定義されますObject。とはいえ、通常の形式ではほとんど役に立ちません。

public boolean equals(Object obj) {
   return (this == obj);
}
このようにしてクラスequals()内でメソッドが定義されますObject。改めて参考資料の比較です。なぜそのようにしたのでしょうか?では、言語の作成者は、プログラム内のどのオブジェクトが等しいと見なされ、どのオブジェクトがそうでないとどうやって知るのでしょうか? :) これがこのメソッドの主要なポイントですequals()。クラスの作成者は、クラスのオブジェクトの同等性をチェックするときにどの特性が使用されるかを決定します。次に、equals()クラス内のメソッドをオーバーライドします。「どの特性を決定するか」の意味がよくわからない場合は、例を考えてみましょう。男性を表す単純なクラスを次に示しますMan

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
2 人の人物が一卵性双生児なのか、それとも単に似ているのかを判断する必要があるプログラムを作成しているとします。鼻の大きさ、目の色、髪型、傷の有無、DNA検査の結果の5つの特徴があります(簡単のため整数コードで表します)。これらの特徴のうち、私たちのプログラムが一卵性双生児を識別できるようになると思いますか? equals メソッドと hashCode メソッド: ベスト プラクティス - 2もちろん、保証できるのは DNA 検査だけです。二人の人間が同じ目の色、髪型、鼻、さらには傷跡を持つこともあります。世界にはたくさんの人がいますが、そこにドッペルゲンガーがいないと保証することは不可能です。しかし、私たちには信頼できるメカニズムが必要です。つまり、DNA 検査の結果だけが正確な結論を下すことができるのです。equals()これは私たちのメソッドにとって何を意味するのでしょうか? でオーバーライドする必要があります。Manプログラムの要件を考慮してクラスを決定します。このメソッドはint dnaCode2 つのオブジェクトのフィールドを比較する必要があります。それらが等しい場合、オブジェクトは等しいです。

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
本当にそんな簡単なことなのでしょうか?あまり。私たちは何かを見落としていました。オブジェクトについては、オブジェクトの同等性の確立に関連するフィールドを 1 つだけ特定しました: dnaCode。ここで、関連するフィールドが 1 つではなく 50 個あると想像してください。2 つのオブジェクトの 50 フィールドがすべて等しい場合、オブジェクトは等しいことになります。このようなシナリオも可能です。主な問題は、50 個のフィールドを比較して同等性を確立するのは、時間とリソースを大量に消費するプロセスであることです。ここで、私たちのクラスに加えて、に存在するものとまったく同じフィールドを持つクラスがManあると想像してください。別のプログラマーが私たちのクラスを使用する場合、次のようなコードを簡単に作成できます。 WomanMan

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
この場合、フィールド値をチェックすることは無意味です。2 つの異なるクラスのオブジェクトがあることがすぐにわかります。したがって、それらが等しいはずはありません。equals()これは、比較対象のオブジェクトのクラスを比較するチェックをメソッドに追加する必要があることを意味します。私たちがそれを考えたのは良いことです!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
しかし、もしかしたら私たちは何か他のことを忘れているでしょうか?うーん...少なくとも、オブジェクトをそれ自体と比較していないことを確認する必要があります。参照 A と B が同じメモリ アドレスを指している場合、それらは同じオブジェクトであり、時間を無駄にして 50 個のフィールドを比較する必要はありません。

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
nullまた、 と等しいオブジェクトはない というチェックを追加しても問題はありませんnull。したがって、メソッドのパラメータが null の場合、追加のチェックは意味がありません。これらすべてを念頭に置くと、クラスequals()のメソッドはMan次のようになります。

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
上記のすべての初期チェックを実行します。一日の終わりに、次のような場合:
  • 同じクラスの 2 つのオブジェクトを比較しています
  • 比較されたオブジェクトは同じオブジェクトではありません
  • 渡されたオブジェクトはnull
...その後、関連する特性の比較に進みます。私たちにとって、これはdnaCode2 つのオブジェクトのフィールドを意味します。メソッドをオーバーライドするときはequals()、次の要件を必ず守ってください。
  1. 反射性。

    このequals()メソッドを使用してオブジェクトをそれ自体と比較する場合は、true を返す必要があります。
    私たちはすでにこの要件を満たしています。私たちの方法には次のものが含まれます。

    
    if (this == o) return true;
    

  2. 対称。

    の場合はa.equals(b) == true、 をb.equals(a)返さなければなりませんtrue
    私たちの方法はこの要件も満たします。

  3. 推移性。

    2 つのオブジェクトが 3 番目のオブジェクトと等しい場合、それらは互いに等しい必要があります。および の
    場合、 もtrue を返す必要があります。a.equals(b) == truea.equals(c) == trueb.equals(c)

  4. 持続性。

    の結果は、equals()関連するフィールドが変更された場合にのみ変更される必要があります。2 つのオブジェクトのデータが変更されない場合、結果はequals()常に同じでなければなりません。

  5. との不等式null

    どのオブジェクトに対してもa.equals(null)false を返す必要があります。
    これは単なる「有用な推奨事項」のセットではなく、Oracle のドキュメントに記載されている厳密な規約です。

hashCode() メソッド

では、その方法についてお話しましょうhashCode()。なぜ必要なのでしょうか? まったく同じ目的、つまりオブジェクトを比較するためです。しかし、私たちはすでに持っていますequals()!なぜ別の方法があるのでしょうか? 答えは簡単です。パフォーマンスを向上させるためです。Java で メソッドを使用して表現されるハッシュ関数はhashCode()、任意のオブジェクトに対して固定長の数値を返します。Java では、メソッドはオブジェクトに対してhashCode()32 ビット数値 ( ) を返します。2 つの数値を比較することは、特にそのメソッドが多くのフィールドを考慮する場合、このメソッドをint使用して 2 つのオブジェクトを比較するよりもはるかに高速です。equals()プログラムがオブジェクトを比較する場合、ハッシュ コードを使用するとはるかに簡単です。メソッドに基づいてオブジェクトが等しい場合にのみ、hashCode()比較は次のステップに進みます。equals()方法。ちなみに、ハッシュベースのデータ構造は次のように動作します。たとえば、おなじみのHashMap! このhashCode()メソッドは、メソッドと同様にequals()、開発者によってオーバーライドされます。そして、 と同様にequals()、このhashCode()メソッドには Oracle ドキュメントに詳細に記載されている公式要件があります。
  1. 2 つのオブジェクトが等しい場合 (つまり、equals()メソッドが true を返す場合)、それらは同じハッシュ コードを持つ必要があります。

    そうでなければ、私たちの方法は無意味になってしまいます。上で述べたように、hashCode()パフォーマンスを向上させるには、最初にチェックを行う必要があります。ハッシュ コードが異なる場合、メソッドの定義方法に従ってオブジェクトが実際には等しい場合でも、チェックは false を返しますequals()

  2. メソッドが同じオブジェクトに対して複数回呼び出された場合hashCode()、毎回同じ数値を返す必要があります。

  3. ルール 1 は逆方向には機能しません。2 つの異なるオブジェクトが同じハッシュ コードを持つことができます。

3 番目のルールは少しわかりにくいです。どうすればいいの?説明は非常に簡単です。このhashCode()メソッドは を返しますint。An はint32 ビットの数値です。値の範囲は -2,147,483,648 から +2,147,483,647 までに制限されています。言い換えれば、 に取り得る値は 40 億をわずかに超えるということですint。ここで、地球上に住むすべての人々に関するデータを保存するプログラムを作成していると想像してください。各人は独自のPersonオブジェクト (Manクラスに似たもの) に対応します。地球上には約 75 億人が住んでいます。言い換えれば、変換のために作成したアルゴリズムがどれほど賢くても、Personオブジェクトを int に変換する場合、単に十分な数値がありません。可能な int 値は 45 億個しかありませんが、それよりもはるかに多くの人がいます。これは、どんなに努力しても、異なる人が同じハッシュ コードを持っている可能性があることを意味します。これが発生した場合 (2 つの異なるオブジェクトのハッシュ コードが一致した場合)、これを衝突と呼びます。メソッドをオーバーライドするときhashCode()、プログラマの目的の 1 つは、潜在的な衝突の数を最小限に抑えることです。これらすべてのルールを考慮すると、クラスhashCode()内のメソッドはどのように見えるでしょうかPerson? このような:

@Override
public int hashCode() {
   return dnaCode;
}
驚いた?:) 要件を見れば、私たちがすべてに準拠していることがわかります。このequals()メソッドが true を返すオブジェクトも、 に従って同等になりますhashCode()。2 つのPersonオブジェクトが等しいequals(つまり、同じ を持っているdnaCode) 場合、メソッドは同じ数値を返します。もっと難しい例を考えてみましょう。私たちのプログラムが自動車コレクター向けに高級車を選択するとします。収集は多くの特徴を持つ複雑な趣味です。特定の 1963 年製の車の価格は、1964 年製の車の 100 倍になる場合があります。1970 年の赤い車は、同じ年の同じブランドの青い車の 100 倍の価格になることがあります。 equals メソッドと hashCode メソッド: ベスト プラクティス - 4前の例では、Personクラスを使用して、ほとんどのフィールド (つまり人間の特性) を重要ではないものとして破棄し、dnaCode比較のフィールド。私たちは現在、重要でない詳細が存在しない、非常に特異な領域で作業しています。私たちのクラスは次のとおりですLuxuryAuto

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
ここで、比較のすべてのフィールドを考慮する必要があります。あらゆる間違いが発生すると、クライアントに数十万ドルの損失が発生する可能性があるため、過度に安全を確保することをお勧めします。

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
私たちのequals()方法では、前に説明したすべてのチェックを忘れたわけではありません。しかしここでは、オブジェクトの 3 つのフィールドをそれぞれ比較します。このプログラムでは、絶対的な平等、つまり各フィールドの平等が必要です。どうですかhashCode

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
このクラスのフィールドmodelは文字列です。Stringクラスはすでにメソッドをオーバーライドしているため、これは便利ですhashCode()。フィールドのハッシュ コードを計算しmodel、他の 2 つの数値フィールドの合計をそれに加算します。Java 開発者には、衝突の数を減らすための簡単なトリックがあります。それは、ハッシュ コードを計算するときに、中間結果に奇数の素数を乗算することです。最も一般的に使用される数値は 29 または 31 です。今は数学的な微妙な点については掘り下げませんが、将来的には、中間結果に十分に大きな奇数を乗算すると、ハッシュ関数の結果を「分散」するのに役立つことを覚えておいてください。その結果、同じハッシュ コードを持つオブジェクトの数が減ります。LuxuryAuto のメソッドの場合hashCode()、次のようになります。

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
このメカニズムの複雑さの詳細については、StackOverflow のこの投稿と、Joshua Bloch 著の『Effective Java』を参照してください。最後に、言及する価値のあるもう 1 つの重要な点があります。equals()andメソッドをオーバーライドするたびにhashCode()、これらのメソッドで考慮される特定のインスタンス フィールドを選択しました。これらのメソッドは同じフィールドを考慮します。equals()しかし、とで異なるフィールドを考慮することはできるでしょうかhashCode()? 技術的には可能です。しかし、これは悪い考えであり、その理由は次のとおりです。

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
equals()このクラスのメソッドとhashCode()メソッドを 次に示しますLuxuryAuto。メソッドhashCode()は変更されませんでしたが、メソッドmodelからフィールドを削除しましたequals()。このモデルは、メソッドが 2 つのオブジェクトを比較するときに使用される特性ではなくなりましたequals()。ただし、ハッシュ コードを計算するときは、そのフィールドが引き続き考慮されます。その結果、何が得られるでしょうか? 2台の車を作って調べてみましょう!

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
エラー!equals()とメソッドに異なるフィールドを使用することでhashCode()、それらに対して確立された契約に違反しました。メソッドに従って等しい 2 つのオブジェクトは、equals()同じハッシュ コードを持つ必要があります。私たちはそれらに対して異なる価値観を受け取りました。このようなエラーは、特にハッシュを使用するコレクションを操作する場合に、まったく信じられない結果を引き起こす可能性があります。そのため、equals()と をオーバーライドするときはhashCode()、同じフィールドを考慮する必要があります。このレッスンはかなり長かったですが、今日はたくさんのことを学びました!:) さあ、タスクの解決に戻りましょう。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION