JavaにおけるhashCode()の手引き

JavaのhashCode()ガイド

1. 概要

ハッシュはコンピューターサイエンスの基本概念です。

Javaでは、効率的なハッシュアルゴリズムが、HashMapなどの最も人気のあるコレクションの背後にあります(HashMapの詳細については、this articleを確認してください)。およびHashSet.

この記事では、hashCode()がどのように機能するか、コレクションでどのように再生されるか、およびそれを正しく実装する方法に焦点を当てます。

2. データ構造でのhashCode()の使用

コレクションに対する最も単純な操作は、特定の状況では非効率的です。

たとえば、これにより、巨大なサイズのリストでは非常に効果のない線形検索がトリガーされます。

List words = Arrays.asList("Welcome", "to", "example");
if (words.contains("example")) {
    System.out.println("example is in the list");
}

Javaは、この問題を具体的に処理するための多数のデータ構造を提供します。たとえば、いくつかのMapインターフェース実装はhash tables.です。

ハッシュテーブルを使用する場合、these collections calculate the hash value for a given key using the hashCode() methodを使用し、この値を内部的に使用してデータを格納します。これにより、アクセス操作がはるかに効率的になります。

3. hashCode()のしくみを理解する

簡単に言えば、hashCode()は、ハッシュアルゴリズムによって生成された整数値を返します。

equals()による)等しいオブジェクトは、同じハッシュコードを返す必要があります。 It’s not required for different objects to return different hash codes.

hashCode()の一般契約は次のように述べています。

  • Javaアプリケーションの実行中に同じオブジェクトで複数回呼び出される場合は常に、オブジェクトのequals比較で使用される情報が変更されていない限り、hashCode()は常に同じ値を返す必要があります。 この値は、アプリケーションのある実行から同じアプリケーションの別の実行まで一貫性を保つ必要はありません。

  • equals(Object)メソッドに従って2つのオブジェクトが等しい場合、2つのオブジェクトのそれぞれでhashCode()メソッドを呼び出すと、同じ値が生成される必要があります。

  • equals(java.lang.Object)メソッドに従って2つのオブジェクトが等しくない場合、2つのオブジェクトのそれぞれでhashCodeメソッドを呼び出すと、異なる整数の結果が生成される必要はありません。 ただし、開発者は、等しくないオブジェクトに対して異なる整数結果を生成すると、ハッシュテーブルのパフォーマンスが向上することに注意する必要があります。

「合理的に実用的である限り、クラスObjectによって定義されたhashCode()メソッドは、個別のオブジェクトに対して個別の整数を返します。 (これは通常、オブジェクトの内部アドレスを整数に変換することで実装されますが、この実装手法はJavaTMプログラミング言語では必要ありません。)

4. ナイーブなhashCode()の実装

上記の契約に完全に準拠する単純なhashCode()実装を使用することは、実際には非常に簡単です。

これを示すために、メソッドのデフォルトの実装をオーバーライドするサンプルのUserクラスを定義します。

public class User {

    private long id;
    private String name;
    private String email;

    // standard getters/setters/constructors

    @Override
    public int hashCode() {
        return 1;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null) return false;
        if (this.getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id
          && (name.equals(user.name)
          && email.equals(user.email));
    }

    // getters and setters here
}

Userクラスは、それぞれのコントラクトに完全に準拠するequals()hashCode()の両方のカスタム実装を提供します。 さらに、hashCode()が固定値を返すことで違法なことは何もありません。

ただし、この実装では、すべてのオブジェクトが同じ単一のバケットに格納されるため、ハッシュテーブルの機能が基本的にゼロに低下します。

このコンテキストでは、ハッシュテーブルルックアップは直線的に実行され、実際の利点はありません。これについてはセクション7で詳しく説明します。

5. hashCode()実装の改善

Userクラスのすべてのフィールドを含めることにより、現在のhashCode()実装を少し改善して、等しくないオブジェクトに対して異なる結果を生成できるようにします。

@Override
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode();
}

この基本的なハッシュアルゴリズムは、nameフィールドとemailフィールドおよびidのハッシュコードを乗算するだけでオブジェクトのハッシュコードを計算するため、前のアルゴリズムよりも明らかに優れています。

一般的に、equals()の実装と一貫性を保つ限り、これは妥当なhashCode()の実装であると言えます。

6. 標準のhashCode()実装

ハッシュコードの計算に使用するハッシュアルゴリズムが優れているほど、ハッシュテーブルのパフォーマンスは向上します。

2つの素数を使用して、計算されたハッシュコードにさらに一意性を追加する「標準」実装を見てみましょう。

@Override
public int hashCode() {
    int hash = 7;
    hash = 31 * hash + (int) id;
    hash = 31 * hash + (name == null ? 0 : name.hashCode());
    hash = 31 * hash + (email == null ? 0 : email.hashCode());
    return hash;
}

hashCode()およびequals()メソッドが果たす役割を理解することは不可欠ですが、ほとんどのIDEはカスタムhashCode()およびequals()を生成できるため、毎回最初から実装する必要はありません。 )sの実装と、Java 7以降、快適なハッシュのためのObjects.hash()ユーティリティメソッドを取得しました。

Objects.hash(name, email)

IntelliJ IDEAは、次の実装を生成します。

