JavaでのLinkedHashMapの手引き

JavaのLinkedHashMapのガイド

1. 概要

この記事では、LinkedHashMapクラスの内部実装について説明します。 LinkedHashMapは、Mapインターフェイスの一般的な実装です。

この特定の実装はHashMapのサブクラスであるため、HashMap implementationのコアビルディングブロックを共有します。 したがって、この記事に進む前に、それをブラッシュアップすることを強くお勧めします。

2. LinkedHashMapHashMap

LinkedHashMapクラスは、ほとんどの点でHashMapと非常によく似ています。 ただし、リンクハッシュマップはハッシュテーブルとリンクリストの両方に基づいており、ハッシュマップの機能を強化しています。

デフォルトのサイズ16の基本配列に加えて、すべてのエントリを介して実行される二重リンクリストを維持します。

要素の順序を維持するために、リンクされたハッシュマップは、次および前のエントリへのポインタを追加することにより、HashMapMap.Entryクラスを変更します。

static class Entry extends HashMap.Node {
    Entry before, after;
    Entry(int hash, K key, V value, Node next) {
        super(hash, key, value, next);
    }
}

Entryクラスは単に2つのポインターを追加するだけであることに注意してください。 beforeおよびafterは、リンクリストに自身をフックできるようにします。 それとは別に、HashMapのEntryクラス実装を使用します。

最後に、このリンクされたリストは反復の順序を定義することに注意してください。デフォルトでは、要素の挿入の順序(挿入順序)です。

3. 挿入-注文LinkedHashMap

マップへの挿入方法に従ってエントリを並べ替える、リンクされたハッシュマップインスタンスを見てみましょう。 また、マップのライフサイクル全体でこの順序が維持されることを保証します。

@Test
public void givenLinkedHashMap_whenGetsOrderedKeyset_thenCorrect() {
    LinkedHashMap map = new LinkedHashMap<>();
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    Set keys = map.keySet();
    Integer[] arr = keys.toArray(new Integer[0]);

    for (int i = 0; i < arr.length; i++) {
        assertEquals(new Integer(i + 1), arr[i]);
    }
}

ここでは、リンクされたハッシュマップ内のエントリの順序について、基本的な非決定的なテストを行っています。

挿入順序が常に維持されるため、このテストが常にパスすることを保証できます。 We cannot make the same guarantee for a HashMap.

この属性は、マップを受信し、コピーを作成して操作し、呼び出し元のコードに返すAPIで非常に有利です。 クライアントがAPIを呼び出す前に返されたマップを同じ方法で順序付ける必要がある場合は、リンクされたハッシュマップが最適です。

キーがマップに再挿入されても、挿入順序は影響を受けません。

4. アクセス注文LinkedHashMap

LinkedHashMapは、カスタム負荷係数(LF)と初期容量の中でa different ordering mechanism/strategy called access-orderを指定できる特別なコンストラクターを提供します。

LinkedHashMap map = new LinkedHashMap<>(16, .75f, true);

最初のパラメータは初期容量で、その後に負荷率とlast param is the ordering modeが続きます。 したがって、trueを渡すことにより、アクセス順序が判明しましたが、デフォルトは挿入順序でした。

このメカニズムにより、要素の反復順序は、要素が最後にアクセスされた順序、つまり最近アクセスされたものから最後にアクセスされたものになります。

そのため、Least Recent Used(LRU)キャッシュの構築は、マップのようなもので非常に簡単で実用的です。 putまたはget操作が成功すると、エントリへのアクセスが発生します。

@Test
public void givenLinkedHashMap_whenAccessOrderWorks_thenCorrect() {
    LinkedHashMap map
      = new LinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    Set keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());

    map.get(4);
    assertEquals("[1, 2, 3, 5, 4]", keys.toString());

    map.get(1);
    assertEquals("[2, 3, 5, 4, 1]", keys.toString());

    map.get(3);
    assertEquals("[2, 5, 4, 1, 3]", keys.toString());
}

the order of elements in the key set is transformed as we perform access operations on the map.がどのように

