Java.util.Hashtableクラスの紹介

Java.util.Hashtableクラスの紹介

1. 概要

Hashtable is the oldest implementation of a hash table data structure in Java.HashMapは、JDK1.2で導入された2番目の実装です。

どちらのクラスも同様の機能を提供しますが、このチュートリアルで説明する小さな違いもあります。

2. Hashtableを使用する場合

各単語に定義がある辞書があるとしましょう。 また、辞書から単語をすばやく取得、挿入、削除する必要があります。

したがって、Hashtable(またはHashMap)は理にかなっています。 単語は一意であると想定されているため、Hashtableのキーになります。 一方、定義は値になります。

3. 使用例

辞書の例を続けましょう。 Wordをキーとしてモデル化します。

public class Word {
    private String name;

    public Word(String name) {
        this.name = name;
    }

    // ...
}

値がStringsであるとしましょう。 これで、Hashtableを作成できます。

Hashtable table = new Hashtable<>();

まず、エントリを追加しましょう。

Word word = new Word("cat");
table.put(word, "an animal");

また、エントリを取得するには:

String definition = table.get(word);

最後に、エントリを削除しましょう。

definition = table.remove(word);

クラスにはさらに多くのメソッドがあり、それらのいくつかについては後で説明します。

ただし、最初に、キーオブジェクトのいくつかの要件について説明しましょう。

4. hashCode()の重要性

To be used as a key in a Hashtable, the object mustn’t violate the hashCode() contract.要するに、等しいオブジェクトは同じコードを返す必要があります。 なぜハッシュテーブルがどのように編成されているかを見てみましょう。

Hashtableは配列を使用します。 配列内の各位置は「バケット」であり、ヌルにすることも、1つ以上のキーと値のペアを含めることもできます。 各ペアのインデックスが計算されます。

しかし、要素を連続して保存し、新しい要素を配列の最後に追加しないのはなぜですか?

ポイントは、インデックスで要素を見つけることは、要素を順番に比較して要素を反復処理するよりもはるかに速いということです。 したがって、キーをインデックスにマップする関数が必要です。

4.1. 直接アドレステーブル

このようなマッピングの最も簡単な例は、直接アドレステーブルです。 ここで、キーはインデックスとして使用されます。

index(k)=k,
where k is a key

キーは一意です。つまり、各バケットには1つのキーと値のペアが含まれます。 この手法は、可能な範囲が適度に小さい場合に整数キーに適しています。

しかし、ここには2つの問題があります。

  • まず、キーは整数ではなく、Wordオブジェクトです

  • 第二に、それらが整数である場合、だれもそれらが小さいことを保証しません。 キーが1、2、および1000000であると想像してください。 サイズが1000000で、要素が3つしかない大きな配列があり、残りは無駄なスペースになります

hashCode()メソッドは最初の問題を解決します。

Hashtableのデータ操作のロジックは、2番目の問題を解決します。

これについて詳しく説明しましょう。

4.2. hashCode()メソッド

すべてのJavaオブジェクトは、int値を返すhashCode()メソッドを継承します。 この値は、オブジェクトの内部メモリアドレスから計算されます。 デフォルトでは、hashCode()は個別のオブジェクトに対して個別の整数を返します。

したがって、any key object can be converted to an integer using hashCode()。 ただし、この整数は大きい場合があります。

4.3. 範囲を狭める

get()put()、およびremove()メソッドには、2番目の問題(可能な整数の範囲を縮小する)を解決するコードが含まれています。

数式はキーのインデックスを計算します:

int index = (hash & 0x7FFFFFFF) % tab.length;

ここで、tab.lengthは配列サイズであり、hashはキーのhashCode()メソッドによって返される数値です。

ご覧のとおり、index is a reminder of the division hash by the array sizeです。 等しいハッシュコードは同じインデックスを生成することに注意してください。

4.4. 衝突

さらに、different hash codes can produce the same indexでも。 これを衝突と呼びます。 衝突を解決するために、Hashtableはキーと値のペアのLinkedListを格納します。

このようなデータ構造は、連鎖を伴うハッシュテーブルと呼ばれます。

4.5. 負荷率

衝突は要素との操作を遅くすると推測するのは簡単です。 エントリを取得するには、そのインデックスを知るだけでは十分ではありませんが、リストを調べて各項目と比較する必要があります。

したがって、衝突の数を減らすことが重要です。 配列が大きいほど、衝突の可能性は小さくなります。 The load factor determines the balance between the array size and the performance.デフォルトでは0.75です。これは、バケットの75%が空でなくなると、配列サイズが2倍になることを意味します。 この操作はrehash()メソッドで実行されます。

しかし、キーに戻りましょう。

4.6. equals()とhashCode()をオーバーライドする。

Hashtableにエントリを入れてそこから取得すると、同じキーのインスタンスだけでなく、同じキーでも値を取得できることが期待されます。

