Методы класса Collectors в Java - Вопросы

Всего: 7 вопросов

1. 

Чем Collectors.partitioningBy() отличается от Collectors.groupingBy()?

groupingBy() принимает функцию-классификатор и создаёт столько ключей, сколько разных значений она вернула: группы, которая не встретилась, в карте просто не будет. partitioningBy() принимает предикат и всегда возвращает Map<Boolean, ...> ровно с двумя ключами — false и true, — даже если одна половина пуста. Отсюда практическая разница: partitioned.get(true) вернёт пустой список, а grouped.get(true)null, и следующий же вызов метода на нём даст NullPointerException. С downstream-коллектором пустая половина получает «нулевое» значение коллектора: partitioningBy(n -> n % 2 == 0, counting()) на списке [1, 3, 5] даёт {false=3, true=0}. Поэтому при бинарном условии partitioningBy() безопаснее: не нужны ни проверки на null, ни getOrDefault().

2. 

Что делать, если Collectors.toMap() бросает IllegalStateException: Duplicate key, и зачем у него четвёртый аргумент?

Двухаргументный toMap(keyMapper, valueMapper) падает на первом же повторяющемся ключе. Лечится третьим аргументом — функцией слияния (merge): она получает старое и новое значение и возвращает то, которое останется в карте. Типовые варианты: (oldValue, newValue) -> oldValue (оставить первое), (oldValue, newValue) -> newValue (перезаписать последним) и Integer::sum (сложить). Например, toMap(Employee::department, Employee::salary, Integer::sum) посчитает фонд оплаты труда по отделам. Четвёртый аргумент — фабрика Map: TreeMap::new отсортирует ключи, LinkedHashMap::new сохранит порядок появления, EnumMap подойдёт для ключей-перечислений. Если нужны все значения с одинаковым ключом, а не одно, берите groupingBy() вместо toMap(). Отдельная ловушка: toMap() не переносит null в значениях — внутри вызывается Map.merge(), который бросает NullPointerException.

3. 

Какой порядок ключей у результата groupingBy() и как сделать его предсказуемым?

Без явной фабрики groupingBy() складывает результат в HashMap, а её порядок обхода зависит от хешей ключей и не связан ни с порядком элементов в потоке, ни с сортировкой. Поэтому Stream.of(30, 10, 20, 10, 50).collect(groupingBy(n -> n / 10)) печатает {1=[10, 10], 2=[20], 3=[30], 5=[50]}, хотя ключ 3 встретился первым. Предсказуемость даёт трёхаргументная форма groupingBy(classifier, mapFactory, downstream): TreeMap::new отсортирует ключи по естественному порядку, LinkedHashMap::new сохранит порядок первого появления в потоке. Сортировать поток через sorted() перед группировкой бесполезно — HashMap всё равно переставит ключи. При этом списки внутри групп порядок источника сохраняют всегда: перемешиваются только ключи.

4. 

Какие три формы есть у Collectors.joining() и какие у него ограничения?

joining() без аргументов склеивает элементы подряд; joining(delimiter) вставляет разделитель; joining(delimiter, prefix, suffix) дополнительно обрамляет результат. Например, joining(", ", "[", "]") на именах даёт [Анна, Борис, Вера], а joining(", ", "WHERE department IN (", ")") собирает готовый фрагмент SQL. Ограничение одно, но важное: коллектор работает только с потоком CharSequence, поэтому объекты сначала переводят в строки через map(Employee::name) или map(Object::toString) — иначе код не скомпилируется. Внутри используется StringBuilder, так что лишних промежуточных строк, как у наивного reduce("", String::concat), не возникает. На пустом потоке трёхаргументная форма возвращает []: префикс и суффикс добавляются всегда.

5. 

Зачем нужны counting(), summingInt(), averagingInt() и summarizingInt(), если у потока есть count(), sum() и average()?

Их место — позиция downstream-коллектора внутри groupingBy() или partitioningBy(), где методы самого потока недоступны: groupingBy(Employee::department, counting()) даёт численность каждого отдела, а groupingBy(Employee::department, summingInt(Employee::salary)) — фонд оплаты труда. summarizingInt() за один проход отдаёт IntSummaryStatistics сразу с count, sum, min, max и average. О чём важно помнить: counting() возвращает Long, поэтому объявление Map<String, Integer> с ним не скомпилируется; summingInt() копит сумму в int и молча переполняется — для денег берите summingLong(); averaging* всегда возвращают Double и на пустом потоке дают 0.0, а не пустой Optional, так что «данных не было» и «среднее равно нулю» по результату неразличимы.

6. 

Что делает Collectors.teeing() и когда он незаменим?

teeing() появился в Java 12 и принимает три аргумента: два коллектора и функцию-объединитель, которая получает оба их результата. Он считает две разные агрегации по одним и тем же данным за один проход: teeing(counting(), summingInt(Employee::salary), Payroll::new) вернёт Payroll[headcount=5, total=750000], а teeing(minBy(cmp), maxBy(cmp), (min, max) -> ...) даст минимум и максимум сразу. Это принципиально, когда источник одноразовый — файл, сетевой ответ, результат запроса, — и повторный обход попросту невозможен: поток нельзя использовать дважды. teeing() можно вкладывать в groupingBy() как downstream-коллектор и получать пару агрегатов по каждой группе.

7. 

Как написать свой коллектор через Collector.of() и почему в нём легко не заметить ошибку?

Реализовывать интерфейс Collector отдельным классом не нужно — достаточно статического метода Collector.of() с четырьмя функциями: supplier создаёт пустой контейнер-накопитель, accumulator добавляет в него очередной элемент, combiner объединяет два контейнера, finisher превращает контейнер в итоговый результат. В типах Collector<T, A, R> буква T — тип элемента потока, A — тип промежуточного контейнера, R — тип результата; контейнер обязан быть изменяемым, поэтому для произведения чисел берут long[] из одного элемента, а не Long. Если контейнер и результат совпадают по типу, finisher не нужен — есть перегрузка из трёх функций. Главная ловушка: в последовательном потоке combiner не вызывается ни разу, поэтому ошибка в нём годами не проявляется и всплывает в день, когда кто-то допишет .parallel(). Проверяйте свой коллектор и на parallelStream(). Для одноразовой сборки проще трёхаргументный collect(ArrayList::new, ArrayList::add, ArrayList::addAll).

Страница 1 из 1