Группировка по атрибуту Collection с использованием потоков Java

У меня есть объект, который содержит коллекцию строк, скажем, языки, на которых говорит человек.

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

   // ...
}

Теперь, создавая несколько экземпляров, как это...

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

... то, что я хочу, это Map<String, List<Person>>, для каждого языка, перечисляющая людей, говорящих на этом языке:

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

Конечно, это может быть легко запрограммировано в обязательном порядке, но как сделать это функционально с помощью Java Streams? Я пробовал что-то вроде people.stream.collect(groupingBy(Person::getLanguagesSpoken)) но это, конечно, дает мне Map<List<String>, List<Person>>. Все примеры, которые я смог найти, используют groupingBy для Primitives или Strings.

Ответ 1

Вы можете разбить экземпляры Person на пары языка и Person (используя flatMap), а затем сгруппировать их по мере необходимости:

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

Ответ 2

Это можно обойтись и без потоков, все еще используя новые функции java-8.

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