Word word = new Word("cat");
table.put(word, "an animal");
String extracted = table.get(new Word("cat"));

等式のルールを設定するために、キーのequals()メソッドをオーバーライドします。

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Word))
        return false;

    Word word = (Word) o;
    return word.getName().equals(this.name);
}

ただし、equals()をオーバーライドするときにhashCode()をオーバーライドしないと、Hashtableがハッシュコードを使用してキーのインデックスを計算するため、2つの等しいキーが異なるバケットに配置される可能性があります。

上記の例をよく見てみましょう。 hashCode()をオーバーライドしないとどうなりますか?

  • ここでは、Wordの2つのインスタンスが関係しています。1つはエントリを配置するためのもので、もう1つはエントリを取得するためのものです。 これらのインスタンスは同じですが、それらのhashCode()メソッドは異なる数値を返します

  • 各キーのインデックスは、セクション4.3の式によって計算されます。 この式によれば、異なるハッシュコードは異なるインデックスを生成する可能性があります

  • つまり、エントリを1つのバケットに入れてから、他のバケットから取得しようとします。 このようなロジックはHashtableを壊します

等しいキーは等しいハッシュコードを返す必要があるため、hashCode()メソッドをオーバーライドします。

public int hashCode() {
    return name.hashCode();
}

it’s also recommended to make not equal keys return different hash codesに注意してください。そうしないと、同じバケットに入れられてしまいます。 これはパフォーマンスに影響を与えるため、Hashtableの利点の一部が失われます。

また、StringIntegerLong、または別のラッパータイプのキーは気にしないことに注意してください。 equal()メソッドとhashCode()メソッドの両方が、ラッパークラスですでにオーバーライドされています。

5. Hashtablesの反復

Hashtables. を反復する方法はいくつかあります。このセクションでは、それらについてよく説明し、いくつかの影響について説明します。

5.1. 早く失敗する:Iteration

フェイルファスト反復とは、Iterator isの作成後にHashtableが変更された場合、ConcurrentModificationExceptionがスローされることを意味します。 これをデモンストレーションしましょう。

まず、Hashtableを作成し、それにエントリを追加します。

Hashtable table = new Hashtable();
table.put(new Word("cat"), "an animal");
table.put(new Word("dog"), "another animal");

次に、Iteratorを作成します。

Iterator it = table.keySet().iterator();

そして3番目に、テーブルを変更します。

table.remove(new Word("dog"));

ここで、テーブルを反復処理しようとすると、ConcurrentModificationExceptionが得られます。

while (it.hasNext()) {
    Word key = it.next();
}
java.util.ConcurrentModificationException
    at java.util.Hashtable$Enumerator.next(Hashtable.java:1378)

ConcurrentModificationExceptionは、バグを見つけるのに役立ちます。たとえば、あるスレッドがテーブルを反復処理し、別のスレッドが同時にテーブルを変更しようとしている場合に、予期しない動作を回避できます。

5.2. 早く失敗しない:Enumeration

HashtableEnumerationはフェイルファストではありません。 例を見てみましょう。

まず、Hashtableを作成し、それにエントリを追加しましょう。

Hashtable table = new Hashtable();
table.put(new Word("1"), "one");
table.put(new Word("2"), "two");

次に、Enumerationを作成しましょう。

Enumeration enumKey = table.keys();

第三に、テーブルを変更しましょう。

table.remove(new Word("1"));

これで、テーブルを反復処理しても、例外はスローされません。

while (enumKey.hasMoreElements()) {
    Word key = enumKey.nextElement();
}

5.3. 予測できない反復順序

また、Hashtableの反復順序は予測不可能であり、エントリが追加された順序と一致しないことに注意してください。

キーのハッシュコードを使用して各インデックスを計算するため、これは理解できます。 さらに、再ハッシュは時々行われ、データ構造の順序を並べ替えます。

したがって、いくつかのエントリを追加して、出力を確認しましょう。

Hashtable table = new Hashtable();
    table.put(new Word("1"), "one");
    table.put(new Word("2"), "two");
    // ...
    table.put(new Word("8"), "eight");

    Iterator> it = table.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry entry = it.next();
        // ...
    }
}
five
four
three
two
one
eight
seven

6. Hashtable対。 HashMap

HashtableHashMapは、非常によく似た機能を提供します。

両方とも提供します:

  • フェイルファースト反復

  • 予測不可能な反復順序

しかし、いくつかの違いもあります。

  • HashMapEnumeration, while Hashtableを提供せず、フェイルファストを提供しませんEnumeration

  • Hashtablenullキーとnull値を許可しませんが、HashMapは1つのnullキーと任意の数のnull値を許可します

  • Hashtableのメソッドは同期されますが、HashMapsのメソッドは同期されません

7. Java 8のHashtable API

