Javaのマップに重複キーを保存する方法は?
1. 概要
このチュートリアルでは、重複キーを使用してMapを処理するために使用できるオプション、つまり、単一のキーに複数の値を格納できるMapを使用する方法について説明します。
2. 標準マップ
Javaには、インターフェースMapのいくつかの実装があり、それぞれに独自の特殊性があります。
ただし、none of the existing Java core Map implementations allow a Map to handle multiple values for a single key。
ご覧のとおり、同じキーに2つの値を挿入しようとすると、2番目の値が保存され、最初の値が削除されます。
また、(put(K key, V value)メソッドの適切な実装ごとに)返されます。
Map 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> map = new HashMap<>();
List 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> 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. Apache Commonsコレクション
いつものように、Apacheには問題の解決策があります。
Common Collectionsの最新リリース(今後はCC)をインポートすることから始めましょう。
org.apache.commons
commons-collections4
4.1
4.1. マルチマップ
org.apache.commons.collections4.MultiMapインターフェースは、各キーに対する値のコレクションを保持するマップを定義します。
これはorg.apache.commons.collections4.map.MultiValueMapクラスによって実装され、内部でボイラープレートのほとんどを自動的に処理します。
MultiMap map = new MultiValueMap<>();
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection) map.get("key1"))
.contains("value1", "value2");
このクラスはCC3.2以降で使用できますが、it’s not thread-safe、およびit’s been deprecated in CC 4.1です。 新しいバージョンにアップグレードできない場合にのみ使用してください。
4.2. MultiValuedMap
MultiMapの後継はorg.apache.commons.collections4.MultiValuedMapインターフェースです。 すぐに使用できる複数の実装があります。
複数の値をArrayListに格納する方法を見てみましょう。これにより、重複が保持されます。
MultiValuedMap map = new ArrayListValuedHashMap<>();
map.put("key1", "value1");
map.put("key1", "value2");
map.put("key1", "value2");
assertThat((Collection) map.get("key1"))
.containsExactly("value1", "value2", "value2");
または、HashSetを使用して、重複を削除することもできます。
MultiValuedMap map = new HashSetValuedHashMap<>();
map.put("key1", "value1");
map.put("key1", "value1");
assertThat((Collection) map.get("key1"))
.containsExactly("value1");
両方のabove implementations are not thread-safe。
UnmodifiableMultiValuedMapデコレータを使用してそれらを不変にする方法を見てみましょう。
@Test(expected = UnsupportedOperationException.class)
public void givenUnmodifiableMultiValuedMap_whenInserting_thenThrowingException() {
MultiValuedMap map = new ArrayListValuedHashMap<>();
map.put("key1", "value1");
map.put("key1", "value2");
MultiValuedMap immutableMap =
MultiMapUtils.unmodifiableMultiValuedMap(map);
immutableMap.put("key1", "value3");
}
5. グアバMultimap
Guavaは、Java API用のGoogleコアライブラリです。
com.google.common.collect.Multimapインターフェースは、バージョン2以降にあります。 執筆時点では最新リリースは25ですが、バージョン23以降はjreとandroid(25.0-jreと25.0-android)の異なるブランチに分割されているため、例では引き続きバージョン23を使用します。
プロジェクトにGuavaをインポートすることから始めましょう。
com.google.guava
guava
23.0
グアバは最初から複数の実装の道を歩んでいました。
最も一般的なものはcom.google.common.collect.ArrayListMultimapで、すべての値にArrayListで裏打ちされたHashMapを使用します。
Multimap map = ArrayListMultimap.create();
map.put("key1", "value2");
map.put("key1", "value1");
assertThat((Collection) 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 map = LinkedHashMultimap.create();
map.put("key1", "value3");
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection) map.get("key1"))
.containsExactly("value3", "value1", "value2");
または、com.google.common.collect.TreeMultimapを使用して、キーと値を自然な順序で反復することもできます。
Multimap map = TreeMultimap.create();
map.put("key1", "value3");
map.put("key1", "value1");
map.put("key1", "value2");
assertThat((Collection) map.get("key1"))
.containsExactly("value1", "value2", "value3");
5.2. カスタムMultiMapの偽造
他の多くの実装が利用可能です。
ただし、まだ実装されていないMapやListを装飾したい場合があります。
幸いなことに、Guavaにはそれを可能にするファクトリメソッドがあります:Multimap.newMultimap()。
6. 結論
キーの複数の値を既存のすべての方法でマップに保存する方法を見てきました。
Apache Commons CollectionsとGuavaの最も人気のある実装を調査しました。これらは、可能な場合はカスタムソリューションよりも優先されるはずです。
いつものように、完全なソースコードはover on Githubで利用できます。