«1. Обзор

Интерфейс Spliterator, представленный в Java 8, можно использовать для обхода и разделения последовательностей. Это базовая утилита для потоков, особенно параллельных.

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

2. API разделителя

2.1. tryAdvance

Это основной метод, используемый для пошагового выполнения последовательности. Метод принимает Consumer, который используется для последовательного использования элементов Spliterator, и возвращает false, если нет элементов, которые нужно пройти.

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

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

public class Article {
    private List<Author> listOfAuthors;
    private int id;
    private String name;
    
    // standard constructors/getters/setters
}

Теперь давайте реализуем задачу, которая обрабатывает список статей и добавляет суффикс †«— опубликовано Baeldung» для каждого имени статьи:

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

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

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

2.2. trySplit

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

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

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

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

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

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

Процесс разделения работал, как и предполагалось, и разделил записи поровну.

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

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

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

Это выведет:

Size: 17500

2.4. hasCharacteristics

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

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

3. Характеристики разделителя

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

SIZED — если он способен возвращать точное количество элементов с помощью метода AssessmentSize(). SORTED — если он повторяет отсортированный источник. SUBSIZED — если мы разбить экземпляр с помощью метода trySplit() и получить разделители, которые также имеют SIZED, а также CONCURRENT — если исходный код может быть безопасно изменен одновременно DISTINCT — если для каждой пары встречающихся элементов x, y, !x.equals(y) IMMUTABLE — если элементы, хранящиеся в источнике, не могут быть структурно изменены. NONNULL — если источник содержит нули или не ORDERED — если повторяется упорядоченная последовательность

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

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

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

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

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

    // standard getters, setters & constructors
}

Наш класс Author будет выглядеть следующим образом:

Далее мы реализуем класс для подсчета авторов при обходе потока авторов. Затем класс выполнит сокращение потока.

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);
    }
}

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

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

Во-первых, метод collect() итеративно перебирает авторов одного за другим, а затем comb() суммирует два счетчика, используя их значения. Наконец, getCounter() возвращает счетчик.

Stream<Author> stream = article.getListOfAuthors().stream();

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

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

И реализуем метод countAuthor() для выполнения сокращения потока с использованием RelatedAuthorCounter:

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

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

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

«

«Видимо, что-то пошло не так — разбиение потока в случайном месте привело к двойному учету автора.

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

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

    @Override
    public Spliterator<Author> 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<Author> 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;
   }
}

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

@Test
public void
  givenAStreamOfAuthors_whenProcessedInParallel_countProducesRightOutput() {
    Stream<Author> stream2 = StreamSupport.stream(spliterator, true);
 
    assertThat(Executor.countAutors(stream2.parallel())).isEqualTo(9);
}

Чтобы решить эту проблему, нам нужно реализовать Spliterator, который разделяет авторов только тогда, когда соответствующие идентификаторы и articleId совпадают. Вот реализация нашего пользовательского Spliterator:

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

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

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

tryAdvance — передает авторов Потребителю в текущей позиции индекса и увеличивает свою позицию trySplit — определяет механизм разделения, в нашем случае , RelatedAuthorSpliterator создается при совпадении идентификаторов, а разбиение делит список на две части. предполагаемый размер — разница между размером списка и позицией итерируемых в данный момент характеристик автора — возвращает характеристики Spliterator, в нашем случае SIZED как значение, возвращаемое методом оцениваемого размера(), является точным; кроме того, CONCURRENT указывает, что источник этого Spliterator может быть безопасно изменен другими потоками

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

API Spliterator поддерживает примитивные значения, включая double, int и long.

    Единственная разница между использованием универсального и примитивного выделенного Spliterator заключается в данном Потребителе и типе Spliterator.

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

OfPrimitive\u003cT, T_CONS, T_SPLITR extends Spliterator.OfPrimitive\u003cT, T_CONS, T_SPLITR\u003e\u003e: родительский интерфейс для других примитивов OfInt: Spliterator, специализированный для int OfDouble: выделенный Spliterator for double OfLong: Spliterator, предназначенный для long

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