簡単に言えば、マップに対するアクセス操作は、反復がすぐに実行される場合にアクセスされた要素が最後に表示されるような順序になります。

上記の例の後、putAll操作が、指定されたマップ内のマッピングごとに1つのエントリアクセスを生成することは明らかです。

当然、マップのビューに対する反復は、バッキングマップの反復の順序には影響しません。 only explicit access operations on the map will affect the order

LinkedHashMapは、固定数のマッピングを維持し、新しいエントリを追加する必要がある場合に最も古いエントリを削除し続けるためのメカニズムも提供します。

removeEldestEntryメソッドをオーバーライドして、古いマッピングを自動的に削除するこのポリシーを適用できます。

これを実際に確認するために、LinkedHashMapを拡張して古いマッピングを強制的に削除することを唯一の目的として、独自のリンクされたハッシュマップクラスを作成しましょう。

public class MyLinkedHashMap extends LinkedHashMap {

    private static final int MAX_ENTRIES = 5;

    public MyLinkedHashMap(
      int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }

}

上記のオーバーライドにより、マップは最大5エントリのサイズに拡大できます。 サイズがそれを超えると、マップ内の最も古いエントリを失うという犠牲を払って新しいエントリがそれぞれ挿入されます。 最終アクセス時刻が他のすべてのエントリに先行するエントリ:

@Test
public void givenLinkedHashMap_whenRemovesEldestEntry_thenCorrect() {
    LinkedHashMap map
      = new MyLinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);
    Set keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());

    map.put(6, null);
    assertEquals("[2, 3, 4, 5, 6]", keys.toString());

    map.put(7, null);
    assertEquals("[3, 4, 5, 6, 7]", keys.toString());

    map.put(8, null);
    assertEquals("[4, 5, 6, 7, 8]", keys.toString());
}

マップに新しいエントリを追加するときに、キーセットの先頭にある最も古いエントリがドロップオフし続けることに注意してください。

5. パフォーマンスの考慮事項

HashMapと同様に、LinkedHashMapは、ハッシュ関数のディメンションが適切である限り、追加、削除、および包含の基本的なMap操作を一定時間で実行します。 また、null値と同様にnullキーも受け入れます。

ただし、このconstant-time performance of LinkedHashMap is likely to be a little worse than the constant-time of HashMapは、二重リンクリストを維持するための追加のオーバーヘッドによるものです。

LinkedHashMapのコレクションビューの反復も、HashMapと同様の線形時間O(n)を要します。 反対に、LinkedHashMap‘s linear time performance during iteration is better than HashMap‘s linear time

これは、LinkedHashMapの場合、O(n)nは、容量に関係なく、マップ内のエントリの数のみであるためです。 一方、HashMapの場合、nは容量であり、サイズの合計はO(size+capacity).です。

負荷率と初期容量は、HashMapと同様に正確に定義されます。 ただし、このクラスの反復時間は容量の影響を受けないため、the penalty for choosing an excessively high value for initial capacity is less severe for LinkedHashMap than for HashMapに注意してください。

6. 並行性

HashMapと同様に、LinkedHashMapの実装は同期されません。 したがって、複数のスレッドからアクセスし、これらのスレッドの少なくとも1つが構造的に変更する可能性がある場合は、外部で同期する必要があります。

作成時にこれを行うのが最善です。

Map m = Collections.synchronizedMap(new LinkedHashMap());

HashMapとの違いは、構造の変更を伴うものにあります。 In access-ordered linked hash maps, merely calling the get API results in a structural modification。 これに加えて、putremoveのような操作があります。

7. 結論

この記事では、JavaLinkedHashMapクラスを、使用法の観点からMapインターフェースの最も重要な実装の1つとして検討しました。 また、スーパークラスであるHashMapとの違いの観点から、その内部動作を調査しました。

この投稿を読んだ後、ユースケースでどのMap実装を採用するかについて、より多くの情報に基づいた効果的な決定ができ​​ることを願っています。

この記事で使用されているすべての例の完全なソースコードは、GitHub projectにあります。