Atajo para agregar a la Lista en un HashMap

6 minutos de lectura

A menudo tengo la necesidad de tomar una lista de objetos y agruparlos en un Mapa basado en un valor contenido en el objeto. P.ej. tome una lista de Usuarios y agrupe por País.

Mi código para esto generalmente se ve así:

Map<String, List<User>> usersByCountry = new HashMap<String, List<User>>();
for(User user : listOfUsers) {
    if(usersByCountry.containsKey(user.getCountry())) {
        //Add to existing list
        usersByCountry.get(user.getCountry()).add(user);

    } else {
        //Create new list
        List<User> users = new ArrayList<User>(1);
        users.add(user);
        usersByCountry.put(user.getCountry(), users);
    }
}

Sin embargo, no puedo evitar pensar que esto es incómodo y que algún gurú tiene un mejor enfoque. Lo más cercano que puedo ver hasta ahora es el MultiMap de Google Collections.

¿Existen enfoques estándar?

¡Gracias!

  • ¿Debería ser eso realmente? Map<String, Set<User>>? La respuesta hace una diferencia para lo que elijas construir o usar. Tenga en cuenta que Google Collections proporciona mejoras para las colecciones anidadas que son de varios tipos de listas y conjuntos.

    – seh

    11 de junio de 2010 a las 0:04

  • Simplemente suelte Java para .Net y Linq.

    – Hamish Grubijan

    11 de junio de 2010 a las 0:11

  • @Hamish: sí, ¡debido a que nuestras preocupaciones sobre las dependencias son totalmente irrelevantes entonces!

    – Carlos

    11 de junio de 2010 a las 1:20

  • @Hamish: Nunca podría volver a enfrentar a mi hermano programador .Net si hiciera eso. ¡La verguenza!

    – Damo

    11 de junio de 2010 a las 6:14

avatar de usuario
BalusC

Desde Java 8 puedes hacer uso de Map#computeIfAbsent().

Map<String, List<User>> usersByCountry = new HashMap<>();

for (User user : listOfUsers) {
    usersByCountry.computeIfAbsent(user.getCountry(), k -> new ArrayList<>()).add(user);
}

O bien, utilice las API de transmisión Collectors#groupingBy() ir de List a Map directamente:

Map<String, List<User>> usersByCountry = listOfUsers.stream().collect(Collectors.groupingBy(User::getCountry));

En Java 7 o inferior, lo mejor que puede obtener es lo siguiente:

Map<String, List<User>> usersByCountry = new HashMap<>();

for (User user : listOfUsers) {
    List<User> users = usersByCountry.get(user.getCountry());
    if (users == null) {
        users = new ArrayList<>();
        usersByCountry.put(user.getCountry(), users);
    }
    users.add(user);
}

Colecciones comunes tiene un LazyMappero no está parametrizado. Guayaba no tiene una especie de LazyMap o LazyListpero puedes usar Multimap para esto, como se muestra en la respuesta de polygenelubricants a continuación.

  • Podrías acortarlo un poco más: usersByCountry.put(user.getCountry(), users = new ArrayList<>()); Aunque estoy seguro de que algunos fruncirían el ceño.

    – shmosel

    17 de marzo de 2017 a las 1:30

  • Genial, una lista mutable como valor.

    – WesternGun

    hace 2 días

avatar de usuario
poligenelubricantes

guayaba Multimap realmente es la estructura de datos más apropiada para esto, y de hecho, hay Multimaps.index(Iterable<V>, Function<? super V,K>) método de utilidad que hace exactamente lo que quiere: tomar un Iterable<V> (que un List<V> es), y aplicar el Function<? super V, K> para conseguir las llaves de la Multimap<K,V>.

Aquí hay un ejemplo de la documentación:

Por ejemplo,

  List<String> badGuys
      = Arrays.asList("Inky", "Blinky", "Pinky", "Pinky", "Clyde");
  Function<String, Integer> stringLengthFunction = ...;
  Multimap<Integer, String> index
      = Multimaps.index(badGuys, stringLengthFunction);
  System.out.println(index);

