Java: Como converter a lista em mapa

224

Recentemente eu ter uma conversa com um colega sobre o que seria a melhor maneira de converter Lista Mapem Java e se há quaisquer benefícios específicos de fazê-lo.

Quero conhecer a melhor abordagem de conversão e realmente aprecio se alguém puder me orientar.

Essa é uma boa abordagem:

List<Object[]> results;
Map<Integer, String> resultsMap = new HashMap<Integer, String>();
for (Object[] o : results) {
    resultsMap.put((Integer) o[0], (String) o[1]);
}
Rachel
fonte
2
Qual é a melhor maneira ideal? A otimização é feita com determinado parâmetro (velocidade / memória) em mente.
quer
6
A lista difere do mapa da maneira conceitual - o mapa tem noção de par 'chave, valor', enquanto a lista não. Dado isso, não está claro como exatamente você irá converter da Lista para o Mapa e vice-versa.
Victor Sorokin
1
@ Daniel: Por Optimal, eu quis dizer qual é a melhor maneira de fazê-lo, de todas as maneiras diferentes, entre não ter certeza de todas as formas e seria bom ver algumas maneiras diferentes de converter a lista em mapa.
Rachel

Respostas:

188
List<Item> list;
Map<Key,Item> map = new HashMap<Key,Item>();
for (Item i : list) map.put(i.getKey(),i);

Supondo, é claro, que cada item tenha um getKey()método que retorne uma chave do tipo apropriado.

Jim Garrison
fonte
1
Você também pode digitar a posição na lista.
Jeremy
@ Jim: Preciso definir o getKey()para qualquer parâmetro específico?
Rachel
Além disso, qual seria o valor no mapa, você pode elaborar com um exemplo?
Rachel
1
@ Rachel - O valor é o item da lista e a chave é algo que torna o item único, determinado por você. O uso de Jim getKey()foi arbitrário.
Jeremy
1
Você conhece o tamanho de antemão para poder Map <Key, Item> map = new HashMap <Key, Item> (list.size ());
Víctor Romero
316

Com , você poderá fazer isso em uma linha usando fluxos e a Collectorsclasse.

Map<String, Item> map = 
    list.stream().collect(Collectors.toMap(Item::getKey, item -> item));

Demonstração curta:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Test{
    public static void main (String [] args){
        List<Item> list = IntStream.rangeClosed(1, 4)
                                   .mapToObj(Item::new)
                                   .collect(Collectors.toList()); //[Item [i=1], Item [i=2], Item [i=3], Item [i=4]]

        Map<String, Item> map = 
            list.stream().collect(Collectors.toMap(Item::getKey, item -> item));

        map.forEach((k, v) -> System.out.println(k + " => " + v));
    }
}
class Item {

    private final int i;

    public Item(int i){
        this.i = i;
    }

    public String getKey(){
        return "Key-"+i;
    }

    @Override
    public String toString() {
        return "Item [i=" + i + "]";
    }
}

Resultado:

Key-1 => Item [i=1]
Key-2 => Item [i=2]
Key-3 => Item [i=3]
Key-4 => Item [i=4]

Como observado nos comentários, você pode usar em Function.identity()vez de item -> item, embora eu ache i -> ibastante explícito.

E, para completar, observe que você pode usar um operador binário se sua função não for bijetiva. Por exemplo, vamos considerar isso Liste a função de mapeamento que, para um valor int, calcule o resultado do módulo 3:

List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6);
Map<String, Integer> map = 
    intList.stream().collect(toMap(i -> String.valueOf(i % 3), i -> i));

Ao executar esse código, você receberá um erro dizendo java.lang.IllegalStateException: Duplicate key 1. Isso ocorre porque 1% 3 é igual a 4% 3 e, portanto, tem o mesmo valor de chave, dada a função de mapeamento de teclas. Nesse caso, você pode fornecer um operador de mesclagem.

Aqui está um que soma os valores; (i1, i2) -> i1 + i2;que pode ser substituído com o método de referência Integer::sum.

Map<String, Integer> map = 
    intList.stream().collect(toMap(i -> String.valueOf(i % 3), 
                                   i -> i, 
                                   Integer::sum));

que agora gera:

0 => 9 (i.e 3 + 6)
1 => 5 (i.e 1 + 4)
2 => 7 (i.e 2 + 5)

Espero que ajude! :)

