Как сохранить дубликаты ключей на карте в Java?

Как сохранить дубликаты ключей на карте в 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.

Как мы видим, если мы попытаемся вставить два значения для одного и того же ключа, второе значение будет сохранено, а первое будет удалено.

Он также будет возвращен (каждой правильной реализацией метода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. Коллекция как ценность

Очевидно, что использованиеCollection для каждого значения нашегоMap сработает:

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. MultiMap

Интерфейс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");

Хотя этот класс доступен начиная с CC 3.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

Гуава - это Google Core Libraries для Java API.

Интерфейсcom.google.common.collect.Multimap существует с версии 2. На момент написания последней версии - 25, но поскольку после версии 23 она была разделена на разные ветки дляjre иandroid (25.0-jre и25.0-android), мы Я по-прежнему буду использовать версию 23 для наших примеров.

Начнем с импорта Guava в наш проект:


  com.google.guava
  guava
  23.0

Guava следовал пути нескольких реализаций с самого начала.

Самый распространенный -com.google.common.collect.ArrayListMultimap, который используетHashMap, подкрепленныйArrayList для каждого значения:

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

Как всегда, мы должны предпочесть неизменные реализации интерфейса Multimap: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.