Рубрики
Без рубрики

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

Краткое и практическое руководство по обработке дубликатов ключей с помощью multimaps в Java.

Автор оригинала: Andrea Ligios.

1. Обзор

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

2. Стандартные карты

Java имеет несколько реализаций интерфейса Map , каждая из которых имеет свои особенности.

Однако ни одна из существующих реализаций карты ядра Java не позволяет Map обрабатывать несколько значений для одного ключа .

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

Он также будет возвращен (каждой правильной реализацией метода put(ключ K, значение V) ):

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> 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. Многозначная карта класс, который автоматически обрабатывает большую часть шаблона под капотом:

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

Хотя этот класс доступен с CC 3.2, он не является потокобезопасным , и он устарел в CC 4.1 . Мы должны использовать его только тогда, когда мы не можем перейти на более новую версию.

4.2. Многозначная карта

Преемником MultiMap является org.apache.commons.collections4. Многозначная карта интерфейс. Он имеет несколько реализаций, готовых к использованию.

Давайте посмотрим , как сохранить наши множественные значения в 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");

Обе вышеприведенные реализации не являются потокобезопасными .

Давайте посмотрим, как мы можем использовать 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. Гуава Мультимап

Guava-это основные библиотеки Google для Java API.

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

Давайте начнем с импорта гуавы в наш проект:


  com.google.guava
  guava
  23.0

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

Наиболее распространенным из них является 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 , первое, что нужно сделать, это проверить, существует ли она, потому что, вероятно, Гуава уже реализовала ее.

Например, мы можем использовать 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. Создание Нашей Пользовательской Мультикарты

Доступно множество других реализаций.

Однако мы можем захотеть украсить Карту и/или Список , еще не реализованный.

К счастью, у Гуавы есть заводской метод, позволяющий нам это сделать: Multimaps.newMultimap() .

6. Заключение

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

Мы изучили наиболее популярные реализации коллекций Apache Commons и Guava, которые по возможности следует предпочесть пользовательским решениям.

Как всегда, полный исходный код доступен на Github .