CodeGym /Java Course /モジュール 2: Java コア /equals メソッドと hashCode メソッド: それらを使用する理由と使用場所、およびその仕組み

equals メソッドと hashCode メソッド: それらを使用する理由と使用場所、およびその仕組み

モジュール 2: Java コア
レベル 9 , レッスン 1
使用可能

「次に、同様に役立ついくつかのメソッドについて説明します。equals  (Object o) と hashCode() です。」

「Java では、参照変数を比較するときに、オブジェクト自体ではなく、オブジェクトへの参照が比較されることをすでに覚えているでしょう。」

コード 説明
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i==j);
i は j と等しくありません。
変数は異なるオブジェクトを指します。
オブジェクトに同じデータが含まれている場合でも。
Integer i = new Integer(1);
Integer j = i;
System.out.println(i==j);
i は j に等しい。変数には、同じオブジェクトへの参照が含まれています。

「はい、覚えています。」

に 等しい

「ここでは、equals メソッドが標準的なソリューションです。equals メソッドの目的は、オブジェクトの内部に格納されているものを比較することによって、オブジェクトが内部的に同一であるかどうかを判断することです。」

「それで、どうやってそれができるのですか?」

「これはすべて toString() メソッドとよく似ています。」

Object クラスには、単に参照を比較する独自の equals メソッドの実装があります。

public boolean equals(Object obj)
{
return (this == obj);
}

「すごいですね...また話に戻りますね?」

「顎を上げてください。実際には非常に難しいです。」

「このメソッドは、開発者が独自のクラスで上書きできるようにするために作成されました。結局のところ、比較するときにどのデータが関連し、どのデータがそうでないかを知っているのはクラスの開発者だけです。」

「例を挙げてもらえますか?」

「そうですね。数学的な分数を表すクラスがあるとします。次のようになります。」

例:
class Fraction
{
private int numerator;
private int denominator;
Fraction(int numerator, int denominator)
{
this.numerator  = numerator;
this.denominator = denominator;
}public boolean equals(Object obj)
{
if (obj==null)
return false;

if (obj.getClass() != this.getClass() )
return false;

Fraction other = (Fraction) obj;
return this.numerator* other.denominator == this.denominator * other.numerator;
}
}
メソッド呼び出しの例:
Fraction one = new Fraction(2,3);
Fraction two = new Fraction(4,6);
System.out.println(one.equals(two));
メソッド呼び出しは true を返します。
分数 2/3 は分数 4/6 に等しい

「それでは、この例を詳しく見てみましょう。」

equalsメソッドをオーバーライドしたので、Fractionオブジェクトは独自の実装を持つことになります。

「メソッドにはいくつかのチェックがあります。」

" 1) 比較のために渡されたオブジェクトがnullの場合、それらのオブジェクトは等しくありません。オブジェクトに対してequalsメソッドを呼び出すことができる場合、そのオブジェクトは明らかにnullではありません。"

" 2) クラスの比較。オブジェクトが異なるクラスのインスタンスである場合、それらを比較しようとしません。代わりに、すぐにreturn falseを使用して、これらが異なるオブジェクトであることを示します。"

3) 誰もが 2/3 が 4/6 に等しいことを 2 年生から覚えています。でも、どうやってそれを確認するのですか?」

2/3 == 4/6
両辺に両方の約数 (6 と 3) を掛けると、次のようになります。
6 * 2 == 4 * 3
12 == 12
原則:

a / b == c / dの場合、
a
* d == c * b

「したがって、 equalsメソッドの 3 番目の部分では、渡されたオブジェクトを Fraction にキャストし、その小数を比較します。」

「分かった。分子と分子、分母と分母を単純に比較すると、2/3 は 4/6 に等しくない。」

「クラスの開発者だけがクラスを正しく比較する方法を知っている、と言った意味がわかりました。」

「はい、でもそれは話の半分にすぎません。 別のメソッドがあります: hashCode()。

「equals メソッドに関するすべてのことは理にかなっていますが、なぜ hashCode () が必要なのでしょうか?

「簡単に比較するにはhashCodeメソッドが必要です。」

equalsメソッドには大きな欠点があります。動作が遅すぎるということです。数百万の要素の Set があり、それに特定のオブジェクトが含まれているかどうかを確認する必要があるとします。どうすればよいでしょうか?」

「ループを使用してすべての要素を循環し、そのオブジェクトをセット内の各オブジェクトと比較できます。一致するものが見つかるまで。」

「それで、もしそれが存在しないとしたら? オブジェクトがそこに存在しないことを確認するためだけに、100 万回の比較を実行することになるでしょうか? それは大変だと思いませんか?」

「はい、それは比較しすぎだと私でも認識しています。別の方法はありますか?」

「はい、これにはhashCode ()を使用できます。

hashCode () メソッドはオブジェクトごとに特定の数値を返します。クラスの開発者は、equals メソッドの場合と同様に、返される数値を決定します。

「例を見てみましょう:」

