1. Обзор
Интерфейс Spliterator
, представленный в Java 8, можно использовать для обхода и разделения последовательностей . Это базовая утилита для потоков
, особенно параллельных.
В этой статье мы рассмотрим его использование, характеристики, методы и способы создания собственных пользовательских реализаций.
2. API -разделитель
2.1. попробуй вперед
Это основной метод, используемый для пошагового прохождения последовательности. Метод принимает Consumer
, который используется для последовательного использования элементов Spliterator
, и возвращает false
, если нет элементов для обхода.
Здесь мы рассмотрим, как использовать его для обхода и разделения элементов.
Во-первых, давайте предположим, что у нас есть список ArrayList
с 35 000 статей и класс Article
определен как:
public class Article {
private List<Author> listOfAuthors;
private int id;
private String name;
// standard constructors/getters/setters
}
Теперь давайте реализуем задачу, которая обрабатывает список статей и добавляет к названию каждой статьи суффикс « -published by ForEach» :
public String call() {
int current = 0;
while (spliterator.tryAdvance(a -> a.setName(article.getName()
.concat("- published by ForEach")))) {
current++;
}
return Thread.currentThread().getName() + ":" + current;
}
Обратите внимание, что эта задача выводит количество обработанных статей после завершения выполнения.
Еще одним ключевым моментом является то, что мы использовали метод tryAdvance()
для обработки следующего элемента.
2.2. попробоватьСплит
Далее давайте разобьем сплитеры
(отсюда и название) и обработаем разделы независимо друг от друга.
Метод 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. имеетХарактеристики
Этот API проверяет, соответствуют ли заданные характеристики свойствам Spliterator.
Затем, если мы вызовем описанный выше метод, результатом будет представление этих характеристик в виде int :
LOG.info("Characteristics: " + split1.characteristics());
Characteristics: 16464
3. Характеристики сплиттеров
Он имеет восемь различных характеристик, описывающих его поведение. Их можно использовать как подсказки для внешних инструментов:
SIZED
—
если он может возвращать точное количество элементов с помощью методаAssessmentSize() .
SORTED
— если он перебирает отсортированный источникSUBSIZED
— если мы разделим экземпляр с помощьюметода trySplit()
и получим Spliterators, которые такжеимеют SIZED
CONCURRENT
— если исходный код может быть безопасно изменен одновременноDISTINCT
— если для каждой пары встречающихся элементовx, y, !x.equals(y)
IMMUTABLE
— если элементы, хранящиеся в источнике, не могут быть структурно измененыNONNULL
— если источник содержит нули или нетORDERED
- при повторении упорядоченной последовательности
4. Пользовательский сплитератор
4.1. Когда настраивать
Во-первых, давайте предположим следующий сценарий:
У нас есть класс статьи со списком авторов и статья, у которой может быть более одного автора. Кроме того, мы считаем автора связанным со статьей, если идентификатор его связанной статьи совпадает с идентификатором статьи.
Наш класс Author
будет выглядеть следующим образом:
public class Author {
private String name;
private int relatedArticleId;
// standard getters, setters & constructors
}
Далее мы реализуем класс для подсчета авторов при обходе потока авторов. Затем класс выполнит сокращение потока.
Давайте посмотрим на реализацию класса:
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);
}
}
Каждый метод в приведенном выше классе выполняет определенную операцию для подсчета при обходе.
Во-первых, метод calculate()
итеративно проходит по авторам один за другим , а затем comb()
суммирует два счетчика, используя их значения . Наконец, getCounter()
возвращает счетчик.
Теперь, чтобы проверить, что мы сделали до сих пор. Преобразуем список авторов нашей статьи в поток авторов:
Stream<Author> stream = article.getListOfAuthors().stream();
И реализуйте метод countAuthor()
для выполнения сокращения потока с помощью RelatedAuthorCounter
:
private int countAutors(Stream<Author> 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. Как настроить
Чтобы решить эту проблему, нам нужно реализовать Spliterator
, который разделяет авторов только тогда, когда соответствующие идентификаторы
и articleId
совпадают . Вот реализация нашего пользовательского Spliterator
:
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;
}
}
Теперь применение метода countAuthors()
даст правильный результат. Следующий код демонстрирует, что:
@Test
public void
givenAStreamOfAuthors_whenProcessedInParallel_countProducesRightOutput() {
Stream<Author> stream2 = StreamSupport.stream(spliterator, true);
assertThat(Executor.countAutors(stream2.parallel())).isEqualTo(9);
}
Кроме того, пользовательский Spliterator
создается из списка авторов и проходит через него, удерживая текущую позицию.
Остановимся подробнее на реализации каждого метода:
tryAdvance —
передает авторовПотребителю
в текущей позиции индекса и увеличивает свою позициюtrySplit —
определяет механизм разделения, в нашем случаеRelatedAuthorSpliterator
создается при совпадении id, и разделение делит список на две частипредполагаемый
размер — разница между размером списка и позицией текущего итерируемого авторахарактеристики
— возвращаетхарактеристики Spliterator
, в нашем случаеSIZED
, так как значение, возвращаемое методомоцениваемого размера()
, является точным; кроме того,CONCURRENT
указывает, что источник этогоSpliterator
может быть безопасно изменен другими потоками.
5. Поддержка примитивных значений
API Spliterator поддерживает примитивные значения
,
включая double
, int
и long
.
Единственная разница между использованием универсального и примитивного выделенного Spliterator
заключается в заданном Потребителе
и типе Spliterator
.
Например, когда нам это нужно для значения int
, нам нужно передать intConsumer
. Кроме того, вот список примитивных выделенных разветвителей
:
OfPrimitive<T, T_CONS, T_SPLITR расширяет Spliterator.OfPrimitive<T, T_CONS, T_SPLITR>>
: родительский интерфейс для других примитивовOfInt
:сплитератор,
специализирующийся наint .
OfDouble
:сплитератор,
предназначенный длядвойного
OfLong
:сплитератор,
предназначенный длядлинных
6. Заключение
В этой статье мы рассмотрели использование Java 8 Spliterator
, методы, характеристики, процесс разделения, поддержку примитивов и способы его настройки.
Как всегда, полную реализацию этой статьи можно найти на Github .