@Override
public int hashCode() {
    int result = (int) (id ^ (id >>> 32));
    result = 31 * result + name.hashCode();
    result = 31 * result + email.hashCode();
    return result;
}

そして、Eclipseはこれを生成します:

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

上記のIDEベースのhashCode()実装に加えて、たとえばLombokを使用して、効率的な実装を自動的に生成することもできます。 この場合、lombok-maven依存関係をpom.xmlに追加する必要があります。


    org.projectlombok
    lombok-maven
    1.16.18.0
    pom

これで、Userクラスに@EqualsAndHashCodeアノテーションを付けるだけで十分です。

@EqualsAndHashCode
public class User {
    // fields and methods here
}

同様に、Apache Commons Lang’s HashCodeBuilder classhashCode()実装を生成する場合は、commons-langMaven依存関係をpomファイルに含める必要があります。


    commons-lang
    commons-lang
    2.6

そして、hashCode()は次のように実装できます。

public class User {
    public int hashCode() {
        return new HashCodeBuilder(17, 37).
        append(id).
        append(name).
        append(email).
        toHashCode();
    }
}

一般に、hashCode()の実装に関しては、固執する普遍的なレシピはありません。 効率的なハッシュアルゴリズムを実装するためのthorough guidelinesのリストを提供するJoshua Bloch’s Effective Javaを読むことを強くお勧めします。

ここで気づくことができるのは、これらの実装はすべて何らかの形で番号31を利用していることです。これは31が素晴らしい特性を持っているためです。

31 * i == (i << 5) - i

7. ハッシュ衝突の処理

ハッシュテーブルの固有の動作により、これらのデータ構造の関連する側面が浮き彫りになります。効率的なハッシュアルゴリズムを使用しても、2つ以上のオブジェクトは、等しくない場合でも同じハッシュコードを持つ可能性があります。 そのため、ハッシュテーブルキーが異なる場合でも、それらのハッシュコードは同じバケットを指します。

この状況は一般にハッシュ衝突およびvarious methodologies exist for handling itとして知られており、それぞれに長所と短所があります。 JavaのHashMapは、衝突の処理にthe separate chaining methodを使用します。

「2つ以上のオブジェクトが同じバケットを指している場合、それらは単にリンクリストに保存されます。 このような場合、ハッシュテーブルはリンクリストの配列であり、同じハッシュを持つ各オブジェクトは、配列のバケットインデックスでリンクリストに追加されます。

In the worst case, several buckets would have a linked list bound to it, and the retrieval of an object in the list would be performed linearly。」

ハッシュ衝突の方法論は、hashCode()を効率的に実装することが非常に重要である理由を一言で示しています.

Java 8は興味深いenhancement to HashMap implementationをもたらしました–バケットサイズが特定のしきい値を超えると、リンクリストはツリーマップに置き換えられます。 これにより、悲観的なO(n)の代わりにO(logn _)_ルックアップを実現できます。

8. 簡単なアプリケーションの作成

標準のhashCode()実装の機能をテストするために、いくつかのUserオブジェクトをHashMapに追加し、SLF4Jを使用してコンソールにメッセージを記録する単純なJavaアプリケーションを作成しましょう。メソッドが呼び出されるたび。

サンプルアプリケーションのエントリポイントは次のとおりです。

public class Application {

    public static void main(String[] args) {
        Map users = new HashMap<>();
        User user1 = new User(1L, "John", "[email protected]");
        User user2 = new User(2L, "Jennifer", "[email protected]");
        User user3 = new User(3L, "Mary", "[email protected]");

        users.put(user1, user1);
        users.put(user2, user2);
        users.put(user3, user3);
        if (users.containsKey(user1)) {
            System.out.print("User found in the collection");
        }
    }
}

そしてこれはhashCode()の実装です:

public class User {

    // ...

    public int hashCode() {
        int hash = 7;
        hash = 31 * hash + (int) id;
        hash = 31 * hash + (name == null ? 0 : name.hashCode());
        hash = 31 * hash + (email == null ? 0 : email.hashCode());
        logger.info("hashCode() called - Computed hash: " + hash);
        return hash;
    }
}

ここで強調する価値のある唯一の詳細は、オブジェクトがハッシュマップに格納され、containsKey()メソッドでチェックされるたびに、hashCode()が呼び出され、計算されたハッシュコードがコンソールに出力されることです。

[main] INFO com.example.entities.User - hashCode() called - Computed hash: 1255477819
[main] INFO com.example.entities.User - hashCode() called - Computed hash: -282948472
[main] INFO com.example.entities.User - hashCode() called - Computed hash: -1540702691
[main] INFO com.example.entities.User - hashCode() called - Computed hash: 1255477819
User found in the collection

9. 結論

効率的なhashCode()実装を作成するには、多くの場合、いくつかの数学的概念を組み合わせる必要があることは明らかです(つまり、 素数および任意数)、論理的および基本的な数学演算。

いずれにせよ、ハッシュアルゴリズムが等しくないオブジェクトに対して異なるハッシュコードを生成し、equals()の実装と一貫性があることを確認する限り、これらの手法にまったく頼らずにhashCode()を効果的に実装することは完全に可能です。

いつものように、この記事に示されているすべてのコード例は利用可能なover on GitHubです。