Alexis C.
fonte
19
melhor usar em Function.identity()vez deitem -> item
Emmanuel Touzery 4/11/14
1
@EmmanuelTouzery Bem, Function.identity()retorna t -> t;.
Alexis C.
2
Claro, ambos funcionam. Eu acho que é uma questão de gosto. Acho Function.identity () mais imediatamente reconhecível.
Emmanuel Touzery
O OP não lida com pojos, apenas seqüências de caracteres e números inteiros nos quais você não pode chamar ::getKey.
31517 philip4
@Blauhirn Eu sei, meu exemplo é baseado na classe personalizada logo abaixo. Você é livre para usar qualquer função para gerar suas chaves a partir dos valores.
Alexis C.
115

Caso essa pergunta não seja fechada como duplicada, a resposta certa é usar o Google Collections :

Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, new Function<Role,String>() {
  public String apply(Role from) {
    return from.getName(); // or something else
  }});
ripper234
fonte
17
Isso deve estar no topo
Martin Andersson
6
"O Goiaba contém um superconjunto estritamente compatível da antiga e obsoleta Biblioteca de coleções do Google . Você não deve mais usá-la. " Pode ser necessária uma atualização.
minúsculo
3
O uso de uma biblioteca externa para uma operação tão simples é um exagero. Isso ou um sinal de uma biblioteca padrão muito fraca. Nesse caso, a resposta de @ jim-garrison é perfeitamente razoável. É triste que o java não tenha métodos úteis como "map" e "reduzir", mas não totalmente necessário.
Linuxdan
2
Isso usa goiaba. Infelizmente, o Guava é muito lento no Android, portanto, esta solução não deve ser usada em um projeto Android.
IgorGanapolsky
Se o item da lista tem roleNames duplicados, o código acima lança exceção,
Junchen Liu
17

Usando o Java 8, você pode fazer o seguinte:

Map<Key, Value> result= results
                       .stream()
                       .collect(Collectors.toMap(Value::getName,Function.identity()));

Value pode ser qualquer objeto que você usar.

stackFan
fonte
16

Desde o Java 8, a resposta do @ZouZou usando o Collectors.toMapcoletor é certamente a maneira idiomática de resolver esse problema.

E como essa é uma tarefa tão comum, podemos transformá-la em um utilitário estático.

Dessa forma, a solução realmente se torna uma linha.

/**
 * Returns a map where each entry is an item of {@code list} mapped by the
 * key produced by applying {@code mapper} to the item.
 *
 * @param list the list to map
 * @param mapper the function to produce the key from a list item
 * @return the resulting map
 * @throws IllegalStateException on duplicate key
 */
public static <K, T> Map<K, T> toMapBy(List<T> list,
        Function<? super T, ? extends K> mapper) {
    return list.stream().collect(Collectors.toMap(mapper, Function.identity()));
}

E aqui está como você o usaria em List<Student>:

Map<Long, Student> studentsById = toMapBy(students, Student::getId);
glts
fonte
Para uma discussão sobre os parâmetros de tipo desse método, consulte minha pergunta de acompanhamento .
MLDs
Isso gerará uma exceção, caso haja chaves duplicadas. Sth como: Exception in thread "main" java.lang.IllegalStateException: Chave duplicada .... Para mais detalhes ver: codecramp.com/java-8-streams-api-convert-list-map
EMM
@EMM Obviamente, conforme planejado e documentado no Javadoc.
MLDs
Sim, atualizou a resposta para cobrir o caso de duplicatas. Por favor revise. Obrigado
EMM
10

A Liste Mapsão conceitualmente diferentes. A Listé uma coleção ordenada de itens. Os itens podem conter duplicatas e um item pode não ter nenhum conceito de identificador exclusivo (chave). UMAMap possui valores mapeados para chaves. Cada chave pode apontar apenas para um valor.

Portanto, dependendo Listdos itens do seu, pode ou não ser possível convertê-lo em a Map. Os Listitens do seu não têm duplicatas? Cada item tem uma chave exclusiva? Nesse caso, é possível colocá-los em a Map.

Steve Kuo
fonte
10

Alexis já postou uma resposta em Java 8 usando o método toMap(keyMapper, valueMapper). Conforme o documento para a implementação deste método:

Não há garantias sobre o tipo, mutabilidade, serialização ou segurança do thread retornado.

Portanto, caso estejamos interessados ​​em uma implementação específica da Mapinterface, por exemplo HashMap, podemos usar o formulário sobrecarregado como:

Map<String, Item> map2 =
                itemList.stream().collect(Collectors.toMap(Item::getKey, //key for map
                        Function.identity(),    // value for map
                        (o,n) -> o,             // merge function in case of conflict with keys
                        HashMap::new));         // map factory - we want HashMap and not any Map implementation

Embora o uso de um Function.identity()ou i->iseja bom, parece que ao Function.identity()invés de i -> ipode economizar memória, conforme esta resposta relacionada .