huellas dactilares

 {4=[Inky], 5=[Pinky, Pinky, Clyde], 6=[Blinky]}

En tu caso, escribirías un Function<User,String> userCountryFunction = ....

  • +1 Me frustra que las respuestas que involucran escribir mucho más código que este tengan una clasificación más alta, solo porque fueron las más rápidas en llegar. 🙁

    –Kevin Bourrillion

    11 de junio de 2010 a las 16:37

  • @Kevin: Esperaba que eventualmente pasaras por aquí =) Por cierto, planeo eventualmente escribir artículos de preguntas y respuestas sobre stackoverflow en varias clases de guayaba para demostrar sus capacidades.

    – lubricantes poligenéticos

    11 de junio de 2010 a las 16:39


  • Paso solo una o dos veces al día, lo que garantiza que nunca tendré la oportunidad de que mis respuestas sean votadas. Creo que tu idea es genial. Supongo que te refieres a publicar una pregunta y responderla tú mismo. Obtendrá algunas personas que le dirán que hay algo inmoral en esto, pero está explícitamente sancionado por la comunidad SO en general, ya que su objetivo es que SO tenga un gran contenido.

    –Kevin Bourrillion

    11 de junio de 2010 a las 16:47

  • +1 para el ejemplo. Yo sería curioso a los artículos. Se debe prestar más atención a las bibliotecas de guayaba.

    – BalusC

    11 de junio de 2010 a las 16:49

Cuando tengo que lidiar con un mapa con valor de colección, casi siempre termino escribiendo un pequeño método de utilidad estática putIntoListMap() en la clase. Si me encuentro necesitándolo en varias clases, lanzo ese método a una clase de utilidad. Las llamadas a métodos estáticos como ese son un poco feas, pero son mucho más limpias que escribir el código cada vez. A menos que los mapas múltiples jueguen un papel bastante central en su aplicación, en mi humilde opinión, probablemente no valga la pena obtener otra dependencia.

Mediante el uso lambdaj puede obtener ese resultado con solo una línea de código de la siguiente manera:

Group<User> usersByCountry = group(listOfUsers, by(on(User.class).getCountry()));

Lambdaj también ofrece muchas otras características para manipular colecciones con un lenguaje específico de dominio muy legible.

Parece que hacemos esto muchas veces, así que creé una clase de plantilla

public abstract class ListGroupBy<K, T> {
public Map<K, List<T>> map(List<T> list) {
    Map<K, List<T> > map = new HashMap<K, List<T> >();
    for (T t : list) {
        K key = groupBy
        List<T> innerList = map.containsKey(key) ? map.get(key) : new ArrayList<T>();
        innerList.add
        map.put(key, innerList);
    }
    return map;
}

protected abstract K groupBy(T t);
}

Solo proporciona impl para groupBy

en tu caso

String groupBy(User u){return user.getCountry();}

avatar de usuario
Carlos

Parece que sus necesidades exactas son satisfechas por LinkedHashMultimapa en la biblioteca de GC. Si puede vivir con las dependencias, todo su código se convierte en:

SetMultimap<String,User> countryToUserMap = LinkedHashMultimap.create();
// .. other stuff, then whenever you need it:
countryToUserMap.put(user.getCountry(), user);

se mantiene el orden de inserción (sobre todo lo que parece que estaba haciendo con su lista) y se excluyen los duplicados; por supuesto, puede cambiar a un conjunto simple basado en hash o un conjunto de árboles según lo dicten las necesidades (o una lista, aunque eso no parece ser lo que necesita). Las colecciones vacías se devuelven si solicita un país sin usuarios, todos obtienen ponis, etc. Lo que quiero decir es que consulte la API. Hará mucho por ti, por lo que la dependencia podría valer la pena.

avatar de usuario
usuario1529313

Map<String, List<User>> usersByCountry = new HashMap<String, List<User>>();
for(User user : listOfUsers) {
    List<User> users = usersByCountry.get(user.getCountry());
    if (users == null) {        
        usersByCountry.put(user.getCountry(), users = new ArrayList<User>());
    }
    users.add(user);
}

¿Ha sido útil esta solución?