Javaのequals()およびhashCode()

Javaのequals()メソッドおよびhashCode()メソッドは、Objectクラスに存在します。したがって、すべてのJavaクラスはequals()およびhashCode()のデフォルトの実装を取得します。この記事では、Javaのequals()メソッドとhashCode()メソッドについて詳しく説明します。

Javaのequals()

Objectクラスは、equals()メソッドを次のように定義しています:

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

equals()メソッドのJavaのドキュメントによると、以下の原則に従った実装がされるべきです。

  • 任意のオブジェクトxに対して、x.equals(x)trueを返すべきです。
  • 任意の2つのオブジェクトxとyに対して、x.equals(y)trueを返すべきです。ただし、y.equals(x)trueを返す場合に限ります。
  • 複数のオブジェクトx、y、およびzに対して、x.equals(y)trueを返し、かつy.equals(z)trueを返す場合、x.equals(z)trueを返すべきです。
  • x.equals(y)の複数回の呼び出しは、オブジェクトのプロパティがequals()メソッドの実装で使用されている場合を除いて、同じ結果を返すべきです。
  • Objectクラスのequals()メソッドの実装は、両方の参照が同じオブジェクトを指している場合にのみtrueを返します。

JavaのhashCode()

JavaオブジェクトのhashCode()はネイティブメソッドであり、オブジェクトの整数ハッシュコード値を返します。hashCode()メソッドの一般契約は次のとおりです:

  • hashCode()の複数の呼び出しは、equals()メソッドで使用されているオブジェクトのプロパティが変更されない限り、同じ整数値を返さなければなりません。
  • オブジェクトのハッシュコード値は、同じアプリケーションの複数の実行で変更される可能性があります。
  • 2つのオブジェクトがequals()メソッドによって等しい場合、それらのハッシュコードは同じでなければなりません。
  • 2つのオブジェクトがequals()メソッドによって等しくない場合、それらのハッシュコードは異なっている必要はありません。ハッシュコード値が等しい場合もありますし、等しくない場合もあります。

equals()およびhashCode()メソッドの重要性

JavaのhashCode()およびequals()メソッドは、データの格納と取得のためのJavaのハッシュテーブルベースの実装で使用されます。詳細はJavaでのHashMapの動作で説明しています。equals()およびhashCode()の実装はこれらのルールに従う必要があります。

  • もしo1.equals(o2)ならば、o1.hashCode() == o2.hashCode()は常にtrueであるべきです。
  • o1.hashCode() == o2.hashCodeがtrueであるということは、o1.equals(o2)trueであるということではありません。

いつequals()およびhashCode()メソッドをオーバーライドする必要がありますか?

equals()メソッドをオーバーライドする場合、実装によって契約が違反されないように、通常hashCode()メソッドもオーバーライドする必要があります。 equals()およびhashCode()の契約が違反された場合、プログラムは例外をスローしません。クラスをハッシュテーブルのキーとして使用する予定がない場合、問題は発生しません。クラスをハッシュテーブルのキーとして使用する場合は、equals()およびhashCode()メソッドの両方をオーバーライドする必要があります。デフォルトのequals()およびhashCode()メソッドの実装に依存し、カスタムクラスをHashMapのキーとして使用する場合の動作を見てみましょう。

package com.journaldev.java;

public class DataKey {

	private String name;
	private int id;

        // ゲッターおよびセッターメソッド

	@Override
	public String toString() {
		return "DataKey [name=" + name + ", id=" + id + "]";
	}

}
package com.journaldev.java;

import java.util.HashMap;
import java.util.Map;

public class HashingTest {

	public static void main(String[] args) {
		Map<DataKey, Integer> hm = getAllData();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		Integer value = hm.get(dk);

		System.out.println(value);

	}

	private static Map<DataKey, Integer> getAllData() {
		Map<DataKey, Integer> hm = new HashMap<>();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		hm.put(dk, 10);

		return hm;
	}

}