akhil_mittal
fonte
1
O fato engraçado de que em 2019 uma enorme quantidade de pessoas ainda não percebe que não conhece a implementação real do mapa que obtém com o lambda! Na verdade, essa é apenas uma resposta que encontrei com o Java 8 lambdas que usaria na produção.
Pavlo
Existe uma maneira de coletar especificando um tipo de mapa, mas sem usar uma função de mesclagem?
Rosberg Linhares
8

Também existe uma maneira simples de fazer isso usando o Maps.uniqueIndex (...) do Google bibliotecas

Andrejs
fonte
5

Método universal

public static <K, V> Map<K, V> listAsMap(Collection<V> sourceList, ListToMapConverter<K, V> converter) {
    Map<K, V> newMap = new HashMap<K, V>();
    for (V item : sourceList) {
        newMap.put( converter.getKey(item), item );
    }
    return newMap;
}

public static interface ListToMapConverter<K, V> {
    public K getKey(V item);
}
xxf
fonte
Como usar isso? O que devo passar como converterparâmetro no método?
IgorGanapolsky
4

Sem o java-8, você poderá fazer isso em coleções comuns do Commons e na classe Closure

List<Item> list;
@SuppressWarnings("unchecked")
Map<Key, Item> map  = new HashMap<Key, Item>>(){{
    CollectionUtils.forAllDo(list, new Closure() {
        @Override
        public void execute(Object input) {
            Item item = (Item) input;
            put(i.getKey(), item);
        }
    });
}};
Vitaliy Oliynyk
fonte
2

Muitas soluções vêm à mente, dependendo do que você deseja obter:

Cada item da lista é chave e valor

for( Object o : list ) {
    map.put(o,o);
}

Os elementos da lista têm algo para procurá-los, talvez um nome:

for( MyObject o : list ) {
    map.put(o.name,o);
}

Os elementos da lista têm algo para procurá-los e não há garantia de que sejam exclusivos: use o MultiMaps do Google

for( MyObject o : list ) {
    multimap.put(o.name,o);
}

Dando a todos os elementos a posição como chave:

for( int i=0; i<list.size; i++ ) {
    map.put(i,list.get(i));
}

...

Realmente depende do que você deseja obter.

Como você pode ver nos exemplos, um mapa é um mapeamento de uma chave para um valor, enquanto uma lista é apenas uma série de elementos com uma posição cada. Portanto, eles simplesmente não são convertíveis automaticamente.

Daniel
fonte
Mas podemos considerar a posição do elemento da lista como chave e colocar seu valor no mapa. Essa é uma boa solução?
Rachel
AFAIK sim! Não há nenhuma função no JDK que faça isso automaticamente, portanto, você deve rolar por conta própria.
Daniel
é possível fazer a última versão (usando o índice de matriz como chave de mapa) com java 8 streams?
22417 philip4
2

Aqui está um pequeno método que escrevi exatamente para esse fim. Ele usa o Validate do Apache Commons.

Sinta-se livre para usá-lo.

/**
 * Converts a <code>List</code> to a map. One of the methods of the list is called to retrive
 * the value of the key to be used and the object itself from the list entry is used as the
 * objct. An empty <code>Map</code> is returned upon null input.
 * Reflection is used to retrieve the key from the object instance and method name passed in.
 *
 * @param <K> The type of the key to be used in the map
 * @param <V> The type of value to be used in the map and the type of the elements in the
 *            collection
 * @param coll The collection to be converted.
 * @param keyType The class of key
 * @param valueType The class of the value
 * @param keyMethodName The method name to call on each instance in the collection to retrieve
 *            the key
 * @return A map of key to value instances
 * @throws IllegalArgumentException if any of the other paremeters are invalid.
 */
public static <K, V> Map<K, V> asMap(final java.util.Collection<V> coll,
        final Class<K> keyType,
        final Class<V> valueType,
        final String keyMethodName) {

    final HashMap<K, V> map = new HashMap<K, V>();
    Method method = null;

    if (isEmpty(coll)) return map;
    notNull(keyType, Messages.getString(KEY_TYPE_NOT_NULL));
    notNull(valueType, Messages.getString(VALUE_TYPE_NOT_NULL));
    notEmpty(keyMethodName, Messages.getString(KEY_METHOD_NAME_NOT_NULL));

    try {
        // return the Method to invoke to get the key for the map
        method = valueType.getMethod(keyMethodName);
    }
    catch (final NoSuchMethodException e) {
        final String message =
            String.format(
                    Messages.getString(METHOD_NOT_FOUND),
                    keyMethodName,
                    valueType);
        e.fillInStackTrace();
        logger.error(message, e);
        throw new IllegalArgumentException(message, e);
    }
    try {
        for (final V value : coll) {

            Object object;
            object = method.invoke(value);
            @SuppressWarnings("unchecked")
            final K key = (K) object;
            map.put(key, value);
        }
    }
    catch (final Exception e) {
        final String message =
            String.format(
                    Messages.getString(METHOD_CALL_FAILED),
                    method,
                    valueType);
        e.fillInStackTrace();
        logger.error(message, e);
        throw new IllegalArgumentException(message, e);
    }
    return map;
}
Kango_V
fonte
2

