Java 8 представила концепцию коллекционеров. Большую часть времени мы едва используем фабричные методы из Collectors
класса, например collect(toList())
, toSet()
или, может быть, что-то более причудливое, например counting()
или groupingBy()
. Не многие из нас на самом деле удосуживаются посмотреть, как коллекторы определены и реализованы. Давайте начнем с анализа того, что на Collector<T, A, R>
самом деле и как это работает.
Collector<T, A, R>
работает как « приемник » для потоков — поток помещает элементы (один за другим) в коллектор, который должен в конечном итоге создать некоторое « собранное » значение. В большинстве случаев это означает создание коллекции (например toList()
) путем накопления элементов или уменьшения потока до чего-то меньшего (например, counting()
сборщик, который едва ли считает элементы). Каждый коллекционер принимает предметы типа T
и производит агрегированное (накопленное) значение типа R
(например R = List<T>
). Универсальный тип A
просто определяет тип промежуточной изменяемой структуры данных, которую мы собираемся использовать для накопления элементов типа T
за это время. Тип A
может, но не должен совпадать с R
— простыми словами, изменяемая структура данных, которую мы используем для сбора элементов из входных данных, Stream<T>
может отличаться от фактической выходной коллекции / значения. При этом каждый сборщик должен реализовывать следующие методы:
interface Collector<T,A,R> { Supplier<A> supplier() BiConsumer<A,T> acumulator() BinaryOperator<A> combiner() Function<A,R> finisher() Set<Characteristics> characteristics() }
supplier()
возвращает функцию, которая создает экземпляр изменяемой структуры данных, которую мы будем использовать для накопления элементов ввода типаT
.accumulator()
возвращает функцию, которая возьмет аккумулятор и один элемент типаT
, мутирующий аккумулятор.combiner()
используется для объединения двух аккумуляторов в один. Он используется, когда сборщик выполняется параллельно,Stream<T>
сначала разделяя входные данные и собирая их независимо.finisher()
берет аккумуляторA
и превращает его в значение результата, например, коллекцию, типаR
. Все это звучит довольно абстрактно, поэтому давайте сделаем простой пример.
Очевидно, что в Java 8 нет встроенного сборщика для ImmutableSet<T>
Guava. Однако создать его очень просто. Помните, что для итеративной сборки ImmutableSet
мы используем ImmutableSet.Builder<T>
— это будет наш аккумулятор.
import com.google.common.collect.ImmutableSet; public class ImmutableSetCollector<T> implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> { @Override public Supplier<ImmutableSet.Builder<T>> supplier() { return ImmutableSet::builder; } @Override public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() { return (builder, t) -> builder.add(t); } @Override public BinaryOperator<ImmutableSet.Builder<T>> combiner() { return (left, right) -> { left.addAll(right.build()); return left; }; } @Override public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() { return ImmutableSet.Builder::build; } @Override public Set<Characteristics> characteristics() { return EnumSet.of(Characteristics.UNORDERED); } }
Прежде всего, внимательно посмотрите на общие типы. Наш ImmutableSetCollector
принимает входные элементы типа T
, поэтому он работает для любого Stream<T>
. В конце концов он будет производить ImmutableSet<T>
— как и ожидалось. ImmutableSet.Builder<T>
будет нашей промежуточной структурой данных.
supplier()
возвращает функцию, которая создает новыйImmutableSet.Builder<T>
. Если вы не знакомы с лямбдами в Java 8,ImmutableSet::builder
это сокращение от() -> ImmutableSet.builder()
.accumulator()
возвращает функцию, которая принимаетbuilder
и один элемент типаT
. Это просто добавляет указанный элемент к строителю.combiner()
возвращает функцию, которая примет двух компоновщиков и превратит их в одного, добавив все элементы из одного из них в другой — и вернув последний. Наконец,finisher()
возвращает функцию, которая превратитсяImmutableSet.Builder<T>
вImmutableSet<T>
. Опять же, это сокращенная синтаксис:builder -> builder.build()
.- И последнее, но не менее важное,
characteristics()
сообщает JDK о возможностях нашего коллекционера Например, если быImmutableSet.Builder<T>
был потокобезопасным (это не так), мы могли бы также сказатьCharacteristics.CONCURRENT
.
Теперь мы можем использовать наш собственный сборщик везде collect()
:
final ImmutableSet<Integer> set = Arrays .asList(1, 2, 3, 4) .stream() .collect(new ImmutableSetCollector<>());
Однако создание нового экземпляра немного многословно, поэтому я предлагаю создать статический метод фабрики, аналогичный тому, что делает JDK:
public class ImmutableSetCollector<T> implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> { //... public static <T> Collector<T, ?, ImmutableSet<T>> toImmutableSet() { return new ImmutableSetCollector<>(); } }
Теперь мы можем воспользоваться всеми преимуществами нашего самодельного коллектору просто набрав: collect(toImmutableSet())
. Во второй части мы научимся писать более сложные и полезные сборщики.