「10 桁の数字が 100 万個あると想像してください。その後、各数字の hashCode を、数字を 100 で割った余りになるようにすることができます。」

以下に例を示します。

番号 私たちのハッシュコード
1234567890 90
9876554321 21
9876554221 21
9886554121 21

「はい、それは理にかなっています。それで、このハッシュコードをどうすればよいでしょうか?」

「数値を比較する代わりに、ハッシュコードを比較します。その方が高速です。」

「そして、ハッシュコードが等しい場合にのみ、equalsを呼び出します。」

「はい、そのほうが速いです。しかし、それでも 100 万件の比較を行う必要があります。私たちは小さい数値を比較しているだけで、ハッシュコードが一致する数値については依然として等しいものを呼び出す必要があります。」

「いいえ、はるかに少ない比較数で済みます。」

「私たちの Set が数値をhashCodeによってグループ化またはソートして保存していると想像してください(同じ hashCode を持つ数値が隣り合うため、この方法で数値をソートすることは、本質的にはグループ化することになります)。そうすれば、無関係なグループを非常に迅速かつ簡単に破棄できます。これで十分です。」グループごとに 1 回チェックして、そのハッシュコードがオブジェクトのハッシュコードと一致するかどうかを確認します。」

「あなたが学生で、見た目で認識でき、17 番寮に住んでいることがわかっている友人を探していると想像してください。そして、大学のすべての寮に行って、『ここは 17 番寮ですか?』と尋ねるだけです。「そうでない場合は、寮の全員を無視して次の寮に進みます。答えが『はい』であれば、友達を探しながら各部屋の前を歩き始めます。」

「この例では、寮番号 (17) がハッシュコードです。」

「hashCode 関数を実装する開発者は、次のことを知っておく必要があります。」

A)  2 つの異なるオブジェクトが同じハッシュコードを持つことができます (異なる人が同じ寮に住むことができます)

B) 同じオブジェクト ( equals メソッドに従って) は、同じ hashCode を持たなければなりません。

C) ハッシュコードは、同じハッシュコードを持つ異なるオブジェクトが多数存在しないように選択する必要があります。 もし存在する場合、ハッシュコードの潜在的な利点は失われます (17 寮に到着すると、大学の半分がそこに住んでいることがわかります。残念!)。

「そしてここで最も重要なことです。equalsメソッドをオーバーライドする場合は、必ずhashCode () メソッドをオーバーライドし、上記の 3 つのルールに従う必要があります。

その理由は次のとおりです。Javaでは、コレクション内のオブジェクトは常に、equals を使用して比較/取得される前に hashCode() を使用して比較/取得されます。 また、同一のオブジェクトが異なる hashCode を持つ場合、オブジェクトは異なるものとみなされ、equals メソッドが実行されます。呼ばれることもありません。

「分数の例では、ハッシュコードを分子と等しくすると、分数 2/3 と 4/6 は異なるハッシュコードを持つことになります。分数は同じであり、equals メソッドでは同じであると示されますが、それらのハッシュコードでは次のようになります。そして、equals を使用して比較する前に hashCode を使用して比較すると、オブジェクトは異なると結論付けられ、equals メソッドに到達することさえありません。」

以下に例を示します。

HashSet<Fraction>set = new HashSet<Fraction>();
set.add(new Fraction(2,3));System.out.println( set.contains(new Fraction(4,6)) );
hashCode() メソッドが分数の分子を返す 場合、結果はfalseになります。
そして、«new Fraction(4,6) » オブジェクトはコレクション内に見つかりません。

「では、分数の hashCode を実装する正しい方法は何でしょうか?」

「ここで、同等の分数は同じハッシュコードを持つ必要があることを覚えておく必要があります。」

バージョン 1 : ハッシュコードは整数除算の結果と等しくなります。」

「7/5 と 6/5 の場合、これは 1 になります。」

「4/5 と 3/5 の場合、これは 0 になります。」

「しかし、このオプションは、意図的に 1 より小さい分数を比較するのにはあまり適していません。hashCode (整数の除算の結果) は常に 0 になります。」

バージョン 2 : ハッシュコードは、分母を分子で整数で割った結果と等しくなります。」

「このオプションは、分数が 1 未満の場合に適しています。分数が 1 未満の場合、その逆数は 1 より大きくなります。そして、すべての分数を反転しても、比較にはまったく影響がありません。」

「私たちの最終バージョンは、両方のソリューションを組み合わせたものです。」

public int hashCode()
{
return numerator/denominator + denominator/numerator;
}

2/3 と 4/6 を使用してテストしてみましょう。それらは同一の hashCode を持つ必要があります。

分数2/3 分数4/6
分子 / 分母 2 / 3 == 0 4 / 6 == 0
分母 / 分子 3 / 2 == 1 6 / 4 == 1
分子 / 分母
+
分母 / 分子
0 + 1 == 1 0 + 1 == 1

"それは今のところすべてです。"

「ありがとう、エリー。とても面白かったです。」

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION