Java TreeMapとHashMapの比較

Java TreeMapとHashMap

1. 前書き

この記事では、2つのMap実装(TreeMapHashMap)を比較します。

どちらの実装も、JavaCollectionsフレームワークの不可欠な部分を形成し、データをkey-valueペアとして格納します。

2. 違い

2.1. 実装

最初に、ハッシュテーブルベースの実装であるHashMapについて説明します。 AbstractMapクラスを拡張し、Mapインターフェイスを実装します。 HashMapは、hashingの原則に基づいて機能します。

このMap実装は通常、バケット化されたhash tableとして機能しますが、when buckets get too large, they get transformed into nodes of TreeNodesは、それぞれjava.util.TreeMap.と同様に構造化されています。

the article focused on itHashMap’s内部の詳細を見つけることができます。

一方、TreeMapAbstractMapクラスを拡張し、NavigableMapインターフェイスを実装します。 TreeMapは、マップ要素をRed-Blackツリー(Self-Balancing Binary Search Tree)に格納します。

また、the article focused on it hereTreeMap’s内部についても詳しく知ることができます。

2.2. 注文

HashMap doesn’t provide any guarantee over the way the elements are arranged in the Map

つまり、we can’t assume any order while iterating over keys and values of a HashMap:

@Test
public void whenInsertObjectsHashMap_thenRandomOrder() {
    Map hashmap = new HashMap<>();
    hashmap.put(3, "TreeMap");
    hashmap.put(2, "vs");
    hashmap.put(1, "HashMap");

    assertThat(hashmap.keySet(), containsInAnyOrder(1, 2, 3));
}

ただし、TreeMapの項目はsorted according to their natural orderです。

TreeMapオブジェクトを自然な順序で並べ替えることができない場合は、ComparatorまたはComparableを使用して、要素がMap:内に配置される順序を定義できます。

@Test
public void whenInsertObjectsTreeMap_thenNaturalOrder() {
    Map treemap = new TreeMap<>();
    treemap.put(3, "TreeMap");
    treemap.put(2, "vs");
    treemap.put(1, "HashMap");

    assertThat(treemap.keySet(), contains(1, 2, 3));
}

2.3. Null

HashMapでは、最大1つのnullkeyと多数のnull値を格納できます。

例を見てみましょう:

@Test
public void whenInsertNullInHashMap_thenInsertsNull() {
    Map hashmap = new HashMap<>();
    hashmap.put(null, null);

    assertNull(hashmap.get(null));
}

ただし、TreeMapnullkeyを許可しませんが、多くのnull値を含む可能性があります。

compareTo()またはcompare()メソッドがNullPointerException:をスローするため、nullキーは許可されません

@Test(expected = NullPointerException.class)
public void whenInsertNullInTreeMap_thenException() {
    Map treemap = new TreeMap<>();
    treemap.put(null, "NullPointerException");
}

ユーザー定義のComparatorTreeMapを使用している場合、null値がどのように処理されるかはcompare()メソッドの実装に依存します。

3. パフォーマンス分析

パフォーマンスは、ユースケースを考慮してデータ構造の適合性を理解するのに役立つ最も重要なメトリックです。

このセクションでは、HashMapTreeMap.のパフォーマンスの包括的な分析を提供します

3.1. HashMap

HashMap,はハッシュテーブルベースの実装であり、内部的に配列ベースのデータ構造を使用して、hash functionに従って要素を編成します。

HashMapは、add()remove()contains().などのほとんどの操作で期待される一定時間のパフォーマンスO(1)を提供します。したがって、TreeMapよりも大幅に高速です。

ハッシュテーブルで妥当な仮定の下で要素を検索する平均時間はO(1).ですが、hash functionの実装が不適切な場合、バケット内の値の分散が不十分になり、次のような結果になる可能性があります。

  • メモリオーバーヘッド-多くのバケットが未使用のまま

  • パフォーマンス低下衝突の数が多いほど、パフォーマンスは低くなります

Before Java 8, Separate Chaining was the only preferred way to handle collisions.通常、リンクリストi.e.を使用して実装されます。衝突がある場合、または2つの異なる要素のハッシュ値が同じである場合は、両方のアイテムを同じリンクリストに格納します。

したがって、最悪の場合、HashMap,内の要素を検索するのに、リンクリスト内の要素を検索するのにi.e.O(n)時間かかる可能性があります。

ただし、JEP 180が登場するにつれ、要素の配置方法の実装に微妙な変更が加えられました。 HashMap。

仕様によれば、バケットが大きくなりすぎて十分なノードが含まれると、バケットはTreeNodesのモードに変換され、それぞれがTreeMapのモードと同様に構造化されます。

したがって、ハッシュの衝突が多い場合、最悪の場合のパフォーマンスはO(n)からO(log n).に向上します。

この変換を実行するコードを以下に示します。

if(binCount >= TREEIFY_THRESHOLD - 1) {
    treeifyBin(tab, hash);
}

TREEIFY_THRESHOLDの値は8であり、バケットのリンクリストではなくツリーを使用するためのしきい値カウントを効果的に示します。

それは明白です:

  • HashMapは、データを保持するために必要なメモリよりもはるかに多くのメモリを必要とします

  • HashMapは、70%〜75%を超えてはなりません。 近くなると、サイズが変更され、エントリが再ハッシュされます

  • 再ハッシュにはn操作が必要ですが、これにはコストがかかり、一定時間の挿入はO(n)のオーダーになります。

  • HashMapにオブジェクトを挿入する順序を決定するのはハッシュアルゴリズムです

The performance of a HashMap can be tuned by setting the custom initial capacity and the load factorHashMapオブジェクトの作成時。

ただし、次の場合はHashMapを選択する必要があります。

  • コレクションに保持するアイテムの数をおおよそ知っています

  • 自然な順序でアイテムを抽出したくない

上記の状況では、HashMapは一定時間の挿入、検索、および削除を提供するため、最良の選択です。

3.2. TreeMap

TreeMapは、カスタムComparator.を使用して要素を並べ替えることができる階層ツリーに、データを格納します。

そのパフォーマンスの要約:

  • TreeMapは、add()remove()contains()などのほとんどの操作でO(log(n))のパフォーマンスを提供します

  • Treemapは、メモリの連続領域を使用するHashMapとは異なり、アイテムを保持するために必要なメモリ量のみを使用するため、メモリを節約できます(HashMap)と比較して)

  • ツリーは、意図したパフォーマンスを維持するためにバランスを維持する必要があります。これにはかなりの労力が必要であるため、実装が複雑になります。

次の場合は常にTreeMapを選択する必要があります。

  • メモリ制限を考慮する必要があります

  • メモリに保存する必要のあるアイテムの数がわかりません

  • 自然な順序でオブジェクトを抽出したい

  • アイテムが常に追加および削除される場合

  • O(log n)の検索時間を喜んで受け入れます

4. 類似点

4.1. ユニークな要素

TreeMapHashMapはどちらも重複キーをサポートしていません。 追加された場合、前の要素を上書きします(エラーまたは例外なし):

@Test
public void givenHashMapAndTreeMap_whenputDuplicates_thenOnlyUnique() {
    Map treeMap = new HashMap<>();
    treeMap.put(1, "example");
    treeMap.put(1, "example");

    assertTrue(treeMap.size() == 1);

    Map treeMap2 = new TreeMap<>();
    treeMap2.put(1, "example");
    treeMap2.put(1, "example");

    assertTrue(treeMap2.size() == 1);
}

4.2. 同時アクセス

Both Map implementations aren’t synchronizedであり、同時アクセスを自分で管理する必要があります。

複数のスレッドが同時にアクセスし、少なくとも1つのスレッドがそれらを変更する場合は、両方を外部で同期する必要があります。

提供されたマップの同期ビューを取得するには、Collections.synchronizedMap(mapName)を明示的に使用する必要があります。

4.3. フェイルファストイテレータ

Mapが何らかの方法で変更された場合、およびイテレータが作成された後はいつでも、IteratorConcurrentModificationExceptionをスローします。

さらに、イテレータのremoveメソッドを使用して、反復中にMapを変更できます。

例を見てみましょう:

@Test
public void whenModifyMapDuringIteration_thenThrowExecption() {
    Map hashmap = new HashMap<>();
    hashmap.put(1, "One");
    hashmap.put(2, "Two");

    Executable executable = () -> hashmap
      .forEach((key,value) -> hashmap.remove(1));

    assertThrows(ConcurrentModificationException.class, executable);
}

5. どの実装を使用しますか?

一般に、両方の実装にはそれぞれ長所と短所がありますが、it’s about understanding the underlying expectation and requirement which must govern our choice regarding the same.

要約すると:

  • エントリを並べ替えたままにする場合は、TreeMapを使用する必要があります

  • メモリ消費よりもパフォーマンスを優先する場合は、HashMapを使用する必要があります

  • TreeMapはより重要な局所性を持っているので、自然な順序に従って互いに比較的近いオブジェクトにアクセスしたい場合は、それを検討するかもしれません。

  • HashMapは、initialCapacityloadFactorを使用して調整できますが、TreeMapでは調整できません。

  • 一定時間のアクセスの恩恵を受けながら挿入順序を維持したい場合は、LinkedHashMapを使用できます

6. 結論

この記事では、TreeMapHashMapの違いと類似点を示しました。

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