90

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!

Damo
  • 11,410
  • 5
  • 57
  • 74
  • 1
    Should that really be `Map>`? The answer makes a difference for what you choose to build or use. Note that Google Collections provides refinements for the nested collections being of various types of lists and sets. – seh Jun 11 '10 at 00:04
  • Just drop Java for .Net and Linq. – Hamish Grubijan Jun 11 '10 at 00:11
  • 1
    @Hamish: yeah, because of our worries about dependencies are totally irrelevant then! – Carl Jun 11 '10 at 01:20
  • 2
    @Hamish: I could never face my .Net coding brother again if I did that. The shame! – Damo Jun 11 '10 at 06:14

9 Answers9

145

Since 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.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • You could shorten it a bit further: `usersByCountry.put(user.getCountry(), users = new ArrayList<>());` Though I'm sure some would frown on that. – shmosel Mar 17 '17 at 01:30
23

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

prints

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

In your case you'd write a Function<User,String> userCountryFunction = ....

polygenelubricants
  • 376,812
  • 128
  • 561
  • 623
  • 3
    +1 It frustrates me that answers involving writing a lot more code than this are ranked higher, just because they were the fastest to come in. :( – Kevin Bourrillion Jun 11 '10 at 16:37
  • 3
    @Kevin: I was hoping you'd stop by eventually =) By the way, I plan to eventually write Q/A articles on stackoverflow on various Guava classes to demonstrate its capabilities. – polygenelubricants Jun 11 '10 at 16:39
  • 2
    I stop by only once or twice a day, thus guaranteeing that I never have a chance to get my answers upvoted. I think your idea is a great one. I assume you mean posting a question and answering it yourself. You'll get a few people telling you there's something immoral about this, but it is explicitly sanctioned by the broader SO community, since their goal is for SO to have great content. – Kevin Bourrillion Jun 11 '10 at 16:47
  • 1
    +1 for the example. I'd be curious to the articles. Guava libraries should be brought into attention more. – BalusC Jun 11 '10 at 16:49
2

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();}
Jukey
  • 21
  • 1
2
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);
}
2

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.

Luke Maurer
  • 7,845
  • 2
  • 24
  • 24
2

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.

Mario Fusco
  • 13,548
  • 3
  • 28
  • 37
1

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.

Carl
  • 7,538
  • 1
  • 40
  • 64
0

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.

starblue
  • 55,348
  • 14
  • 97
  • 151
0

ArrayList numbersList = new ArrayList<>(Arrays.asList(1, 1, 2, 3, 3, 3, 4, 5, 6, 6, 6, 7, 8));

Map<Integer, Long> elementCountMap = numbersList.stream() .collect(Collectors.toMap(Function.identity(), v -> 1L, Long::sum));

System.out.println(elementCountMap);

o/p:{1=2, 2=1, 3=3, 4=1, 5=1, 6=3, 7=1, 8=1}

AnushaSP07
  • 186
  • 1
  • 3
  • 7