Você pode aproveitar a API de fluxos do Java 8.

public class ListToMap {

  public static void main(String[] args) {
    List<User> items = Arrays.asList(new User("One"), new User("Two"), new User("Three"));

    Map<String, User> map = createHashMap(items);
    for(String key : map.keySet()) {
      System.out.println(key +" : "+map.get(key));
    }
  }

  public static Map<String, User> createHashMap(List<User> items) {
    Map<String, User> map = items.stream().collect(Collectors.toMap(User::getId, Function.identity()));
    return map;
  }
}

Para mais detalhes, visite: http://codecramp.com/java-8-streams-api-convert-list-map/

EMM
fonte
1

Um exemplo do Java 8 para converter um List<?>de objetos em Map<k, v>:

List<Hosting> list = new ArrayList<>();
list.add(new Hosting(1, "liquidweb.com", new Date()));
list.add(new Hosting(2, "linode.com", new Date()));
list.add(new Hosting(3, "digitalocean.com", new Date()));

//example 1
Map<Integer, String> result1 = list.stream().collect(
    Collectors.toMap(Hosting::getId, Hosting::getName));

System.out.println("Result 1 : " + result1);

//example 2
Map<Integer, String> result2 = list.stream().collect(
    Collectors.toMap(x -> x.getId(), x -> x.getName()));

Código copiado de:
https://www.mkyong.com/java8/java-8-convert-list-to-map/

Doss
fonte
1

como já foi dito, em java-8 temos a solução concisa dos coletores:

  list.stream().collect(
         groupingBy(Item::getKey)
        )

e também, você pode aninhar vários grupos passando um outro método groupingBy como segundo parâmetro:

  list.stream().collect(
         groupingBy(Item::getKey, groupingBy(Item::getOtherKey))
        )

Dessa forma, teremos um mapa de vários níveis, assim: Map<key, Map<key, List<Item>>>

Andrea Scarafoni
fonte
1

Usando fluxos java-8

results.stream().collect(Collectors.toMap(e->((Integer)e[0]), e->(String)e[1]));
Ankit Sharma
fonte
0

Eu gosto da resposta do Kango_V, mas acho que é muito complexo. Eu acho que isso é mais simples - talvez muito simples. Se inclinado, você pode substituir String por um marcador Genérico e fazê-lo funcionar para qualquer tipo de Chave.

public static <E> Map<String, E> convertListToMap(Collection<E> sourceList, ListToMapConverterInterface<E> converterInterface) {
    Map<String, E> newMap = new HashMap<String, E>();
    for( E item : sourceList ) {
        newMap.put( converterInterface.getKeyForItem( item ), item );
    }
    return newMap;
}

public interface ListToMapConverterInterface<E> {
    public String getKeyForItem(E item);
}

Usado assim:

        Map<String, PricingPlanAttribute> pricingPlanAttributeMap = convertListToMap( pricingPlanAttributeList,
                new ListToMapConverterInterface<PricingPlanAttribute>() {

                    @Override
                    public String getKeyForItem(PricingPlanAttribute item) {
                        return item.getFullName();
                    }
                } );
cs94njw
fonte
0

Apache Commons MapUtils.populateMap

Se você não usa o Java 8 e não deseja usar um loop explícito por algum motivo, tente MapUtils.populateMapno Apache Commons.

MapUtils.populateMap

Digamos que você tenha uma lista de Pairs.

List<ImmutablePair<String, String>> pairs = ImmutableList.of(
    new ImmutablePair<>("A", "aaa"),
    new ImmutablePair<>("B", "bbb")
);

E agora você quer um mapa da Pairchave do Pairobjeto.

Map<String, Pair<String, String>> map = new HashMap<>();
MapUtils.populateMap(map, pairs, new Transformer<Pair<String, String>, String>() {

  @Override
  public String transform(Pair<String, String> input) {
    return input.getKey();
  }
});

System.out.println(map);

dá saída:

{A=(A,aaa), B=(B,bbb)}

Dito isto, um forloop é talvez mais fácil de entender. (Isso abaixo fornece a mesma saída):

Map<String, Pair<String, String>> map = new HashMap<>();
for (Pair<String, String> pair : pairs) {
  map.put(pair.getKey(), pair);
}
System.out.println(map);
typoerrpr
fonte