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, являются их собственными. |