Введение в Spliterator в Java

Введение в Spliterator в Java

1. обзор

ИнтерфейсSpliterator, представленный в Java 8, может бытьused for traversing and partitioning sequences. Это базовая утилита дляStreams, особенно параллельных.

В этой статье мы расскажем о его использовании, характеристиках, методах и о том, как создавать собственные реализации.

2. Spliterator API

2.1. tryAdvance

Это основной метод, используемый для пошагового выполнения последовательности. Методtakes a Consumer that’s used to consume elements of the Spliterator one by one sequentially и возвращаетfalse, если нет элементов для обхода.

Здесь мы рассмотрим, как использовать его для обхода и разбиения элементов.

Во-первых, предположим, что у нас естьArrayList с 35000 статей и что классArticle определен как:

public class Article {
    private List listOfAuthors;
    private int id;
    private String name;

    // standard constructors/getters/setters
}

Теперь давайте реализуем задачу, которая обрабатывает список статей и добавляет суффикс «– published by example” к имени каждой статьи:

public String call() {
    int current = 0;
    while (spliterator.tryAdvance(a -> a.setName(article.getName()
      .concat("- published by example")))) {
        current++;
    }

    return Thread.currentThread().getName() + ":" + current;
}

Обратите внимание, что эта задача выводит количество статей, обработанных по завершении выполнения.

Другой ключевой момент - мы использовали методtryAdvance() для обработки следующего элемента.

2.2. trySplitс

Затем давайте разделимSpliterators (отсюда и название) и обработаем разделы независимо.

МетодtrySplit пытается разделить его на две части. Затем элементы вызывающего процесса и, наконец, возвращенный экземпляр будут обрабатывать остальные, что позволяет обрабатывать два объекта параллельно.

Давайте сначала сгенерируем наш список:

public static List
generateElements() { return Stream.generate(() -> new Article("Java")) .limit(35000) .collect(Collectors.toList()); }

Затем мы получаем наш экземплярSpliterator, используя методspliterator(). Затем мы применяем наш методtrySplit():

@Test
public void givenSpliterator_whenAppliedToAListOfArticle_thenSplittedInHalf() {
    Spliterator
split1 = Executor.generateElements().spliterator(); Spliterator
split2 = split1.trySplit(); assertThat(new Task(split1).call()) .containsSequence(Executor.generateElements().size() / 2 + ""); assertThat(new Task(split2).call()) .containsSequence(Executor.generateElements().size() / 2 + ""); }

The splitting process worked as intended and divided the records equally.

2.3. предполагаемый размер

МетодestimatedSize дает нам приблизительное количество элементов:

LOG.info("Size: " + split1.estimateSize());

Это выведет:

Size: 17500

2.4. hasCharacteristics

Этот API проверяет, соответствуют ли заданные характеристики свойствамSpliterator.. Затем, если мы вызовем метод выше, на выходе будетint представление этих характеристик:

LOG.info("Characteristics: " + split1.characteristics());
Characteristics: 16464

3. Spliterator Characteristicsс

Он имеет восемь различных характеристик, описывающих его поведение. Их можно использовать как подсказки для внешних инструментов:

  • SIZED, если он может возвращать точное количество элементов с помощью методаestimateSize()

  • SORTED - если выполняется итерация по отсортированному источнику

  • SUBSIZED - если мы разделим экземпляр с помощью методаtrySplit() и получим сплитераторы, которые также являютсяSIZED

  • CONCURRENT - если источник можно безопасно модифицировать одновременно

  • DISTINCT - если для каждой пары встречающихся элементовx, y, !x.equals(y)

  • IMMUTABLE - если элементы, хранящиеся в источнике, не могут быть структурно изменены

  • NONNULL - если источник содержит нули или нет

  • ORDERED - при итерации по упорядоченной последовательности

4. ПользовательскийSpliterator

4.1. Когда настраивать

Во-первых, давайте предположим следующий сценарий:

У нас есть класс статей со списком авторов и статья, у которой может быть более одного автора. Кроме того, мы рассматриваем автора, связанного со статьей, если идентификатор его связанной статьи совпадает с идентификатором статьи.

Наш классAuthor будет выглядеть так:

public class Author {
    private String name;
    private int relatedArticleId;

    // standard getters, setters & constructors
}

Далее мы реализуем класс для подсчета авторов при обходе потока авторов. Затемthe class will perform a reduction в потоке.

Давайте посмотрим на реализацию класса:

public class RelatedAuthorCounter {
    private int counter;
    private boolean isRelated;

    // standard constructors/getters

    public RelatedAuthorCounter accumulate(Author author) {
        if (author.getRelatedArticleId() == 0) {
            return isRelated ? this : new RelatedAuthorCounter( counter, true);
        } else {
            return isRelated ? new RelatedAuthorCounter(counter + 1, false) : this;
        }
    }

