Неразделимые сплитераторы
Я пытаюсь понять как Spliterator
работает, и как сплитераторы разработаны. Я признаю это trySplit()
вероятно, один из наиболее важных методов Spliterator
, но когда я вижу какой-то сторонний Spliterator
реализации, иногда я вижу, что их сплитераторы возвращают ноль для trySplit()
безусловно.
Вопросы:
- Есть ли разница между обычным итератором и
Spliterator
что возвращает ноль безоговорочно? Кажется, что такой сплитератор побеждает, ну, в общем, расщепление. - Конечно, есть законные случаи использования сплитераторов, которые условно возвращают ноль на
trySplit()
, но есть ли законный вариант использования сплитератора, который безоговорочно возвращает ноль?
3 ответа
Хотя основным преимуществом Spliterator перед Iterator, как вы сказали, является его метод trySplit(), который позволяет его распараллеливать, существуют и другие существенные преимущества:
http://docs.oracle.com/javase/8/docs/api/java/util/Spliterator.html
Spliterator API был разработан для поддержки эффективного параллельного обхода в дополнение к последовательному обходу, поддерживая декомпозицию и одноэлементную итерацию. Кроме того, протокол для доступа к элементам через Spliterator предназначен для наложения меньших накладных расходов на элемент, чем Iterator, и во избежание внутренней гонки, связанной с наличием отдельных методов для hasNext() и next().
Кроме того, Spliterators можно напрямую преобразовать в потоки с помощью StreamSupport.stream, чтобы использовать потоки Java8.
Одна из целей Spliterator
это возможность разделить, но это не единственная цель. Другой основной целью является поддержка класса для создания собственного Stream
источник. Один из способов создания потокового источника - реализовать собственный Spliterator и передать его StreamSupport.stream
, Самое простое, что можно сделать, - это написать Spliterator, который не может быть разделен. Это заставляет поток выполняться последовательно, но это может быть приемлемо для всего, что вы пытаетесь сделать.
Есть и другие случаи, когда написание неразделимого Spliterator имеет смысл. Например, в OpenJDK есть реализации, такие как EmptySpliterator
которые не содержат элементов. Конечно, это нельзя разделить. Аналогичный случай - одноэлементный сплитератор, который содержит ровно один элемент. Это тоже нельзя разделить. Обе реализации возвращают null
безусловно из trySplit
,
Другой случай, когда написание неразделимого Spliterator является простым и эффективным, а объем кода, необходимого для реализации разделяемого, является непомерно высоким. (По крайней мере, не стоит того, чтобы записывать его в ответ переполнения стека.) Например, см. Пример Spliterator из этого ответа. Дело в том, что реализация Spliterator хочет обернуть другой Spliterator и сделать что-то особенное, в этом случае проверьте, не является ли он пустым. В противном случае он просто передает все обернутый Spliterator. Делать это с неразделимым Spliterator довольно легко.
Обратите внимание, что в этом ответе есть обсуждение, комментарий к этому ответу в моем ответе на тот же вопрос и цепочка комментариев к моему ответу о том, как можно создать сплитератор с разделением (то есть готовый параллельно). Но на самом деле никто не написал код для разделения.:-) В зависимости от того, сколько лени вы хотите сохранить в исходном потоке, и сколько параллельной эффективности вы хотите, написание разделяемого Spliterator может быть довольно сложным.
По моим оценкам, сделать это несколько проще, написав Iterator вместо Spliterator (как в моем ответе, указанном выше). Оказывается, что Spliterators.spliteratorUnknownSize
может обеспечить ограниченное количество параллелизма, даже от Итератора, который, очевидно, является чисто последовательной конструкцией. Это происходит в течение IteratorSpliterator
, который извлекает несколько элементов из Итератора и обрабатывает их в пакетном режиме. К сожалению, размер пакета жестко задан, но, по крайней мере, это дает возможность обрабатывать элементы, извлекаемые из Итератора параллельно в некоторых случаях.
Есть больше преимуществ, чем просто разделение поддержки:
Логика итерации содержится в одном
tryAdvance
метод, а не распространяется на два метода, такие какhasNext
,next
, Разделение логики на два метода усложняет многоIterator
реализации, так как это часто подразумевает, чтоhasNext
Метод должен выполнить фактическую попытку запроса, которая может привести к значению, которое затем необходимо запомнить для последующегоnext
вызов. И тот факт, что этот запрос был сделан, также должен быть запомнен, явно или неявно.Было бы проще, если бы была гарантия, что
hasNext
/next
всегда называются типичным чередующимся способом, однако такой гарантии нет.Одним из примеров является
BufferedReader.readLine()
который имеет простойtryAdvance
логика. УпаковкаIterator
должен вызвать этот метод в пределахhasNext
реализация и запомнить линию дляnext
вызов. (Как ни странно, нынешнийBufferedReader.stream()
реализация действительно реализует такой сложныйIterator
это будет завернуто вSpliterator
вместо реализации гораздо прощеSpliterator
непосредственно. Кажется, что проблему "я не знаком с этим" не следует недооценивать)estimateSize()
;Spliterator
может вернуть оценку (или даже точное число) оставшихся элементов, которые можно использовать для предварительного распределения ресурсов. Это может повысить эффективность.characteristics()
;Spliterator
s может предоставить дополнительную информацию об их содержании или поведении. Помимо того, чтобы сказать, является ли предполагаемый размер точным размером, вы можете узнать, можете ли вы увидетьnull
значения, есть ли определенный порядок встречи или все значения различны. Конкретный алгоритм может воспользоваться этим. Очевидно, чтоStream
API представляет собой набор таких алгоритмов, которые могут использовать преимущества, поэтому при планировании создания (или поддержки создания) потоков и возможности выбора при реализацииSpliterator
предпочтительнее сообщать как можно больше метаинформацииIterator
это будет завернуто позже.