Accumulative
— это интерфейс, предложенный для промежуточного типа накопления A
Collector<T, A, R>
, чтобы упростить определение пользовательских Java Collector
.
Вступление
Если вы когда-либо использовали Java Stream
s, вы, скорее всего, использовали некоторые Collector
s, например:
Но вы когда-нибудь использовали …
- Составленный
Collector
?- В качестве параметра он принимает другой
Collector
, например:Collectors.collectingAndThen
.
- В качестве параметра он принимает другой
- Собственный
Collector
?- Его функции указаны явно в
Collector.of
.
- Его функции указаны явно в
Этот пост о кастомных Collector
.
Коллектор
Напомним суть Collector
договора (комментарии мои):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
/** * @param <T> (input) element type * @param <A> (intermediate) mutable accumulation type (container) * @param <R> (output) result type */ public interface Collector<T, A, R> { Supplier<A> supplier(); // create a container BiConsumer<A, T> accumulator(); // add to the container BinaryOperator<A> combiner(); // combine two containers Function<A, R> finisher(); // get the final result from the container Set<Characteristics> characteristics(); // irrelevant here } |
Вышеуказанный договор носит функциональный характер, и это очень хорошо! Это позволяет нам создавать Collector
с использованием произвольных типов накопления ( A
), например:
-
A
:StringBuilder
(Collectors.joining
) -
A
:OptionalBox
(Collectors.reducing
) -
A
:long[]
(Collectors.averagingLong
)
Предложение
Прежде чем дать какое-либо обоснование, я представлю предложение, потому что оно краткое. Полный исходный код этого предложения доступен в виде GitHub .
Накопительный интерфейс
Я предлагаю добавить следующий интерфейс под названием « Accumulative
(имя, которое будет обсуждаться) в JDK:
1
2
3
4
5
6
7
8
|
public interface Accumulative<T, A extends Accumulative<T, A, R>, R> { void accumulate(T t); // target for Collector.accumulator() A combine(A other); // target for Collector.combiner() R finish(); // target for Collector.finisher() } |
Этот интерфейс, в отличие от Collector
, является объектно-ориентированным по своей природе, и классы, реализующие его, должны представлять некоторое изменяемое состояние .
Collector.of перегрузки
Имея Accumulative
, мы можем добавить следующую перегрузку Collector.of
:
1
2
3
4
|
public static <T, A extends Accumulative<T, A, R>, R> Collector<T, ?, R> of( Supplier<A> supplier, Collector.Characteristics... characteristics) { return Collector.of(supplier, A::accumulate, A::combine, A::finish, characteristics); } |
Average-Developer Story
В этом разделе я покажу, как предложение может повлиять на среднего разработчика , который знает только основы API Collector . Если вы хорошо знаете этот API, пожалуйста, сделайте все возможное, чтобы представить, что вы не знаете, прежде чем читать …
пример
Давайте снова воспользуемся примером из моего последнего поста (еще более упрощенного). Предположим, что у нас есть Stream
:
1
2
3
4
|
interface IssueWiseText { int issueLength(); int textLength(); } |
и что нам нужно рассчитать охват вопроса :
общая длина вопроса
─────────────
общая длина текста
Это требование переводится в следующую подпись:
1
|
Collector<IssueWiseText, ?, Double> toIssueCoverage(); |
Решение
Средний разработчик может решить использовать пользовательский тип накопления A
для решения этой проблемы (хотя возможны и другие решения ). Допустим, разработчик называет его CoverageContainer
чтобы:
-
T
:IssueWiseText
-
A
:CoverageContainer
-
R
:Double
Ниже я покажу, как такой разработчик может прийти к структуре CoverageContainer
.
Структура без накопительного
Примечание . В этом разделе приводится длинная иллюстрация того, насколько сложной может быть процедура для разработчика, неопытного в Collector
s. Вы можете пропустить это, если вы уже поняли это
Без Accumulative
, разработчик взглянет на Collector.of
и увидит четыре основных параметра:
-
Supplier<A> supplier
-
BiConsumer<A, T> accumulator
-
BinaryOperator<A> combiner
-
Function<A, R> finisher
Чтобы справиться с Supplier <A> supplier
, разработчик должен:
- мысленно замените
A
вSupplier<A>
чтобы получитьSupplier<CoverageContainer>
- мысленно разрешить подпись в
CoverageContainer get ()
- вспомните JavaDoc для
Collector.supplier()
- вызвать ссылку на метод 4-го типа ( ссылка на конструктор )
- понять, что
supplier = CoverageContainer::new
Для работы с BiConsumer <A, T> accumulator
разработчик должен:
-
BiConsumer<CoverageContainer, IssueWiseText>
-
void accept (CoverageContainer a, IssueWiseText t)
- мысленно преобразовать подпись в экземпляр-метод один
void accumulate(IssueWiseText t)
- ссылка на метод вызова 3-го типа ( ссылка на метод экземпляра произвольного объекта определенного типа )
- понять, что
accumulator = CoverageContainer::accumulate
Для работы с BinaryOperator <A> combiner
:
-
BinaryOperator<CoverageContainer>
-
CoverageContainer apply (CoverageContainer a, CoverageContainer b)
-
CoverageContainer combine(CoverageContainer other)
-
combiner = CoverageContainer::combine
Для обработки Function <A, R> finisher
:
-
Function<CoverageContainer, Double>
-
Double apply (CoverageContainer a)
-
double issueCoverage()
-
finisher = CoverageContainer::issueCoverage
Эта длинная процедура приводит к:
1
2
3
4
5
6
7
|
class CoverageContainer { void accumulate(IssueWiseText t) { } CoverageContainer combine(CoverageContainer other) { } double issueCoverage() { } } |
И разработчик может определить toIssueCoverage()
(предоставляя аргументы в правильном порядке):
1
2
3
4
5
6
|
Collector<IssueWiseText, ?, Double> toIssueCoverage() { return Collector.of( CoverageContainer:: new , CoverageContainer::accumulate, CoverageContainer::combine, CoverageContainer::finish ); } |
Структура с накопительным
Теперь, с помощью Accumulative
, разработчик посмотрит на новую перегрузку Collector.of
и увидит только один основной параметр:
-
Supplier<A> supplier
и один параметр ограниченного типа :
-
A extends Accumulative<T, A, R>
Таким образом, разработчик начнёт с естественной вещи — реализации Accumulative<T, A, R>
и разрешения T
, A
, R
в первый и последний раз:
1
2
3
|
class CoverageContainer implements Accumulative<IssueWiseText, CoverageContainer, Double> { } |
На этом этапе достойная IDE будет жаловаться, что класс должен реализовывать все абстрактные методы. Более того — и это самая красивая часть — он предложит быстрое решение. В IntelliJ вы нажимаете «Alt + Enter» → «Реализация методов», и… все готово!
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class CoverageContainer implements Accumulative<IssueWiseText, CoverageContainer, Double> { @Override public void accumulate(IssueWiseText issueWiseText) { } @Override public CoverageContainer combine(CoverageContainer other) { return null ; } @Override public Double finish() { return null ; } } |
Так что … вам не нужно манипулировать типами, писать что-либо вручную и ничего не называть!
Ах, да — вам все еще нужно определить toIssueCoverage()
, но теперь это просто:
1
2
3
|
Collector<IssueWiseText, ?, Double> toIssueCoverage() { return Collector.of(CoverageContainer:: new ); } |
Разве это не мило ?
Реализация
Реализация здесь не актуальна, так как она практически одинакова для обоих случаев ( diff ).
обоснование
Слишком сложная процедура
Надеюсь, я продемонстрировал, как определить собственный Collector
может быть непросто. Я должен сказать, что даже я всегда чувствую себя неохотно в определении одного. Тем не менее, я также чувствую, что — с Accumulative
— это нежелание исчезнет, потому что процедура будет сокращена до двух этапов:
- Реализация
Accumulative<T, A, R>
- Вызовите
Collector.of(YourContainer::new)
Привод к реализации
JetBrains придумал « стремление к развитию », и я хотел бы превратить его в «стремление к реализации».
Поскольку Collector
— это просто набор функций, обычно нет смысла (насколько я могу судить) реализовать его (есть исключения ). Тем не менее, поиск в Google по запросу «Implements Collector» показывает (~ 5000 результатов), что люди делают это.
И это естественно, потому что для создания «пользовательского» TYPE
в Java обычно расширяют / реализуют TYPE
. На самом деле, это настолько естественно, что даже опытные разработчики (например, Томаш Нуркевич , чемпион по Java) могут это сделать.
Подводя итог, люди чувствуют стремление к реализации , но — в этом случае — JDK не дает им ничего для реализации. И Accumulative
может заполнить этот пробел …
Соответствующие примеры
Наконец, я искал примеры, где было бы просто реализовать Accumulative
.
В OpenJDK (который не является целевым местом, хотя), я нашел два:
-
Collectors.reducing
( diff ) -
Collectors.teeing
( diff )
На переполнении стека, однако, я нашел много: 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 30 , 31 , 32 , 33 , 34 , 35 , 36 , 37 , 38 , 39 , 40 , 41 , 42 , 43 , 44 , 45 , 46 , 47 , 48 , 49 , 50 , 51 , 52 , 53 .
Я также нашел несколько основанных на массиве примеров, которые могут быть реорганизованы в Accumulative
для лучшей читаемости: a , b , c .
Именование
Accumulative
не лучшее имя, главным образом потому, что это прилагательное . Тем не менее, я выбрал его, потому что:
- Я хотел, чтобы имя начиналось с
A
(как в<T, A, R>
), - мой лучший кандидат (
Accumulator
) уже былBiConsumer<A, T> accumulator()
, -
AccumulativeContainer
казался слишком длинным.
В OpenJDK A
называется:
который предлагает следующие альтернативы:
-
AccumulatingBox
-
AccumulationState
-
Collector.Container
-
MutableResultContainer
Конечно, если бы идея была принята, название прошло бы через «традиционное» название
Резюме
В этой статье я предложил добавить Accumulative
интерфейс и новую перегрузку Collector.of
в JDK. С ними создание собственного Collector
больше не будет связано с большими усилиями разработчиков. Вместо этого он просто стал «выполнять контракт» и «ссылаться на конструктор».
Другими словами, это предложение направлено на снижение планки вхождения в мир Collector
!
аппендикс
Дополнительное чтение ниже.
Пример решения: JDK 12+
В JDK 12+ мы сможем определить toIssueCoverage()
как toIssueCoverage()
Collector
, благодаря Collectors.teeing
( JDK-8209685 ):
1
2
3
4
5
6
7
|
static Collector<IssueWiseText, ?, Double> toIssueCoverage() { return Collectors.teeing( Collectors.summingInt(IssueWiseText::issueLength), Collectors.summingInt(IssueWiseText::textLength), (totalIssueLength, totalTextLength) -> (double) totalIssueLength / totalTextLength ); } |
Вышесказанное является кратким, но для новичка Collector API может быть довольно сложно следовать.
Пример решения: JDK Way
В качестве альтернативы toIssueCoverage()
может быть определено как:
1
2
3
4
5
6
7
8
|
static Collector<IssueWiseText, ?, Double> toIssueCoverage() { return Collector.of( () -> new int[2], (a, t) -> { a[0] += t.issueLength(); a[1] += t.textLength(); }, (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; }, a -> (double) a[0] / a[1] ); } |
Я назвал это «JDK way», потому что некоторые Collector
реализованы в OpenJDK (например, Collector.averagingInt
).
Тем не менее, хотя такой краткий код может быть подходящим для OpenJDK, он определенно не подходит для бизнес-логики из-за уровня читабельности (который низок до уровня, который я называю загадочным ).
Опубликовано на Java Code Geeks с разрешения Томаша Линковски, партнера нашей программы JCG. См. Оригинальную статью здесь: Накопительный: пользовательские коллекторы Java Made Easy Мнения, высказанные участниками Java Code Geeks, являются их собственными. |