Статьи

Введение в написание пользовательских сборщиков в Java 8

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> может отличаться от фактической выходной коллекции / значения. При этом каждый сборщик должен реализовывать следующие методы:

1
2
3
4
5
6
7
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> из Гуавы. Однако создать его очень просто. Помните, что для итеративной сборки ImmutableSet мы используем ImmutableSet.Builder<T> — это будет наш аккумулятор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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() :

1
2
3
4
final ImmutableSet<Integer> set = Arrays
        .asList(1, 2, 3, 4)
        .stream()
        .collect(new ImmutableSetCollector<>());

Однако создание нового экземпляра немного многословно, поэтому я предлагаю создать статический метод фабрики, аналогичный тому, что делает JDK:

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

Обновить

@akarazniewicz отметил, что коллекторы являются просто многословной реализацией свертывания. С любовью и ненавистью к складкам я должен это прокомментировать. Коллекторы в Java 8 в основном являются объектно-ориентированной инкапсуляцией самого сложного типа сгиба, найденного в Scala, а именно GenTraversableOnce.aggregate[B](z: ⇒ B)(seqop: (B, A) ⇒ B, combop: (B, B) ⇒ B): B aggregate() похож на fold() , но требует дополнительного combop для объединения двух аккумуляторов (типа B ) в один. Сравнивая это с коллекторами, параметр z исходит от supplier() , seqop() сокращения seqop() — это accumulator() а combop — это combiner() . В псевдокоде мы можем написать:

1
2
3
finisher(
    seq.aggregate(collector.supplier())
        (collector.accumulator(), collector.combiner()))

GenTraversableOnce.aggregate() используется, когда возможно параллельное сокращение — так же, как с коллекционерами.

Ссылка: Введение в написание пользовательских коллекторов на Java 8 от нашего партнера по JCG Томаша Нуркевича в блоге, посвященном Java и соседству .