JavaにおけるHashSetの手引き

JavaのHashSetガイド

1. 概要

この記事では、HashSet.について詳しく説明します。これは、最も人気のあるSetの実装のひとつであり、Javaコレクションフレームワークの不可欠な部分です。

2. HashSetの紹介

HashSetは、JavaコレクションAPIの基本的なデータ構造の1つです.

この実装の最も重要な側面を思い出してみましょう。

  • 一意の要素を格納し、nullを許可します

  • HashMapに支えられています

  • 挿入順序を維持しません

  • スレッドセーフではありません

この内部HashMapは、HashSetのインスタンスが作成されるときに初期化されることに注意してください。

public HashSet() {
    map = new HashMap<>();
}

HashMapがどのように機能するかを詳しく知りたい場合は、the article focused on it hereを読むことができます。

3. API

このセクションでは、最も一般的に使用される方法を確認し、いくつかの簡単な例を見ていきます。

3.1. add()

add()メソッドは、要素をセットに追加するために使用できます。 The method contract states that an element will be added only when it isn’t already present in a set.要素が追加された場合、メソッドはtrue,を返します。それ以外の場合–false.

次のように要素をHashSetに追加できます。

@Test
public void whenAddingElement_shouldAddElement() {
    Set hashset = new HashSet<>();

    assertTrue(hashset.add("String Added"));
}

実装の観点から、addメソッドは非常に重要です。 実装の詳細は、HashSetが内部でどのように機能し、HashMap’sputメソッドを活用するかを示しています。

public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

map変数は、内部のバッキングHashMap:への参照です。

private transient HashMap map;

最初にhashcodeに精通して、要素がハッシュベースのデータ構造でどのように編成されているかを詳細に理解することをお勧めします。

要約すると:

  • HashMapは、デフォルトの容量が16要素のbucketsの配列です。各バケットは、異なるハッシュコード値に対応します。

  • さまざまなオブジェクトのハッシュコード値が同じ場合、それらは単一のバケットに保存されます

  • load factorに達すると、新しい配列が前の配列の2倍のサイズで作成され、すべての要素が再ハッシュされ、対応する新しいバケットに再配布されます。

  • 値を取得するには、キーをハッシュして変更し、対応するバケットに移動して、オブジェクトが複数ある場合は潜在的なリンクリストを検索します。

3.2. contains()

The purpose of the contains method is to check if an element is present in a given HashSet.要素が見つかった場合はtrueを返し、それ以外の場合はfalse.を返します。

HashSetの要素を確認できます。

@Test
public void whenCheckingForElement_shouldSearchForElement() {
    Set hashsetContains = new HashSet<>();
    hashsetContains.add("String Added");

    assertTrue(hashsetContains.contains("String Added"));
}

オブジェクトがこのメソッドに渡されるたびに、ハッシュ値が計算されます。 次に、対応するバケットの場所が解決され、トラバースされます。

3.3. remove()

指定された要素が存在する場合、このメソッドはその要素をセットから削除します。 セットに指定された要素が含まれている場合、このメソッドはtrueを返します。

実用的な例を見てみましょう。

@Test
public void whenRemovingElement_shouldRemoveElement() {
    Set removeFromHashSet = new HashSet<>();
    removeFromHashSet.add("String Added");

    assertTrue(removeFromHashSet.remove("String Added"));
}

3.4. clear()

セットからすべてのアイテムを削除する場合は、このメソッドを使用します。 基礎となる実装は、基礎となるHashMap.からすべての要素を単純にクリアします

実際の動作を見てみましょう。

@Test
public void whenClearingHashSet_shouldClearHashSet() {
    Set clearHashSet = new HashSet<>();
    clearHashSet.add("String Added");
    clearHashSet.clear();

    assertTrue(clearHashSet.isEmpty());
}

3.5. size()

これは、APIの基本的なメソッドの1つです。 HashSetに存在する要素の数を特定するのに役立つため、頻繁に使用されます。 基礎となる実装は、計算をHashMap’s size()メソッドに委任するだけです。

実際の動作を見てみましょう。

@Test
public void whenCheckingTheSizeOfHashSet_shouldReturnThesize() {
    Set hashSetSize = new HashSet<>();
    hashSetSize.add("String Added");

    assertEquals(1, hashSetSize.size());
}

3.6. isEmpty()

