1. 概要

このチュートリアルでは、重複キーを持つ Map 、つまり、単一のキーに複数の値を格納できるMapを処理するために使用できるオプションについて説明します。

2. 標準マップ

Javaには、インターフェース Map のいくつかの実装があり、それぞれに独自の特殊性があります。

ただし、既存のJavaコアMap実装のいずれも、Mapが単一のキーに対して複数の値を処理することを許可していません。

ご覧のとおり、同じキーに2つの値を挿入しようとすると、2番目の値が保存され、最初の値は削除されます。

また、( put(Kキー、V値)メソッドの適切な実装ごとに)返されます。

Map<String, String> map = new HashMap<>();
assertThat(map.put("key1", "value1")).isEqualTo(null);
assertThat(map.put("key1", "value2")).isEqualTo("value1");
assertThat(map.get("key1")).isEqualTo("value2");

では、どうすれば目的の動作を実現できますか?

3. 価値としてのコレクション

明らかに、Mapのすべての値にCollectionを使用すると次のようになります。

Map<String, List<String>> map = new HashMap<>();
List<String> list = new ArrayList<>();
map.put("key1", list);
map.get("key1").add("value1");
map.get("key1").add("value2");
 
assertThat(map.get("key1").get(0)).isEqualTo("value1");
assertThat(map.get("key1").get(1)).isEqualTo("value2");

ただし、この冗長なソリューションには複数の欠点があり、エラーが発生しやすくなります。 これは、すべての値に対して Collection をインスタンス化し、値を追加または削除する前にその存在を確認し、値が残っていない場合は手動で削除する必要があることを意味します。

Java 8から、 compute()メソッドを悪用して、それを改善することができます。

Map<String, List<String>> map = new HashMap<>();
map.computeIfAbsent("key1", k -> new ArrayList<>()).add("value1");
map.computeIfAbsent("key1", k -> new ArrayList<>()).add("value2");

assertThat(map.get("key1").get(0)).isEqualTo("value1");
assertThat(map.get("key1").get(1)).isEqualTo("value2");

これは知っておく価値のあることですが、サードパーティのライブラリの使用を禁止する制限的な会社のポリシーなど、非常に正当な理由がない限り、避ける必要があります。

それ以外の場合は、独自のカスタム Map 実装を作成して車輪の再発明を行う前に、すぐに使用できるいくつかのオプションから選択する必要があります。

4. ApacheCommonsコレクション

いつものように、Apacheには問題の解決策があります。

Common Collections の最新リリース(今後はCC)をインポートすることから始めましょう。

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-collections4</artifactId>
  <version>4.1</version>
</dependency>

4.1. マルチマップ

org.apache.commons.collections4.MultiMap インターフェースは、各キーに対する値のコレクションを保持するマップを定義します。

これは、 org.apache.commons.collections4.map.MultiValueMap クラスによって実装され、内部でボイラープレートのほとんどを自動的に処理します。

MultiMap<String, String> map = new MultiValueMap<>();
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection<String>) map.get("key1"))
  .contains("value1", "value2");

このクラスはCC3.2以降で使用できますが、はスレッドセーフではなく CC4.1では非推奨になっています。 新しいバージョンにアップグレードできない場合にのみ使用してください。

4.2. MultiValuedMap

MultiMap の後継は、org.apache.commons.collections4.MultiValuedMapインターフェースです。 すぐに使用できる複数の実装があります。

複数の値をArrayListに格納する方法を見てみましょう。これは、重複を保持します。

MultiValuedMap<String, String> map = new ArrayListValuedHashMap<>();
map.put("key1", "value1");
map.put("key1", "value2");
map.put("key1", "value2");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value1", "value2", "value2");

または、 HashSet を使用して、重複を削除することもできます。

MultiValuedMap<String, String> map = new HashSetValuedHashMap<>();
map.put("key1", "value1");
map.put("key1", "value1");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value1");

上記の実装は両方ともスレッドセーフではありません

UnmodizableMultiValuedMapデコレータを使用してそれらを不変にする方法を見てみましょう。

@Test(expected = UnsupportedOperationException.class)
public void givenUnmodifiableMultiValuedMap_whenInserting_thenThrowingException() {
    MultiValuedMap<String, String> map = new ArrayListValuedHashMap<>();
    map.put("key1", "value1");
    map.put("key1", "value2");
    MultiValuedMap<String, String> immutableMap =
      MultiMapUtils.unmodifiableMultiValuedMap(map);
    immutableMap.put("key1", "value3");
}

5. グアバマルチマップ

Guavaは、JavaAPI用のGoogleコアライブラリです。

私たちのプロジェクトにGuavaをインポートすることから始めましょう:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>31.0.1-jre</version>
</dependency>

Guavaは、最初から複数の実装のパスをたどりました。

最も一般的なものはcom.google.common.collect.ArrayListMultimapで、すべての値に対してArrayListに裏打ちされたHashMapを使用します。

Multimap<String, String> map = ArrayListMultimap.create();
map.put("key1", "value2");
map.put("key1", "value1");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value2", "value1");

いつものように、マルチマップインターフェースの不変の実装を優先する必要があります:com.google.common.collect.ImmutableListMultimapおよびcom.google.common.collect.ImmutableSetMultimap

5.1. 一般的なマップの実装

特定のMap実装が必要な場合、おそらくGuavaがすでに実装しているため、最初に行うことは、それが存在するかどうかを確認することです。

たとえば、 com.google.common.collect.LinkedHashMultimap を使用できます。これにより、キーと値の挿入順序が保持されます。

Multimap<String, String> map = LinkedHashMultimap.create();
map.put("key1", "value3");
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value3", "value1", "value2");

または、 com.google.common.collect.TreeMultimap を使用して、キーと値を自然な順序で繰り返します。

Multimap<String, String> map = TreeMultimap.create();
map.put("key1", "value3");
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection<String>) map.get("key1"))
  .containsExactly("value1", "value2", "value3");

5.2. カスタムMultiMapの鍛造

他の多くの実装が利用可能です。

ただし、まだ実装されていないMapListを装飾したい場合があります。

幸い、Guavaにはそれを可能にするファクトリメソッド Multimap.newMultimap()があります。

6. 結論

キーの複数の値を既存のすべての方法でマップに格納する方法を見てきました。

Apache Commons CollectionsとGuavaの最も一般的な実装を調査しました。これらは、可能な場合はカスタムソリューションよりも優先されるはずです。

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