上記のプログラムを実行すると、nullが表示されます。これは、オブジェクトhashCode()メソッドがキーを探すために使用されるためです。 HashMapのキーにアクセスできないため、データを取得するためにキーを再作成しています。両方のオブジェクトのハッシュコード値が異なるため、値が見つかりません。

equals()およびhashCode()メソッドの実装

私たちは自分自身のequals()とhashCode()メソッドの実装を定義することができますが、注意して実装しないと、実行時に奇妙な問題が発生する可能性があります。幸いなことに、ほとんどのIDEはこれらを自動的に実装する方法を提供しています。必要に応じて、要件に応じて変更することもできます。Eclipseを使用してequals()とhashCode()メソッドを自動生成できます。以下に、自動生成されたequals()とhashCode()メソッドの実装があります。

@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + id;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}

@Override
public boolean equals(Object obj) {
	if (this == obj)
		return true;
	if (obj == null)
		return false;
	if (getClass() != obj.getClass())
		return false;
	DataKey other = (DataKey) obj;
	if (id != other.id)
		return false;
	if (name == null) {
		if (other.name != null)
			return false;
	} else if (!name.equals(other.name))
		return false;
	return true;
}

注意してください。equals()とhashCode()メソッドの両方が計算に同じフィールドを使用しているため、その契約が有効なままです。テストプログラムを再実行すると、マップからオブジェクトを取得し、プログラムが10を出力します。また、Project Lombokを使用してequalsとhashCodeメソッドの実装を自動生成することもできます。

ハッシュ衝突とは何ですか

非常に単純に言えば、Javaのハッシュテーブルの実装は、以下のロジックを使用してgetおよびput操作を行います。

  1. まず、キーのハッシュコードを使用して使用する「バケット」を特定します。
  2. 同じハッシュコードを持つバケットにオブジェクトが存在しない場合、put操作のためにオブジェクトを追加し、get操作のためにnullを返します。
  3. もしバケット内に同じハッシュコードを持つ他のオブジェクトがある場合、”key”のequalsメソッドが働きます。
    • equals()がtrueを返し、put操作である場合、オブジェクトの値が上書きされます。
    • equals()がfalseを返し、put操作である場合、新しいエントリがバケットに追加されます。
    • equals()がtrueを返し、get操作である場合、オブジェクトの値が返されます。
    • equals()がfalseを返し、get操作である場合、nullが返されます。

下の画像はHashMapのバケットアイテムとそのequals()とhashCode()の関係を示しています。 2つのキーが同じハッシュコードを持つ場合の現象はハッシュ衝突と呼ばれます。hashCode()メソッドが適切に実装されていない場合、ハッシュ衝突の数が増え、マップエントリが適切に分散されず、getとputの操作が遅くなります。これがハッシュコード生成に素数が使用される理由です。それにより、マップエントリがすべてのバケットに適切に分散されるようになります。

hashCode()とequals()の両方を実装しない場合はどうなりますか?

上記で見たように、hashCode()が実装されていない場合、HashMapはエントリを探すためにハッシュコードを使用するため、値を取得できません。 hashCode()のみを使用してequals()を実装しない場合も、equals()メソッドがfalseを返すため、値が取得されません。

equals()とhashCode()メソッドを実装するためのベストプラクティス

  • 両方のequals()とhashCode()メソッドの実装で同じプロパティを使用して、プロパティが更新された場合に契約が違反しないようにします。
  • ハッシュテーブルのキーとして不変のオブジェクトを使用すると、ハッシュコードを毎回計算するのではなくキャッシュできるため、ベストプラクティスです。これが、文字列がハッシュテーブルのキーの良い候補である理由です。それは不変であり、ハッシュコードの値をキャッシュします。
  • hashCode()メソッドを実装して、最小限のハッシュ衝突が発生し、エントリがすべてのバケットに均等に分散されるようにします。

コードの完全なバージョンは、GitHubリポジトリからダウンロードできます。

Source:
https://www.digitalocean.com/community/tutorials/java-equals-hashcode