Статьи

Guava ImmutableCollections, Multimaps и Java 8 Collectors

В этом посте мы собираемся обсудить создание пользовательских экземпляров 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();
}
  1. Функция поставщика возвращает новый экземпляр изменяемого аккумулятора типа А.
  2. Функция аккумулятора берет экматор и экземпляр типа T, который приведет к добавлению T в аккумулятор.
  3. Функция объединения берет два экземпляра аккумулятора и объединяет их в один. Слияние может быть новым экземпляром или результатом объединения одного из аккумуляторов в другой.
  4. Функция финишера выполняет последнюю операцию с аккумулятором (возможно, слитыми аккумуляторами) в конечном типе R. Конечный тип также может быть того же типа, что и аккумулятор.
  5. Функция характеристик возвращает набор 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интерфейса, и он смог приступить к созданию пользовательских сборщиков.