このメソッドを使用して、HashSetの特定のインスタンスが空であるかどうかを判断できます。 セットに要素が含まれていない場合、このメソッドはtrueを返します。

@Test
public void whenCheckingForEmptyHashSet_shouldCheckForEmpty() {
    Set emptyHashSet = new HashSet<>();

    assertTrue(emptyHashSet.isEmpty());
}

3.7. iterator()

このメソッドは、Setの要素に対するイテレーターを返します。 The elements are visited in no particular order and iterators are fail-fast

ここで、ランダムな繰り返し順序を観察できます。

@Test
public void whenIteratingHashSet_shouldIterateHashSet() {
    Set hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator itr = hashset.iterator();
    while(itr.hasNext()){
        System.out.println(itr.next());
    }
}

イテレータが作成された後、イテレータ独自のremoveメソッド以外の方法でセットが変更された場合、IteratorConcurrentModificationExceptionをスローします。

実際の動作を見てみましょう。

@Test(expected = ConcurrentModificationException.class)
public void whenModifyingHashSetWhileIterating_shouldThrowException() {

    Set hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator itr = hashset.iterator();
    while (itr.hasNext()) {
        itr.next();
        hashset.remove("Second");
    }
}

または、イテレータのremoveメソッドを使用した場合、例外は発生しませんでした。

@Test
public void whenRemovingElementUsingIterator_shouldRemoveElement() {

    Set hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator itr = hashset.iterator();
    while (itr.hasNext()) {
        String element = itr.next();
        if (element.equals("Second"))
            itr.remove();
    }

    assertEquals(2, hashset.size());
}

同期されていない同時変更が存在する場合、ハード保証を行うことは不可能であるため、イテレータのフェイルファスト動作は保証できません。

フェイルファストイテレータは、ベストエフォートベースでConcurrentModificationExceptionをスローします。 したがって、その正確性をこの例外に依存するプログラムを作成するのは誤りです。

4. HashSetはどのように一意性を維持しますか?

オブジェクトをHashSetに入れると、オブジェクトのhashcode値を使用して、要素がまだセットに含まれていないかどうかを判断します。

各ハッシュコード値は、計算されたハッシュ値が同じであるさまざまな要素を含むことができる特定のバケットの場所に対応します。 But two objects with the same hashCode might not be equal

したがって、同じバケット内のオブジェクトは、equals()メソッドを使用して比較されます。

5. HashSetのパフォーマンス

HashSetのパフォーマンスは、主に2つのパラメーター(Initial CapacityLoad Factor)の影響を受けます。

セットに要素を追加する際に予想される時間計算量はO(1)であり、最悪のシナリオ(バケットが1つしかない)ではO(n)に低下する可能性があります。したがって、it’s essential to maintain the right HashSet’s capacity.

重要な注意:JDK 8以降、the worst case time complexity is O(log*n).

負荷係数は、最大充填レベルを示し、それを超えると、セットのサイズを変更する必要があります。

initial capacityおよびload factorのカスタム値を使用してHashSetを作成することもできます。

Set hashset = new HashSet<>();
Set hashset = new HashSet<>(20);
Set hashset = new HashSet<>(20, 0.5f);

最初のケースでは、デフォルト値が使用されます。初期容量は16で、負荷係数は0.75です。 2番目では、デフォルトの容量をオーバーライドし、3番目では、両方をオーバーライドします。

初期容量が低いと、スペースの複雑さが軽減されますが、再ハッシュの頻度が高くなり、コストのかかるプロセスになります。

一方、a high initial capacity increases the cost of iteration and the initial memory consumption.

経験則として:

  • 高い初期容量は、反復がほとんどないかまったくない多数のエントリに適しています

  • 低い初期容量は、反復の多い少数のエントリに適しています

したがって、この2つのバランスを正しく取ることが非常に重要です。 通常、デフォルトの実装は最適化されており、正常に機能します。要件に合わせてこれらのパラメーターを調整する必要があると感じた場合は、慎重に行う必要があります。

6. 結論

この記事では、HashSetの有用性、その目的、およびその基礎となる動作について概説しました。 一定の時間パフォーマンスと重複を回避する能力を考えると、使いやすさの点でどれほど効率的であるかを見ました。

APIの重要なメソッドのいくつかを調査し、開発者がHashSetをその可能性に使用するのにどのように役立つかを調べました。

いつものように、コードスニペットはover on GitHubで見つけることができます。