Как посчитать количество вхождений каждого слова с помощью группировки в Java Stream API
Что проверяют
Знаете ли вы Collectors.groupingBy по поведению, а не по названию: какую Map он вернёт, как поменять её тип и зачем нужен вложенный коллектор. Следом просят вывести топ-N слов или посчитать без учёта регистра и пунктуации.
Как отвечать
Базовое решение:
Map<String, Long> counts = Pattern
.compile("\\W+", Pattern.UNICODE_CHARACTER_CLASS)
.splitAsStream(text.toLowerCase(Locale.ROOT))
.filter(w -> !w.isEmpty())
.collect(Collectors.groupingBy(
Function.identity(), Collectors.counting()));Что стоит проговорить:
- разбиение:
split(" ")ломается на двойных пробелах, табуляции и пунктуации. Без флагаUNICODE_CHARACTER_CLASSкласс\wпонимает только ASCII и режет русские слова на пустоту; - регистр:
toLowerCase(Locale.ROOT), чтобы результат не зависел от локали машины; - тип результата:
groupingByотдаётHashMapбез порядка. Нужен порядок, передайте фабрику:groupingBy(identity(), TreeMap::new, counting()); - вложенный коллектор:
counting()возвращаетLong. ДляIntegerберутsummingInt(w -> 1); - альтернатива:
Collectors.toMap(w -> w, w -> 1, Integer::sum)делает то же через функцию слияния; - параллельно: для
parallelStreamестьgroupingByConcurrent, но на небольшом тексте выигрыша не будет.
Топ-5 делается вторым потоком: counts.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).limit(5).
Где спотыкаются
Показывают вывод в «правильном» порядке, хотя HashMap порядка не гарантирует. Интервьюер спросит, почему hello оказался первым.
Не фильтруют пустые строки: если текст начинается с пробела или знака препинания, первым токеном будет "", и он тоже посчитается.
Пишут forEach с map.put(word, map.get(word) + 1). Это побочный эффект, который ломается на parallelStream и падает с NullPointerException на первом же новом слове. Императивный вариант пишется через merge(word, 1, Integer::sum).