Shortcut for adding to List in a HashMap
JavaCollectionsHashmapJava Problem Overview
I often have a need to take a list of objects and group them into a Map based on a value contained in the object. Eg. take a list of Users and group by Country.
My code for this usually looks like:
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);
}
}
However I can't help thinking that this is awkward and some guru has a better approach. The closest I can see so far is the MultiMap from Google Collections.
Are there any standard approaches?
Thanks!
Java Solutions
Solution 1 - Java
In Java 8 you can make use of Map#computeIfAbsent()
.
Map<String, List<User>> usersByCountry = new HashMap<>();
for (User user : listOfUsers) {
usersByCountry.computeIfAbsent(user.getCountry(), k -> new ArrayList<>()).add(user);
}
Or, make use of Stream API's Collectors#groupingBy()
to go from List
to Map
directly:
Map<String, List<User>> usersByCountry = listOfUsers.stream().collect(Collectors.groupingBy(User::getCountry));
In Java 7 or below, best what you can get is below:
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 has a LazyMap
, but it's not parameterized. Guava doesn't have sort of a LazyMap
or LazyList
, but you can use Multimap
for this as shown in answer of polygenelubricants below.
Solution 2 - Java
Guava's Multimap
really is the most appropriate data structure for this, and in fact, there is Multimaps.index(Iterable<V>, Function<? super V,K>)
utility method that does exactly what you want: take an Iterable<V>
(which a List<V>
is), and apply the Function<? super V, K>
to get the keys for the Multimap<K,V>
.
Here's an example from the documentation:
> For example,
> List
> {4=[Inky], 5=[Pinky, Pinky, Clyde], 6=[Blinky]}
In your case you'd write a Function<User,String> userCountryFunction = ...
.
Solution 3 - Java
When I have to deal with a collection-valued map, I just about always wind up writing a little putIntoListMap() static utility method in the class. If I find myself needing it in multiple classes, I throw that method into a utility class. Static method calls like that are a bit ugly, but they're much cleaner than typing the code out every time. Unless multi-maps play a pretty central role in your app, IMHO it's probably not worth it to pull in another dependency.
Solution 4 - Java
By using lambdaj you can obtain that result with just one line of code as it follows:
Group<User> usersByCountry = group(listOfUsers, by(on(User.class).getCountry()));
Lambdaj also offers lots of other features to manipulate collections with a very readable domain specific language.
Solution 5 - Java
We seem to do this a lot of times so I created a template class
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);
}
You just provide impl for groupBy
in your case
String groupBy(User u){return user.getCountry();}
Solution 6 - Java
It looks like your exact needs are met by LinkedHashMultimap in the GC library. If you can live with the dependencies, all your code becomes:
SetMultimap<String,User> countryToUserMap = LinkedHashMultimap.create();
// .. other stuff, then whenever you need it:
countryToUserMap.put(user.getCountry(), user);
insertion order is maintained (about all it looks like you were doing with your list) and duplicates are precluded; you can of course switch to a plain hash-based set or a tree set as needs dictate (or a list, though that doesn't seem to be what you need). Empty collections are returned if you ask for a country with no users, everyone gets ponies, etc - what I mean is, check out the API. It'll do a lot for you, so the dependency might be worth it.
Solution 7 - Java
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);
}
Solution 8 - Java
A clean and readable way to add an element is the following:
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);
Note that calling containsKey
and get
is not slower than just calling get
and testing the result for null
.