2010-06-10 14 views
37

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. tomar una lista de usuarios y agrupar por país.Acceso directo para agregar a la Lista en un HashMap

Mi código para esto por lo general se parece a:

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 algún gurú tiene un mejor enfoque. Lo más cercano que puedo ver hasta ahora es el MultiMap from Google Collections.

¿Hay algún enfoque estándar?

Gracias!

+1

En caso de que se realmente ser 'Map >'? La respuesta hace la diferencia para lo que elijas construir o usar. Tenga en cuenta que Google Collections proporciona mejoras para que las colecciones anidadas sean de varios tipos de listas y conjuntos. – seh

+0

Simplemente suelte Java para .Net y Linq. –

+1

@Hamish: ¡sí, debido a nuestras preocupaciones sobre las dependencias son totalmente irrelevantes! – Carl

Respuesta

49

En Java 8 puede 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, hacer uso de la Corriente del API de Collectors#groupingBy() pasar de List a Map directamente:

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

En Java 7 o menos, mejor de lo que puede conseguir es a continuación:

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

Commons Collections tiene un LazyMap, pero no tiene parámetros. Guava no tiene un tipo de LazyMap o LazyList, pero puede usar Multimap para esto como se muestra en answer of polygenelubricants below.

+0

Podría acortarlo un poco más: 'usersByCountry.put (user.getCountry(), users = new ArrayList <>());' Aunque estoy seguro de que algunos no prestarían atención a eso. – shmosel

+0

Sé que no importa, pero quizás los novatos necesiten saber que la función de mapeo obtendrá la clave como argumento, por lo que sería mejor usar 'k' en lugar de 'v' 'usersByCountry.computeIfAbsent (user.getCountry (), k -> new ArrayList <>()). add (usuario); ' –

2

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

+0

Además, la optimización de BalusC es buena para saber. –

1

Parece que sus necesidades exactas se cumplen por LinkedHashMultimap en la biblioteca de GC. Si se puede vivir con las dependencias, todo el código se vuelve: se mantiene

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

orden de inserción (sobre todo parece que estaba haciendo con su lista) y los duplicados imposibilitan; Por supuesto, puede cambiar a un conjunto sencillo basado en hash o un conjunto de árboles según lo requiera la necesidad (o una lista, aunque eso no parece ser lo que necesita). Se devuelven colecciones vacías si solicita un país sin usuarios, todos obtienen ponis, etc., lo que quiero decir es que revise la API. Hará mucho por ti, por lo que la dependencia puede valer la pena.

+0

+1 Gracias, es bueno saberlo, pero la opción de BalusC era lo que buscaba. – Damo

0

una manera limpia y fácil de leer para añadir un elemento es el siguiente:

String country = user.getCountry(); 
Set<User> users 
if (users.containsKey(country)) 
{ 
    users = usersByCountry.get(user.getCountry()); 
} 
else 
{ 
    users = new HashSet<User>(); 
    usersByCountry.put(country, users); 
} 
users.add(user); 

Tenga en cuenta que llamar containsKey y get no es más lento que simplemente llamando get y probando el resultado para null.

+0

La llamada en sí misma no es más lenta, pero la búsqueda ahora ocurre dos veces en lugar de una vez. – BalusC

+0

Lo he aclarado. – starblue

19

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

He aquí 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); 

impresiones

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

En el caso de que iba a escribir un Function<User,String> userCountryFunction = ....

+2

+1 Me frustra que las respuestas que implican escribir mucho más código que este se clasifiquen más alto, simplemente porque fueron las más rápidas para entrar. (( –

+2

) @ Kevin: Esperaba que finalmente pasaras por aquí =) Por cierto , Planeo eventualmente escribir artículos Q/A sobre stackoverflow en varias clases de Guava para demostrar sus capacidades. – polygenelubricants

+2

Me detengo solo una o dos veces al día, garantizando así que nunca tenga la oportunidad de obtener una respuesta favorable. Creo que tu idea es genial. Supongo que te refieres a publicar una pregunta y responderla tú mismo. Algunas personas te dirán que hay algo inmoral al respecto, pero está explícitamente sancionado por la comunidad SO más amplia, ya que su objetivo es que SO tenga un gran contenido. –

2

Mediante el uso de lambdaj puede obtener ese resultado con una sola línea de código como sigue:

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 fácil de leer.

+0

+1 eso es bueno. Parece muy útil. – Damo

2

Nos parece que hacer 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(t); 
     List<T> innerList = map.containsKey(key) ? map.get(key) : new ArrayList<T>(); 
     innerList.add(t); 
     map.put(key, innerList); 
    } 
    return map; 
} 

protected abstract K groupBy(T t); 
} 

Usted acaba de proporcionar impl para GroupBy

en su caso

String groupBy(User u){return user.getCountry();} 
0
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); 
} 
Cuestiones relacionadas