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 и соседству . |