2010-06-10 10 views
37

Ich brauche oft eine Liste von Objekten und gruppiere sie in einer Map basierend auf einem Wert, der im Objekt enthalten ist. Z.B. Nimm eine Liste von Nutzern und Gruppen nach Ländern.Verknüpfung zum Hinzufügen zur Liste in einer HashMap

Mein Code für diese sieht in der Regel wie:

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

aber ich kann nicht umhin zu denken, dass dies umständlich ist und einige Guru haben einen besseren Ansatz. Die nächste, die ich bis jetzt sehen kann, ist die MultiMap from Google Collections.

Gibt es Standardansätze?

Danke!

+1

Sollte das wirklich "Map >'? Die Antwort macht einen Unterschied für das, was Sie bauen oder verwenden möchten. Beachten Sie, dass Google Collections Verfeinerungen für die geschachtelten Auflistungen verschiedener Arten von Listen und Gruppen bietet. – seh

+0

Lassen Sie Java einfach für .Net und Linq fallen. –

+1

@Hamish: Ja, wegen unserer Sorgen über Abhängigkeiten sind dann total irrelevant! – Carl

Antwort

49

In Java 8 können Sie Map#computeIfAbsent() verwenden.

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

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

Oder Collectors#groupingBy() Verwendung von Stream-API machen von List zu Map direkt zu gehen:

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

In Java 7 oder darunter, am besten, was man bekommt, ist unten:

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 hat eine LazyMap, aber es ist nicht parametrisiert. Guava hat keine Art von LazyMap oder LazyList, aber Sie können Multimap dafür verwenden, wie in answer of polygenelubricants below gezeigt.

+0

Sie könnten es noch ein wenig weiter verkürzen: 'usersByCountry.put (user.getCountry(), users = new ArrayList <>());' Obwohl ich sicher bin, dass einige darüber die Stirn runzeln würden. – shmosel

+0

Ich weiß, es ist egal, aber vielleicht müssen Neulinge wissen, dass die Mapping-Funktion den Schlüssel als Argument bekommt, also wäre es besser, 'k' anstelle von 'v' zu verwenden. UsersByCountry.computeIfAbsent (user.getCountry (), k -> new ArrayList <>()). add (user); ' –

2

Wenn ich mit einer Sammlung-Wert-Karte beschäftigen muss, komme ich immer fast immer schreiben ein wenig putIntoListMap() statische Dienstprogramm-Methode in der Klasse. Wenn ich finde, dass ich es in mehreren Klassen brauche, werfe ich diese Methode in eine Utility-Klasse. Statische Methodenaufrufe wie diese sind ein bisschen hässlich, aber sie sind viel sauberer, als den Code jedes Mal zu tippen. Wenn Multi-Maps in Ihrer App nicht eine ziemlich zentrale Rolle spielen, ist es wahrscheinlich nicht wert, eine andere Abhängigkeit zu ziehen.

+0

Auch die Optimierung von BalusC ist gut zu wissen. –

1

Es scheint, dass Ihre genauen Anforderungen von LinkedHashMultimap in der GC-Bibliothek erfüllt werden. Wenn Sie mit den Abhängigkeiten leben können, den gesamten Code wird:

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

Auftrag gehalten wird (über alle sieht es aus wie Sie mit Ihrer Liste taten) und Duplikate ausgeschlossen werden; Sie können natürlich zu einem einfachen Hash-basierten Satz oder einem Baumsatz wechseln, wenn Bedarf diktiert (oder eine Liste, obwohl das nicht das zu sein scheint, was Sie brauchen). Leere Sammlungen werden zurückgegeben, wenn Sie nach einem Land ohne Benutzer fragen, jeder bekommt Ponys usw. - was ich meine ist, sehen Sie sich die API an. Es wird viel für dich tun, also könnte sich die Abhängigkeit lohnen.

+0

+1 Danke, das ist gut zu wissen, aber BalusCs Optimisierung war, was ich suchte. – Damo

0

Eine saubere und gut lesbare Art und Weise ein Element hinzuzufügen, ist die folgende:

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

Beachten Sie, dass containsKey und get Aufruf ist nicht langsamer als get nur anrufen und das Ergebnis für null testen.

+0

Der Aufruf an sich ist zwar nicht langsamer, aber das Nachschlagen erfolgt jetzt zweimal statt einmal. – BalusC

+0

Ich habe es geklärt. – starblue

19

Guava ist Multimap wirklich ist die am besten geeignete Datenstruktur für diese, und in der Tat gibt es Multimaps.index(Iterable<V>, Function<? super V,K>) Dienstprogramm Methode, die genau das tut, was Sie wollen: Nehmen Sie eine Iterable<V> (das ist ein List<V> ist), und wenden Sie die Function<? super V, K> die Schlüssel zu bekommen für die Multimap<K,V>.

Hier ist ein Beispiel aus der Dokumentation:

Zum Beispiel

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

drucken

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

In Ihrem Fall, dass Sie ein Function<User,String> userCountryFunction = ... schreiben würden.

+2

+1 Es frustriert mich, dass Antworten, die viel mehr Code als das schreiben, höher eingestuft werden, nur weil sie am schnellsten kamen. :( –

+2

@Kevin: Ich hatte gehofft, dass du irgendwann vorbeikommen würdest =) Übrigens Ich habe vor, irgendwann Q/A-Artikel über Stackoverflow in verschiedenen Guava-Klassen zu schreiben, um seine Fähigkeiten zu demonstrieren. – polygenelubricants

+2

Ich komme nur ein- oder zweimal am Tag vorbei und garantiere, dass ich nie eine Chance habe, meine Antworten zu ändern. Ich denke, deine Idee ist großartig. Ich nehme an, Sie meinen, eine Frage zu stellen und selbst zu beantworten. Sie werden ein paar Leute bekommen, die Ihnen sagen, dass etwas unmoralisches daran ist, aber es wird explizit von der breiteren SO-Community sanktioniert, da ihr Ziel darin besteht, dass SO großartigen Inhalt hat. –

2

Durch lambdaj verwenden Sie dieses Ergebnis mit nur einer Zeile Code erhalten kann, wie es folgt:

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

Lambdaj bietet auch viele andere Features Sammlungen mit einer sehr lesbaren domänenspezifische Sprache zu manipulieren.

+0

+1 das ist nett. Sieht sehr brauchbar aus. – Damo

2

Wir scheinen dies eine Menge Zeit zu tun, so habe ich eine Template-Klasse

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

Sie bieten nur impl für groupBy

in Ihrem Fall

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