Stream.spliterator: примеры (JAVA)
Stream.spliterator: SpliteratorОписание и сигнатура
Метод Stream.spliterator возвращает объект Spliterator<T>, который представляет сплитератор для элементов стрима. Сплитератор предоставляет низкоуровневые операции итерации и разделения набора данных, полезные для реализации параллельной обработки и пользовательских алгоритмов обхода.
Когда используется
- При необходимости ручной итерации с контролем продолжения через
tryAdvanceили пакетной обработкой черезforEachRemaining. - Для получения информации о размере и характеристиках источника данных (через
estimateSize()иcharacteristics()), что полезно при оптимизации параллельной обработки. - При создании кастомных стримов или при конвертации между итераторами и стримами через
StreamSupport.
Сигнатура и возвращаемое значение
В интерфейсе java.util.stream.Stream определен метод:
Spliterator<T> spliterator()
Возвращаемое значение: Spliterator<T> (или примитивные специализации для IntStream, LongStream, DoubleStream - например, Spliterator.OfInt).
Аргументы
Метод не принимает аргументов.
Важные свойства получаемого Spliterator
tryAdvance(Consumer<? super T> action)- попытается обработать следующий элемент; возвращаетtrue, если элемент был.forEachRemaining(Consumer<? super T> action)- обработает оставшиеся элементы.trySplit()- попытается разделить сплитератор, возвращает новый сплитератор для части данных илиnull, если разделение невозможно или нецелесообразно.estimateSize()- приблизительное количество оставшихся элементов (может вернутьLong.MAX_VALUEдля неизвестного размера).characteristics()- битовая маска характеристик:ORDERED,DISTINCT,SORTED,SIZED,NONNULL,IMMUTABLE,CONCURRENT,SUBSIZED.
Побочные эффекты и ограничения
- Вызов
spliterator()на стриме считается потребляющей операцией; далее стрим использовать нельзя - любой последующий терминальный вызов вызовет исключение состояния. - Поведение
trySplitзависит от источника данных; гарантии равного разделения отсутствуют.
Простые примеры использования
1. Получение Spliterator и последовательная обработка с tryAdvance
Stream<String> s = Stream.of("a", "b", "c");
Spliterator<String> sp = s.spliterator();
sp.tryAdvance(System.out::println);
sp.tryAdvance(System.out::println);
sp.forEachRemaining(System.out::println);
a b c
2. Проверка характеристик и оценки размера
Stream<Integer> s2 = IntStream.range(0, 5).boxed().stream();
Spliterator<Integer> sp2 = s2.spliterator();
System.out.println(sp2.estimateSize());
System.out.println(Integer.toBinaryString(sp2.characteristics()));
5 1001
Значение битовой маски зависит от реализации; пример показывает, что размер известен (SIZED).
3. Демонстрация trySplit для параллельной обработки
Stream<Integer> s3 = IntStream.range(0, 10).boxed().stream();
Spliterator<Integer> sp3 = s3.spliterator();
Spliterator<Integer> left = sp3.trySplit();
if (left != null) {
left.forEachRemaining(i -> System.out.print(i + " "));
}
sp3.forEachRemaining(i -> System.out.print(i + " "));
0 1 2 3 4 5 6 7 8 9
Порядок и распределение между сплитерами зависят от реализации и источника.
Варианты и смежные API в Java
- Collection.spliterator() - у коллекций часто более точная информация о размере и характеристиках. Предпочтительнее использовать для коллекций, так как реализация оптимизирована.
- Iterable.spliterator() - для итерабельных источников; удобен, когда исходный объект не является стримом.
- Arrays.spliterator() и
Spliterators.spliterator- создание сплитераторов поверх массивов или произвольных итераторов; полезно при конструировании кастомных источников. - StreamSupport.stream(Spliterator, boolean) - обратная операция: из сплитератора создается стрим; предпочтительна для создания параллельных стримов на базе собственного сплитератора.
В большинстве случаев для обычной обработки предпочтительнее пользоваться стандартными методами стрима (forEach, map, collect). Сплитератор полезен при необходимости тонкой настройки разделения и итерации.
Аналоги в других языках
JavaScript
// Generator как аналог итератора
function* gen() { yield* [1,2,3,4]; }
const it = gen();
console.log(it.next().value);
1
Генераторы предоставляют управление последовательной выдачей, но не встроенного механизма разделения для параллелизма.
Python
# Итератор и itertools
it = iter([1,2,3,4])
print(next(it))
1
Для параллельного разделения используются библиотеки (multiprocessing, concurrent.futures) и кастомные партиционирующие генераторы.
C#
// IEnumerable и GetEnumerator
IEnumerable<int> seq = new[]{1,2,3};
var en = seq.GetEnumerator();
en.MoveNext();
Console.WriteLine(en.Current);
1
C# имеет Partitioners и PLINQ для разбиения при параллельной обработке, похожих по назначению на Spliterator.
Go
// Срез и каналы
s := []int{1,2,3}
fmt.Println(s[0])
1
Разбиение обычно делается вручную по индексам или через goroutine и каналы.
Kotlin
val seq = listOf(1,2,3).iterator()
println(seq.next())
1
Sequence и коллекции Kotlin используют JVM-сплитераторы под капотом при взаимодействии с Java API.
PHP
// Generator
function gen() { yield 1; yield 2; }
$it = gen();
var_dump($it->current());
int(1)
Вывод: большинство языков предоставляет итераторы/генераторы. Особенность Java - встроенные характеристики и API для эффективного параллельного разделения (Spliterator + StreamSupport).
Типичные ошибки и примеры
1. Попытка повторно использовать стрим после вызова spliterator()
Stream<String> s = Stream.of("x","y");
Spliterator<String> sp = s.spliterator();
// попытка повторного использования s
s.forEach(System.out::println);
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at ...
Причина: получение сплитератора потребляет стрим.
2. Неправильные ожидания от trySplit
Spliterator<Integer> sp = IntStream.range(0,3).boxed().stream().spliterator();
Spliterator<Integer> a = sp.trySplit();
System.out.println(a == null ? "null" : "not null");
null
Причина: для небольших источников или конкретной реализации разделение может быть не выполнено.
3. Игнорирование характеристики SIZED
Spliterator<String> sp = Stream.generate(() -> "x").limit(100).spliterator();
System.out.println(sp.characteristics());
System.out.println(sp.estimateSize());
0 100
При ленивых источниках характеристика SIZED может отсутствовать; оценка размера может быть Long.MAX_VALUE или меняться.
Изменения в спецификации и эволюция
Интерфейсы Spliterator и Stream.spliterator() введены в Java 8 вместе с Stream API. Последующие выпуски Java не меняли сигнатуру метода Stream.spliterator(), но развивались сопутствующие утилиты:
- Добавлялись дополнительные фабричные методы в
SpliteratorsиStreamSupportдля удобства создания сплитераторов из разных источников. - Поддержка примитивных специализированных сплитераторов (
OfInt,OfLong,OfDouble) и соответствующих утилит осталась и развивалась.
В целом поведение API стабильно с 8-й версии; дальнейшие изменения касались расширения утилит и улучшений реализации производительности.
Расширенные и редкие сценарии
1. Кастомный Spliterator через AbstractSpliterator для потоковой генерации
class FiboSpliterator extends Spliterators.AbstractSpliterator<Long> {
private long a = 0, b = 1;
FiboSpliterator(long est) { super(est, Spliterator.NONNULL | Spliterator.ORDERED); }
public boolean tryAdvance(Consumer<? super Long> action) {
action.accept(a);
long next = a + b; a = b; b = next;
return true; // бесконечный сплитератор; граница управляется estimate
}
}
Spliterator<Long> fib = new FiboSpliterator(Long.MAX_VALUE);
StreamSupport.stream(fib, false)
.limit(10)
.forEach(System.out::println);
0 1 1 2 3 5 8 13 21 34
Использование AbstractSpliterator упрощает реализацию сложного stateful-потока.
2. Создание параллельного стрима из кастомного Spliterator с эффективным trySplit
// Пример разделяющегося сплитератора для массива
class ArraySpl<T> implements Spliterator<T> {
private final T[] arr; private int lo, hi;
ArraySpl(T[] arr, int lo, int hi){this.arr=arr;this.lo=lo;this.hi=hi;}
public boolean tryAdvance(Consumer<? super T> action){ if(lo<hi){ action.accept(arr[lo++]); return true;} return false; }
public Spliterator<T> trySplit(){ int mid = (lo+hi)>>>1; return (mid<=lo) ? null : new ArraySpl(arr, lo, lo = mid); }
public long estimateSize(){ return hi-lo; }
public int characteristics(){ return Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED; }
}
String[] data = IntStream.range(0,16).mapToObj(Integer::toString).toArray(String[]::new);
Spliterator<String> sArr = new ArraySpl<>(data, 0, data.length);
Stream<String> parallel = StreamSupport.stream(sArr, true);
parallel.forEach(System.out::println);
(возможный вывод, порядок произвольный при параллельной обработке) 0 1 2 ... 15
Реализация trySplit делит диапазон по середине, что обеспечивает хорошую балансировку при параллельной обработке.
3. Оборачивание Iterator в Spliterator и формирование стрима
Iterator<String> it = Arrays.asList("a","b","c").iterator();
Spliterator<String> sp = Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED);
StreamSupport.stream(sp, false).map(String::toUpperCase).forEach(System.out::println);
A B C
Полезно для интеграции legacy-итераторов в Stream API.
4. Обработчик очереди как CONCURRENT Spliterator
BlockingQueue<String> q = new LinkedBlockingQueue<>();
q.add("x"); q.add("y");
Spliterator<String> qSpl = Spliterators.spliterator(q, Spliterator.CONCURRENT | Spliterator.NONNULL);
StreamSupport.stream(qSpl, true).forEach(System.out::println);
x y
Характеристика CONCURRENT указывает, что источник может безопасно изменяться одновременно с обходом.
5. Использование характеристик для оптимизации собственных операций
// Функция, которая использует знание о SIZED для предвыделения массива
Spliterator<Integer> s = IntStream.range(0,5).boxed().stream().spliterator();
if((s.characteristics() & Spliterator.SIZED) != 0) {
int size = (int) s.estimateSize();
Integer[] arr = new Integer[size];
AtomicInteger idx = new AtomicInteger();
s.forEachRemaining(i -> arr[idx.getAndIncrement()] = i);
System.out.println(Arrays.toString(arr));
} else {
System.out.println("size unknown");
}
[0, 1, 2, 3, 4]
Предвыделение буфера экономит ресурсы при известных размерах.