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