Во второй статье о использовании Stream API я показывал, как использовать стандартные коллекторы. Настало время разработки собственного коллектора.
Допустим, мы ходим посчитать медианное значение длин строк в примере из второй статьи. Готового коллектора, который считает медиану, нет в стандартной библиотеке, поэтому разработаем свой.
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 | public class MedianCollector implements Collector<Integer, TreeSet<Integer>, Integer> { @Override public Supplier<TreeSet<Integer>> supplier() { return TreeSet<Integer>::new; } @Override public BiConsumer<TreeSet<Integer>, Integer> accumulator() { return TreeSet::add; } @Override public BinaryOperator<TreeSet<Integer>> combiner() { return (l, r) -> { l.addAll(r); return l; }; } @Override public Function<TreeSet<Integer>, Integer> finisher() { return s -> { long size = s.size(); if (size%2==0) { return new Double(s .stream() .skip(size % 2+2) .limit(2) .mapToInt(i->i) .average() .getAsDouble()) .intValue(); } return s .stream() .skip(size % 2+2) .findFirst() .get(); }; } @Override public Set<Characteristics> characteristics() { return EnumSet.of(Characteristics.CONCURRENT); } } |
Мы реализуем интерфейс Collector, который типизируется тремя разными типами: входной тип для коллектора ( Integer в нашем случае), тип контейнера для хранения промежуточных вычислений ( TreeSet в нашем случае) и выходной тип коллектора, который он возвращает (опять Integer).
Интерфейс Collector требует реализации пяти методов. Supplier возвращает лямбда-выражение, создающее контейнер для хранения промежуточных выражений:
1 2 3 | public Supplier<TreeSet<Integer>> supplier() { return TreeSet<Integer>::new; } |
Accumulator добавляет очередное значение в контейнер промежуточных значений. Если быть точным, то accumulator возвращает лямбда-выражение, которое обрабатывает очередное значение и сохраняет его.
1 2 3 4 | @Override public BiConsumer<TreeSet<Integer>, Integer> accumulator() { return TreeSet::add; } |
Combiner возвращает лямбда-выражение, объединяющее два контейнера промежуточных значений в один. Дело в том, что Stream API может создать несколько таки контейнеров, для параллельной обработки и в конце слить их в один общий контейнер.
1 2 3 4 | @Override public BinaryOperator<TreeSet<Integer>> combiner() { return (l, r) -> { l.addAll(r); return l; }; } |
Finisher возвращает лямбда-выражение, которое производит финальное преобразование: обрабатывает содержимого контейнера промежуточных результатов и приводит его к заданному выходному типу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @Override public Function<TreeSet<Integer>, Integer> finisher() { return s -> { long size = s.size(); if (size%2==0) { return new Double(s .stream() .skip(size % 2+2) .limit(2) .mapToInt(i->i) .average() .getAsDouble()) .intValue(); } return s .stream() .skip(size % 2+2) .findFirst() .get(); }; } |
Последний вызов служит для декларирования свойств коллектора.
Полученный коллектор может быть использован в вызове collect():
1 2 3 | public final Integer medianStringLength() { return LONG_WELCOME.stream().map(String::length).collect(new MedianCollector()); } |
1 2 3 4 | @Test public void testMedianStringLength() { assertThat(testedObject.medianStringLength(), is(4)); } |
Вместо собственной реализации интерфейса Collector можно использовать статический метод Collector.of(), принимающий те же самые лямбда выражения и вовзращающий настроенный коллектор.
Код примера доступен на github.