Group by a Collection attribute using Java Streams

Viewed 1527

I have an object that contains a Collection of strings, let's say the languages that a person speaks.

public class Person {
   private String name;
   private int age;
   private List<String> languagesSpoken;

   // ...
}

Now, creating some instances like this...

Person p1 = new Person("Bob", 21, Arrays.asList("English", "French", "German"));
Person p2 = new Person("Alice", 33, Arrays.asList("English", "Chinese", "Spanish"));
Person p3 = new Person("Joe", 43, Arrays.asList("English", "Dutch", "Spanish", "German"));

//put them in list
List<Person> people = Arrays.asList(p1,p2,p3);

... what I want to have is a Map<String, List<Person>>, for every language listing the persons that speak the language:

["English" -> [p1, p2, p3],
 "German"  -> [p1, p3],
 etc. ]

Of course this can be programmed easily in an imperative way, but how to do it the functional way with Java Streams? I have tried something like people.stream.collect(groupingBy(Person::getLanguagesSpoken)) but that of course gives me a Map<List<String>, List<Person>>. All the examples I could find, are using groupingBy on Primitives or Strings.

2 Answers

You can break the Person instances into pairs of language and Person (using flatMap) and then group them as required:

Map<String, List<Person>> langPersons =
    people.stream()
          .flatMap(p -> p.getLanguagesSpoken()
                         .stream()
                         .map(l -> new SimpleEntry<>(l,p)))
          .collect(Collectors.groupingBy(Map.Entry::getKey,
                                         Collectors.mapping(Map.Entry::getValue,
                                                            Collectors.toList())));

This is possible to do without streams too, still using java-8 new features.

people.forEach(x -> {
        x.getLanguagesSpoken().forEach(lang -> {
            langPersons.computeIfAbsent(lang, ignoreMe -> new ArrayList<>()).add(x);
        });
});
Related