Статьи

Накопительный: пользовательские коллекторы Java стали проще

Accumulative — это интерфейс, предложенный для промежуточного типа накопления A Collector<T, A, R> , чтобы упростить определение пользовательских Java Collector .

Вступление

Если вы когда-либо использовали Java Stream s, вы, скорее всего, использовали некоторые Collector s, например:

Но вы когда-нибудь использовали …

  1. Составленный Collector ?
  2. Собственный 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 ), например:

Предложение

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

  1. Supplier<A> supplier
  2. BiConsumer<A, T> accumulator
  3. BinaryOperator<A> combiner
  4. Function<A, R> finisher

Чтобы справиться с Supplier <A> supplier , разработчик должен:

  1. мысленно замените A в Supplier<A> чтобы получить Supplier<CoverageContainer>
  2. мысленно разрешить подпись в CoverageContainer get ()
  3. вспомните JavaDoc для Collector.supplier()
  4. вызвать ссылку на метод 4-го типа ( ссылка на конструктор )
  5. понять, что supplier = CoverageContainer::new

Для работы с BiConsumer <A, T> accumulator разработчик должен:

  1. BiConsumer<CoverageContainer, IssueWiseText>
  2. void accept (CoverageContainer a, IssueWiseText t)
  3. мысленно преобразовать подпись в экземпляр-метод один
    void accumulate(IssueWiseText t)
  4. ссылка на метод вызова 3-го типа ( ссылка на метод экземпляра произвольного объекта определенного типа )
  5. понять, что accumulator = CoverageContainer::accumulate

Для работы с BinaryOperator <A> combiner :

  1. BinaryOperator<CoverageContainer>
  2. CoverageContainer apply (CoverageContainer a, CoverageContainer b)
  3. CoverageContainer combine(CoverageContainer other)
  4. combiner = CoverageContainer::combine

Для обработки Function <A, R> finisher :

  1. Function<CoverageContainer, Double>
  2. Double apply (CoverageContainer a)
  3. double issueCoverage()
  4. 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 и увидит только один основной параметр:

  1. 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 — это нежелание исчезнет, ​​потому что процедура будет сокращена до двух этапов:

  1. Реализация Accumulative<T, A, R>
  2. Вызовите Collector.of(YourContainer::new)

Привод к реализации

JetBrains придумал « стремление к развитию », и я хотел бы превратить его в «стремление к реализации».

Поскольку Collector — это просто набор функций, обычно нет смысла (насколько я могу судить) реализовать его (есть исключения ). Тем не менее, поиск в Google по запросу «Implements Collector» показывает (~ 5000 результатов), что люди делают это.

И это естественно, потому что для создания «пользовательского» TYPE в Java обычно расширяют / реализуют TYPE . На самом деле, это настолько естественно, что даже опытные разработчики (например, Томаш Нуркевич , чемпион по Java) могут это сделать.

Подводя итог, люди чувствуют стремление к реализации , но — в этом случае — JDK не дает им ничего для реализации. И Accumulative может заполнить этот пробел …

Соответствующие примеры

Наконец, я искал примеры, где было бы просто реализовать Accumulative .

В OpenJDK (который не является целевым местом, хотя), я нашел два:

  1. Collectors.reducing ( diff )
  2. 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, являются их собственными.