В этом посте мы собираемся обсудить создание пользовательских экземпляров Collector . Collector
Интерфейс был введен в java.util.stream пакете , когда Java 8 был освобожден. A Collector
используется для «сбора» результатов потоковых операций. Результаты собираются из потока при Stream.collect
вызове метода работы терминала . Хотя доступны реализации по умолчанию, иногда мы хотим использовать какой-то пользовательский контейнер. Наша цель сегодня будет создавать Collector
экземпляры , которые производят гуавы ImmutableCollections и Multimaps .
Справочная информация для коллекционера
Collector
Интерфейс описывается очень хорошо в документации , поэтому здесь мы просто дать обзор кратко. Collector
Интерфейс определяет параметры типа 3 и 4 методы:
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A,T> accumulator();
BinaryOperator<T> combiner();
Function<A,R> finisher();
Set<Collector.Characteristics> characteristics();
}
- Функция поставщика возвращает новый экземпляр изменяемого аккумулятора типа А.
- Функция аккумулятора берет экматор и экземпляр типа T, который приведет к добавлению T в аккумулятор.
- Функция объединения берет два экземпляра аккумулятора и объединяет их в один. Слияние может быть новым экземпляром или результатом объединения одного из аккумуляторов в другой.
- Функция финишера выполняет последнюю операцию с аккумулятором (возможно, слитыми аккумуляторами) в конечном типе R. Конечный тип также может быть того же типа, что и аккумулятор.
- Функция характеристик возвращает набор Collector.Characteristics, который содержит свойства для коллектора. Характерные значения
CONCURRENT
,UNOREDRED
иIDENTITY_FINISH
.
Дополнительно есть 2 статических метода Collector.of
. В Collector.of
методах возвращают новый коллектор на основе предоставленного поставщиком, аккумулятор, combinbiner и (optionall) финишер определений. Поскольку мы здесь для создания пользовательских Collector
экземпляров, мы не будем рассматривать эту функциональность. Для первого примера мы будем использовать Guava ImmutableList в качестве сборщика.
Пример сборщика Guava ImmutableList
Наш первый шаг — создать абстрактный класс, который определяет операции накопителя, сумматора и финишера. Мы используем абстрактный класс, потому что Guava предлагает несколько разных неизменяемых коллекций, поэтому нам нужно будет предоставить разных поставщиков для каждой из них.
private static abstract class ImmutableCollector<T, A extends ImmutableCollection.Builder, R extends ImmutableCollection<T>> implements Collector {
@Override
public BiConsumer<A, T> accumulator() {
return (c, v) -> c.add(v);
}
@Override
public BinaryOperator<A> combiner() {
return (c1, c2) -> (A) c1.addAll(c2.build().iterator());
}
@Override
public Function<A, R> finisher() {
return (bl -> (R) bl.build());
}
@Override
public Set<Characteristics> characteristics() {
return Sets.newHashSet(Characteristics.CONCURRENT);
}
}
Теперь, когда мы определили нашу функциональность коллектора, мы предоставим различные реализации нашего базового класса, чтобы обеспечить поставщиков для различных типов неизменяемых сборщиков:
private static class ImmutableSetCollector<T> extends ImmutableCollector {
@Override
public Supplier<ImmutableSet.Builder<T>> supplier() {
return ImmutableSet::builder;
}
@Override
public Set<Characteristics> characteristics() {
return Sets.newHashSet(Characteristics.CONCURRENT, Characteristics.UNORDERED);
}
}
private static class ImmutableSortedSetCollector<T extends Comparable<?>> extends ImmutableCollector {
@Override
public Supplier<ImmutableSortedSet.Builder<T>> supplier() {
return ImmutableSortedSet::naturalOrder;
}
}
private static class ImmutableListCollector<T> extends ImmutableCollector {
@Override
public Supplier<ImmutableList.Builder<T>> supplier() {
return ImmutableList::builder;
}
}
Здесь вы заметите, что в ImmutableSetCollector
реализации мы переопределили характеристики нашего ImmutableSet
коллектора, чтобы пометить его как параллельный (функция аккумулятора может вызываться из нескольких потоков) и неупорядоченный (элементы не будут поддерживаться в порядке вставки). Наконец , мы обернуть все это вверх по ImmutableCollectors класса и provdide статические методы легко создавать наши коллекционеры:
public class ImmutableCollectors {
public static <E> ImmutableListCollector<E> ofList() {
return new ImmutableListCollector<>();
}
public static <E> ImmutableSetCollector<E> ofSet() {
return new ImmutableSetCollector<>();
}
public static <E extends Comparable<?>> ImmutableSortedSetCollector<E> ofSortedSet(){
return new ImmutableSortedSetCollector<>();
}
//Other functionality previously shown left out here for clarity
}
Вот пример использования ImmutableListCollector
в модульном тесте:
@Test
public void testCollectImmutableList(){
List<String> things = Lists.newArrayList("Apple", "Ajax", "Anna", "banana", "cat", "foo", "dog", "cat");
List<String> aWords = things.stream().filter(w -> w.startsWith("A")).collect(ImmutableCollectors.ofList());
assertThat(aWords.contains("Apple"),is(true));
assertThat(aWords instanceof ImmutableList,is(true));
assertThat(aWords.size(),is(3));
boolean unableToModifyList = false;
try{
aWords.add("Bad Move");
}catch (UnsupportedOperationException e){
unableToModifyList = true;
}
assertTrue("Should not be able to modify list",unableToModifyList);
}
Guava Multimaps как коллекционеры
После работы с коллекциями Guava я обнаружил, что Multimap — очень удобная абстракция для разделения данных, когда у вас есть несколько значений для данного ключа. Так что было бы неплохо, если бы сборщик разделил результаты наших Stream
операций на Guava Multimaps
. Мы собираемся следовать тому же шаблону, который мы использовали с ImmutableCollectors
классом. Существует абстрактный базовый класс, обеспечивающий функции накопителя, объединения и завершения. Затем различные реализации этого абстрактного класса, чтобы обеспечить поставщика для различных вариантов Guava Mulitmaps
. Имея это в виду, давайте посмотрим на некоторые примеры кода:
private static abstract class MultimapCollector<K,T, A extends Multimap<K,T>, R extends Multimap<K,T>> implements Collector {
private Function<T, K> keyFunction;
public MultimapCollector(Function<T, K> keyFunction) {
Preconditions.checkNotNull(keyFunction,"The keyFunction can't be null");
this.keyFunction = keyFunction;
}
@Override
public BiConsumer<A, T> accumulator() {
return (map, value) -> map.put(keyFunction.apply(value),value);
}
@Override
public BinaryOperator<A> combiner() {
return (c1, c2) -> {
c1.putAll(c2);
return c1;
};
}
@Override
@SuppressWarnings("unchecked")
public Function<A, R> finisher() {
return mmap -> (R) mmap;
}
}
Здесь есть одно небольшое отличие. Нам нужно предоставить функцию для определения ключа, который будет использоваться при разбиении данных. Точно названный, keyFunction
предоставленный во время создания объекта, удовлетворяет эту потребность. Вот реализации базового класса, обеспечивающие разных поставщиков:
private static class ListMulitmapCollector<K,T> extends MultimapCollector {
private ListMulitmapCollector(Function<T, K> keyFunction) {
super(keyFunction);
}
@Override
public Supplier<ArrayListMultimap<K,T>> supplier() {
return ArrayListMultimap::create;
}
}
private static class HashSetMulitmapCollector<K,T> extends MultimapCollector {
private HashSetMulitmapCollector(Function<T, K> keyFunction) {
super(keyFunction);
}
@Override
public Supplier<HashMultimap<K,T>> supplier() {
return HashMultimap::create;
}
}
private static class LinkedListMulitmapCollector<K,T> extends MultimapCollector {
private LinkedListMulitmapCollector(Function<T, K> keyFunction) {
super(keyFunction);
}
@Override
public Supplier<LinkedHashMultimap<K,T>> supplier() {
return LinkedHashMultimap::create;
}
}
Опять же, мы создадим этот код в классе контейнера ( MultimapCollectors
) и предоставим статические методы для создания различных сборщиков:
public class MultiMapCollectors {
public static <K,T> ListMulitmapCollector<K,T> listMultimap(Function<T, K> keyFunction) {
return new ListMulitmapCollector<>(keyFunction);
}
public static <K,T> HashSetMulitmapCollector<K,T> setMulitmap(Function<T, K> keyFunction) {
return new HashSetMulitmapCollector<>(keyFunction);
}
public static <K,T> LinkedListMulitmapCollector<K,T> linkedListMulitmap(Function<T,K> keyFunction) {
return new LinkedListMulitmapCollector<>(keyFunction);
}
Наконец, вот пример Multimap
коллектора (HashSetMultimap) в действии:
public class MultiMapCollectorsTest {
private List<TestObject> testObjectList;
@Before
public void setUp() {
TestObject w1 = new TestObject("one", "stuff");
TestObject w2 = new TestObject("one", "foo");
TestObject w3 = new TestObject("two", "bar");
TestObject w4 = new TestObject("two", "bar");
testObjectList = Arrays.asList(w1, w2, w3, w4);
}
@Test
public void testSetMultimap() throws Exception {
HashMultimap<String, TestObject> testMap = testObjectList.stream().collect(MultiMapCollectors.setMulitmap((TestObject w) -> w.id));
assertThat(testMap.size(),is(3));
assertThat(testMap.get("one").size(),is(2));
assertThat(testMap.get("two").size(),is(1));
}
//other details left out for clarity
Вывод
Это завершает наше краткое введение в создание пользовательских Collector
экземпляров в Java 8. Надеемся, что мы продемонстрировали полезность Collector
интерфейса, и он смог приступить к созданию пользовательских сборщиков.