Создание собственного сборщика
Реализуйте интерфейс Collector, чтобы создать повторно используемую агрегацию для конкретной задачи предметной области.
«Создание собственного сборщика» — бесплатный урок Java Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.
Зачем создавать пользовательский сборщик?
Если встроенные сборщики не подходят для вашей предметной области, реализуйте интерфейс Collector<T, A, R>, чтобы создать повторно используемую и комбинируемую логику агрегации. T — входные данные, A — изменяемый аккумулятор, R — результат.
Интерфейс сборщика
Сборщик состоит из пяти компонентов:
supplier()— создаёт начальный аккумуляторaccumulator()— добавляет один элемент в аккумуляторcombiner()— объединяет два аккумулятора для параллельных потоковfinisher()— преобразует аккумулятор в окончательный результатcharacteristics()— предоставляет подсказки для оптимизации
Фабрика Collector.of()
Самый простой способ создать пользовательский сборщик — использовать Collector.of(), не реализуя интерфейс напрямую:
import java.util.stream.*;
// Collector that builds a comma-separated String
Collector<String, StringBuilder, String> csvCollector = Collector.of(
StringBuilder::new, // supplier
(sb, s) -> {
if (sb.length() > 0) sb.append(',');
sb.append(s);
}, // accumulator
(a, b) -> {
if (a.length() > 0 && b.length() > 0) a.append(',');
a.append(b); return a;
}, // combiner
StringBuilder::toString // finisher
);
String csv = Stream.of("Alice","Bob","Carol").collect(csvCollector);
System.out.println(csv); // Alice,Bob,CarolПользовательский сборщик: TopN
Создайте сборщик, который оставляет только N лучших элементов согласно компаратору:
import java.util.*;
import java.util.stream.*;
static <T> Collector<T, ?, List<T>> topN(int n, Comparator<T> comp) {
return Collector.of(
() -> new ArrayList<T>(),
(list, e) -> {
list.add(e);
list.sort(comp.reversed());
if (list.size() > n) list.remove(list.size() - 1);
},
(a, b) -> {
a.addAll(b);
a.sort(comp.reversed());
while (a.size() > n) a.remove(a.size() - 1);
return a;
},
Collector.Characteristics.UNORDERED
);
}Использование сборщика TopN
Применяйте пользовательский сборщик TopN к потоку:
List<Integer> top3 = Stream.of(5,2,9,1,7,3,8)
.collect(topN(3, Comparator.naturalOrder()));
System.out.println(top3); // [9, 8, 7]Флаги характеристик
Три необязательные характеристики позволяют настроить оптимизацию потока:
IDENTITY_FINISH— функция завершения тождественна аккумулятору, поэтому её можно пропуститьUNORDERED— порядок результата не имеет значенияCONCURRENT— аккумулятор можно вызывать одновременно из нескольких потоков
Неправильная установка флага, например CONCURRENT без потокобезопасности, приводит к ошибкам.
Пользовательский сборщик: скользящее среднее
Поддерживайте текущее среднее значение без второго прохода:
record RunningAvg(long count, double sum) {
RunningAvg add(double v) { return new RunningAvg(count+1, sum+v); }
RunningAvg merge(RunningAvg o) { return new RunningAvg(count+o.count, sum+o.sum); }
double avg() { return count == 0 ? 0 : sum / count; }
}
Collector<Double, RunningAvg[], Double> avgCollector = Collector.of(
() -> new RunningAvg[]{new RunningAvg(0, 0)},
(a, v) -> a[0] = a[0].add(v),
(a, b) -> { a[0] = a[0].merge(b[0]); return a; },
a -> a[0].avg()
);
double avg = Stream.of(10.0, 20.0, 30.0).collect(avgCollector);
System.out.println(avg); // 20.0Прямая реализация интерфейса сборщика
Для большего контроля реализуйте Collector<T,A,R> в виде класса:
import java.util.function.*;
import java.util.stream.*;
class StringJoiningCollector implements Collector<String, List<String>, String> {
private final String delimiter;
StringJoiningCollector(String delimiter) { this.delimiter = delimiter; }
public Supplier<List<String>> supplier() { return ArrayList::new; }
public BiConsumer<List<String>, String> accumulator() { return List::add; }
public BinaryOperator<List<String>> combiner() {
return (a,b) -> { a.addAll(b); return a; };
}
public Function<List<String>, String> finisher() {
return list -> String.join(delimiter, list);
}
public Set<Characteristics> characteristics() { return Set.of(); }
}Сравнение пользовательских и встроенных сборщиков
Прежде чем писать пользовательский сборщик, проверьте, нельзя ли получить тот же результат, объединив встроенные сборщики: они уже протестированы и оптимизированы:
// Instead of custom, compose:
Map<String, IntSummaryStatistics> stats = orders.stream().collect(
Collectors.groupingBy(Order::status,
Collectors.summarizingInt(Order::amount))
);
// This replaces many custom collector implementationsПотокобезопасность пользовательских сборщиков
Если задана характеристика CONCURRENT, аккумулятор одновременно вызывается из нескольких потоков. Поэтому аккумулятор должен быть потокобезопасным, например следует использовать ConcurrentHashMap или AtomicLong. Без CONCURRENT параллельность безопасно обрабатывается объединителем.
Практический пример: сборщик гистограммы
Распределяйте числовые значения по диапазонам в карте гистограммы:
Collector<Integer, Map<Integer, Long>, Map<Integer, Long>> histogram =
Collector.of(
LinkedHashMap::new,
(map, val) -> map.merge(val / 10 * 10, 1L, Long::sum),
(a, b) -> { b.forEach((k,v) -> a.merge(k,v,Long::sum)); return a; },
Collector.Characteristics.IDENTITY_FINISH
);
Map<Integer,Long> hist = Stream.of(5,15,25,12,8,22,18,3)
.collect(histogram);
hist.forEach((bucket, cnt) -> System.out.println(bucket+"-"+(bucket+9)+": "+cnt));Быстрая проверка
Какой флаг Collector.Characteristics указывает, что функция завершения является тождественным преобразованием и её можно пропустить?
Повторение: пользовательские сборщики
Основные выводы:
- Collector<T,A,R>: T — входные данные, A — аккумулятор, R — результат
- Используйте Collector.of() для быстрой реализации без создания класса
- supplier/accumulator/combiner/finisher/characteristics — это пять компонентов
- IDENTITY_FINISH, UNORDERED и CONCURRENT — флаги оптимизации
- Предпочитайте объединение встроенных сборщиков; создавайте пользовательский сборщик только при необходимости
Изучай Java с ИИ-репетитором — бесплатно
Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.
- Курсы
- 104
- Уроки
- 374
Часто задаваемые вопросы
Урок «Создание собственного сборщика» бесплатный?
Да — полный текст урока «Создание собственного сборщика» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.
Чему я научусь в уроке «Создание собственного сборщика»?
Реализуйте интерфейс Collector, чтобы создать повторно используемую агрегацию для конкретной задачи предметной области. Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Java Academy?
Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Создание собственного сборщика»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Java Academy?
Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- groupingBy: классификация элементов
- partitioningBy и подсчёт
- toMap, joining и summarizing
- Создание собственного сборщика