J'ai souvent besoin de prendre une liste d'objets et de les regrouper dans une carte basée sur une valeur contenue dans l'objet. Par exemple. prendre une liste d'utilisateurs et regrouper par pays.
Mon code pour cela ressemble généralement à:
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);
}
}
Cependant, je ne peux m'empêcher de penser que c'est gênant et certains gourous ont une meilleure approche. Le plus proche que je peux voir jusqu'à présent est le MultiMap de Google Collections .
Existe-t-il des approches standard?
Merci!
Dans Java 8, vous pouvez utiliser Map#computeIfAbsent()
.
Map<String, List<User>> usersByCountry = new HashMap<>();
for (User user : listOfUsers) {
usersByCountry.computeIfAbsent(user.getCountry(), k -> new ArrayList<>()).add(user);
}
Ou utilisez les API Stream Collectors#groupingBy()
pour passer de List
à Map
directement:
Map<String, List<User>> usersByCountry = listOfUsers.stream().collect(Collectors.groupingBy(User::getCountry));
En Java 7 ou inférieur, le mieux que vous pouvez obtenir est ci-dessous:
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);
}
Collections communes a un LazyMap
, mais il n'est pas paramétré. Guava n'a pas une sorte de LazyMap
ou LazyList
, mais vous pouvez utiliser Multimap
pour cela comme indiqué en réponse des polygénelubrifiants ci-dessous .
Multimap
de la goyave est vraiment la structure de données la plus appropriée pour cela, et en fait, il existe Multimaps.index(Iterable<V>, Function<? super V,K>)
méthode utilitaire qui fait exactement ce que vous voulez: prenez un Iterable<V>
(dont un List<V>
est), et appliquez le Function<? super V, K>
pour obtenir les clés du Multimap<K,V>
.
Voici un exemple tiré de la documentation:
Par exemple,
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);
impressions
{4=[Inky], 5=[Pinky, Pinky, Clyde], 6=[Blinky]}
Dans votre cas, vous écririez un Function<User,String> userCountryFunction = ...
.
Nous semblons le faire souvent, j'ai donc créé une classe de modèles
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);
}
Vous fournissez juste impl pour groupBy
dans ton cas
String groupBy(User u){return user.getCountry();}
Quand je dois gérer une carte de valeur de collection, je finis presque toujours par écrire une petite méthode utilitaire statique putIntoListMap () dans la classe. Si je me retrouve en avoir besoin dans plusieurs classes, je jette cette méthode dans une classe utilitaire. Les appels de méthode statique comme celui-ci sont un peu laids, mais ils sont beaucoup plus propres que de taper le code à chaque fois. À moins que les multi-cartes jouent un rôle assez central dans votre application, à mon humble avis, cela ne vaut probablement pas la peine d'attirer une autre dépendance.
En utilisant lambdaj vous pouvez obtenir ce résultat avec une seule ligne de code comme suit:
Group<User> usersByCountry = group(listOfUsers, by(on(User.class).getCountry()));
Lambdaj offre également de nombreuses autres fonctionnalités pour manipuler les collections avec un langage spécifique au domaine très lisible.
Il semble que vos besoins exacts soient satisfaits par LinkedHashMultimap dans la bibliothèque GC. Si vous pouvez vivre avec les dépendances, tout votre code devient:
SetMultimap<String,User> countryToUserMap = LinkedHashMultimap.create();
// .. other stuff, then whenever you need it:
countryToUserMap.put(user.getCountry(), user);
l'ordre d'insertion est conservé (à peu près tout ce que vous faites avec votre liste) et les doublons sont exclus; vous pouvez bien sûr basculer vers un ensemble basé sur un hachage simple ou un ensemble d'arbres selon les besoins (ou une liste, bien que cela ne semble pas être ce dont vous avez besoin). Les collections vides sont retournées si vous demandez un pays sans utilisateurs, tout le monde obtient des poneys, etc. - ce que je veux dire, consultez l'API. Cela fera beaucoup pour vous, donc la dépendance en vaut la peine.
Une façon claire et lisible d'ajouter un élément est la suivante:
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);
Notez que l'appel de containsKey
et get
n'est pas plus lent que d'appeler simplement get
et de tester le résultat pour null
.
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);
}