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()). Во второй части мы научимся писать более сложные и полезные сборщики.