    public RelatedAuthorCounter combine(RelatedAuthorCounter RelatedAuthorCounter) {
        return new RelatedAuthorCounter(
          counter + RelatedAuthorCounter.counter,
          RelatedAuthorCounter.isRelated);
    }
}

Каждый метод в вышеприведенном классе выполняет определенную операцию для подсчета при обходе.

Сначалаaccumulate() method traverse the authors one by one in an iterative way, затемcombine() sums two counters using their values. Наконец,getCounter() возвращает счетчик.

Теперь, чтобы проверить, что мы сделали до сих пор. Давайте преобразуем список авторов нашей статьи в поток авторов:

Stream stream = article.getListOfAuthors().stream();

И реализуемcountAuthor() method to perform the reduction on the stream using RelatedAuthorCounter:

private int countAutors(Stream stream) {
    RelatedAuthorCounter wordCounter = stream.reduce(
      new RelatedAuthorCounter(0, true),
      RelatedAuthorCounter::accumulate,
      RelatedAuthorCounter::combine);
    return wordCounter.getCounter();
}

Если мы использовали последовательный поток, результат будет, как и ожидалось,“count = 9”, однако проблема возникает, когда мы пытаемся распараллелить операцию.

Давайте посмотрим на следующий тестовый пример:

@Test
void
  givenAStreamOfAuthors_whenProcessedInParallel_countProducesWrongOutput() {
    assertThat(Executor.countAutors(stream.parallel())).isGreaterThan(9);
}

По-видимому, что-то пошло не так - разделение потока на случайную позицию привело к тому, что автор был посчитан дважды.

4.2. Как настроить

Чтобы решить эту проблему, нам нужноimplement a Spliterator that splits authors only when related id and articleId matches. Вот реализация нашего пользовательскогоSpliterator:

public class RelatedAuthorSpliterator implements Spliterator {
    private final List list;
    AtomicInteger current = new AtomicInteger();
    // standard constructor/getters

    @Override
    public boolean tryAdvance(Consumer action) {
        action.accept(list.get(current.getAndIncrement()));
        return current.get() < list.size();
    }

    @Override
    public Spliterator trySplit() {
        int currentSize = list.size() - current.get();
        if (currentSize < 10) {
            return null;
        }
        for (int splitPos = currentSize / 2 + current.intValue();
          splitPos < list.size(); splitPos++) {
            if (list.get(splitPos).getRelatedArticleId() == 0) {
                Spliterator spliterator
                  = new RelatedAuthorSpliterator(
                  list.subList(current.get(), splitPos));
                current.set(splitPos);
                return spliterator;
            }
        }
        return null;
   }

   @Override
   public long estimateSize() {
       return list.size() - current.get();
   }

   @Override
   public int characteristics() {
       return CONCURRENT;
   }
}

Теперь применение методаcountAuthors() даст правильный результат. Следующий код демонстрирует это:

@Test
public void
  givenAStreamOfAuthors_whenProcessedInParallel_countProducesRightOutput() {
    Stream stream2 = StreamSupport.stream(spliterator, true);

    assertThat(Executor.countAutors(stream2.parallel())).isEqualTo(9);
}

Кроме того, пользовательскийSpliterator создается из списка авторов и проходит по нему, удерживая текущую позицию.

Давайте обсудим более подробно реализацию каждого метода:

  • *tryAdvance* – передает авторов вConsumer в текущей позиции индекса и увеличивает его позицию

  • *trySplit* – определяет механизм разделения, в нашем случаеRelatedAuthorSpliterator создается при совпадении идентификаторов, а разделение делит список на две части

  • estimatedSize - разница между размером списка и позицией автора, повторяющегося в данный момент.

  • characteristics - возвращает характеристикиSpliterator, в нашем случаеSIZED, поскольку значение, возвращаемое методомestimatedSize(), является точным; кроме того,CONCURRENT указывает, что источник этогоSpliterator может быть безопасно изменен другими потоками

5. Поддержка примитивных ценностей

Spliterator API supports primitive values including double, int and long.

Единственная разница между использованием общего и примитивного выделенногоSpliterator - это заданныйConsumer и типSpliterator.

Например, когда нам это нужно для значенияint, нам нужно передатьintConsumer. Кроме того, вот список примитивных выделенныхSpliterators:

  • OfPrimitive<T, T_CONS, T_SPLITR extends Spliterator.OfPrimitive<T, T_CONS, T_SPLITR>>: родительский интерфейс для других примитивов

  • OfInt: ASpliterator, специализированный дляint

  • OfDouble: ASpliterator выделен дляdouble

  • OfLong: ASpliterator выделен дляlong

6. Заключение

В этой статье мы рассмотрели Java 8Spliterator usage, methods, characteristics, splitting process, primitive support and how to customize it.

Как всегда, полную реализацию этой статьи можно найти вover on Github.