Como armazenar chaves duplicadas em um mapa em Java?

Como armazenar chaves duplicadas em um mapa em Java?

1. Visão geral

Neste tutorial, vamos explorar as opções disponíveis para lidar com umMap com chaves duplicadas ou, em outras palavras, umMap que permite armazenar vários valores para uma única chave.

2. Mapas Padrão

Java possui diversas implementações da interfaceMap, cada uma com suas particularidades.

No entanto,none of the existing Java core Map implementations allow a Map to handle multiple values for a single key.

Como podemos ver, se tentarmos inserir dois valores para a mesma chave, o segundo valor será armazenado, enquanto o primeiro será descartado.

Ele também será retornado (por cada implementação adequada do métodoput(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");

Como podemos alcançar o comportamento desejado então?

3. Coleção como valor

Obviamente, usar umCollection para cada valor de nossoMap faria o trabalho:

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");

No entanto, esta solução detalhada tem várias desvantagens e é propensa a erros. Isso implica que precisamos instanciar umCollection para cada valor, verificar sua presença antes de adicionar ou remover um valor, excluí-lo manualmente quando nenhum valor for deixado, etc.

Do Java 8, podemos explorar os métodoscompute() e melhorá-los:

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");

Embora isso seja algo que vale a pena conhecer, devemos evitá-lo, a menos que tenha um bom motivo para não fazê-lo, como políticas restritivas da empresa que nos impedem de usar bibliotecas de terceiros.

Caso contrário, antes de escrever nossa própria implementação deMap customizada e reinventar a roda, devemos escolher entre as várias opções prontas para usar.

4. Coleções Apache Commons

Como de costume,Apache tem uma solução para nosso problema.

Vamos começar importando a versão mais recente deCommon Collections (CC de agora em diante):


  org.apache.commons
  commons-collections4
  4.1

4.1. MultiMap

A interfaceorg.apache.commons.collections4.MultiMap define um mapa que contém uma coleção de valores para cada chave.

É implementado pela classeorg.apache.commons.collections4.map.MultiValueMap, que lida automaticamente com a maior parte do clichê sob o capô:

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

Embora essa classe esteja disponível desde CC 3.2,it’s not thread-safe eit’s been deprecated in CC 4.1. Devemos usá-lo apenas quando não podemos atualizar para a versão mais recente.

4.2. MultiValuedMap

O sucessor deMultiMap é a interfaceorg.apache.commons.collections4.MultiValuedMap. Possui várias implementações prontas para serem usadas.

Vamos ver como armazenar nossos múltiplos valores em umArrayList, que retém duplicatas:

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");

Como alternativa, poderíamos usar umHashSet, que elimina duplicatas:

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

Ambos osabove implementations are not thread-safe.

Vamos ver como podemos usar o decoradorUnmodifiableMultiValuedMap para torná-los imutáveis:

@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. GoiabaMultimap

A goiaba é a API do Google Core Libraries para Java.

A interfacecom.google.common.collect.Multimap existe desde a versão 2. No momento em que este artigo foi escrito, a versão mais recente é a 25, mas como após a versão 23 ela foi dividida em diferentes ramos parajreeandroid (25.0-jree25.0-android), nós ainda usaremos a versão 23 para nossos exemplos.

Vamos começar importando Guava em nosso projeto:


  com.google.guava
  guava
  23.0

A goiaba seguiu o caminho das múltiplas implementações desde o início.

O mais comum é ocom.google.common.collect.ArrayListMultimap, que usa umHashMap apoiado por umArrayList para cada valor:

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

Como sempre, devemos preferir as implementações imutáveis ​​da interface Multimap:com.google.common.collect.ImmutableListMultimapecom.google.common.collect.ImmutableSetMultimap.

5.1. Implementações de mapas comuns

Quando precisamos de uma implementação específica deMap, a primeira coisa a fazer é verificar se ela existe, pois provavelmente o Guava já a implementou.

Por exemplo, podemos usarcom.google.common.collect.LinkedHashMultimap, que preserva a ordem de inserção de chaves e valores:

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");

Como alternativa, podemos usar acom.google.common.collect.TreeMultimap, que itera chaves e valores em sua ordem natural:

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. Forjando nossoMultiMap personalizado

Muitas outras implementações estão disponíveis.

No entanto, podemos querer decorar umMape / ou umList ainda não implementado.

Felizmente, o Guava tem um método de fábrica que nos permite fazer isso: oMultimap.newMultimap().

6. Conclusão

Vimos como armazenar vários valores para uma chave em um mapa em todas as principais formas existentes.

Exploramos as implementações mais populares de Apache Commons Collections e Guava, que devem ser preferidas a soluções personalizadas, quando possível.

Como sempre, o código-fonte completo está disponívelover on Github.