Java 8では、コードを簡潔にする新しいメソッドが導入されました。 特に、いくつかのifブロックを取り除くことができます。 これをデモンストレーションしましょう。

7.1. getOrDefault()

「dog” 」という単語の定義を取得し、それがテーブルにある場合はそれを変数に割り当てる必要があるとします。 そうでない場合は、変数に「not found」を割り当てます。

Java 8より前:

Word key = new Word("dog");
String definition;

if (table.containsKey(key)) {
     definition = table.get(key);
} else {
     definition = "not found";
}

Java 8以降:

definition = table.getOrDefault(key, "not found");

7.2. putIfAbsent()

まだ辞書に載っていない場合にのみ、「cat」という単語を入力する必要があるとします。

Java 8より前:

if (!table.containsKey(new Word("cat"))) {
    table.put(new Word("cat"), definition);
}

Java 8以降:

table.putIfAbsent(new Word("cat"), definition);

7.3. boolean remove()

「猫」という単語を削除する必要があるとしましょう。ただし、その定義が「動物」である場合に限ります。

Java 8より前:

if (table.get(new Word("cat")).equals("an animal")) {
    table.remove(new Word("cat"));
}

Java 8以降:

boolean result = table.remove(new Word("cat"), "an animal");

最後に、古いremove()メソッドは値を返しますが、新しいメソッドはbooleanを返します。

7.4. replace()

「猫」の定義を置き換える必要があるとしましょう。ただし、その古い定義が「飼いならされた小さな肉食哺乳類」である場合に限ります。

Java 8より前:

if (table.containsKey(new Word("cat"))
    && table.get(new Word("cat")).equals("a small domesticated carnivorous mammal")) {
    table.put(new Word("cat"), definition);
}

Java 8以降:

table.replace(new Word("cat"), "a small domesticated carnivorous mammal", definition);

7.5. computeIfAbsent()

この方法はputIfabsent()に似ています。 ただし、putIfabsent()は値を直接取得し、computeIfAbsent()はマッピング関数を取得します。 キーをチェックした後にのみ値を計算します。これは、特に値を取得するのが難しい場合に、より効率的です。

table.computeIfAbsent(new Word("cat"), key -> "an animal");

したがって、上記の行は次と同等です。

if (!table.containsKey(cat)) {
    String definition = "an animal"; // note that calculations take place inside if block
    table.put(new Word("cat"), definition);
}

7.6. computeIfPresent()

この方法は、replace()の方法に似ています。 ただし、ここでも、replace()は値を直接取得し、computeIfPresent()はマッピング関数を取得します。 ifブロック内の値を計算するため、より効率的です。

定義を変更する必要があるとしましょう。

table.computeIfPresent(cat, (key, value) -> key.getName() + " - " + value);

したがって、上記の行は次と同等です。

if (table.containsKey(cat)) {
    String concatination=cat.getName() + " - " + table.get(cat);
    table.put(cat, concatination);
}

7.7. compute()

次に、別のタスクを解決します。 Stringの配列があり、要素が一意ではないとします。 また、配列で取得できる文字列の出現回数を計算してみましょう。 これが配列です。

String[] animals = { "cat", "dog", "dog", "cat", "bird", "mouse", "mouse" };

また、キーとして動物を含み、値としてその出現回数を含むHashtableを作成します。

これが解決策です。

Hashtable table = new Hashtable();

for (String animal : animals) {
    table.compute(animal,
        (key, value) -> (value == null ? 1 : value + 1));
}

最後に、テーブルに2匹の猫、2匹の犬、1羽の鳥、2匹のマウスが含まれていることを確認しましょう。

assertThat(table.values(), hasItems(2, 2, 2, 1));

7.8. merge()

上記のタスクを解決する別の方法があります。

for (String animal : animals) {
    table.merge(animal, 1, (oldValue, value) -> (oldValue + value));
}

2番目の引数1は、キーがまだテーブルにない場合にキーにマップされる値です。 キーがすでにテーブルにある場合は、oldValue+1として計算します。

7.9. foreach()

これは、エントリを反復処理する新しい方法です。 すべてのエントリを印刷してみましょう。

table.forEach((k, v) -> System.out.println(k.getName() + " - " + v)

7.10. replaceAll()

さらに、反復なしですべての値を置き換えることができます。

table.replaceAll((k, v) -> k.getName() + " - " + v);

8. 結論

この記事では、ハッシュテーブル構造の目的を説明し、直接アドレステーブル構造を複雑にしてそれを取得する方法を示しました。

さらに、衝突とは何か、Hashtable.の負荷率とは何かについても説明しました。また、キーオブジェクトのequals()hashCode()をオーバーライドする理由についても学びました。

最後に、HashtableのプロパティとJava8固有のAPIについて説明しました。

いつものように、完全なソースコードはon Githubで入手できます。