Как реализовать группировку авторов и их тегов в структуру Map<AuthorId, Map<BookId, List<TagName>>> за один стрим
Что проверяют
Умеете ли вы собирать вложенные структуры коллекторами, а не циклом с внешней мапой. Следом почти всегда спрашивают, что изменится, если теги нужны без повторов, важен порядок или у книги список тегов, а не одна строка.
Как отвечать
Сначала уточните форму входных данных. Если это плоские строки «автор, книга, тег», хватает двух вложенных groupingBy и mapping:
Map<AuthorId, Map<BookId, List<String>>> result = rows.stream()
.collect(groupingBy(Row::authorId,
groupingBy(Row::bookId,
mapping(Row::tagName, toList()))));Если у книги поле List<Tag>, вместо mapping берут flatMapping(b -> b.tags().stream().map(Tag::name), toList()), этот коллектор есть с Java 9. Если на входе авторы со списками книг, удобнее toMap(Author::id, a -> ...) с вложенным toMap по книгам.
Дальше назовите детали, которые отличают понимание от заученного шаблона:
- тип мапы:
groupingByпо умолчанию собираетHashMap, порядок не гарантирован. Нужен порядок, передайте фабрику:groupingBy(Row::authorId, LinkedHashMap::new, ...)илиTreeMap::new; - повторы тегов:
toSet()илиtoCollection(LinkedHashSet::new), если важен порядок появления; - изменяемость:
Collectors.toList()не обещает ни конкретный тип, ни изменяемость списка, аStream.toList()всегда возвращает неизменяемый; - сложность: один проход, O(n) по времени;
- параллельность:
groupingByConcurrentдаётConcurrentMap, но порядок элементов теряется.
Где спотыкаются
Пишут forEach с computeIfAbsent во внешнюю мапу и называют это стримом. Работает, но это побочный эффект, и в параллельном стриме такой код ломается.
Забывают про null: если у строки нет authorId, groupingBy падает с NullPointerException. Такие записи отфильтровывают заранее или подставляют явный ключ.
Используют toMap на данных с повторяющимися ключами без функции слияния и получают IllegalStateException на первом же дубле.