Во второй статье о использовании 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.