Прежде всего, за исключением особых случаев, таких как пропуск избыточной операции sorted
или возврат известного размера на count()
, кроме того, временная сложность операции обычно не меняется, поэтому все различия во времени выполнения обычно о постоянном смещении или (довольно небольшом) факторе, а не о фундаментальных изменениях.
Вы всегда можете написать ручной цикл, делающий в основном то же самое, что внутренняя реализация Stream. Таким образом, внутренняя оптимизация, как отмечал этот ответ , всегда могла быть отклонена словами «но я мог бы сделать то же самое в своем цикле».
Но… когда мы сравниваем «поток» с «циклом», действительно ли разумно предположить, что все ручные циклы написаны наиболее эффективным образом для конкретного варианта использования? Конкретная реализация Stream будет применять свои оптимизации ко всем случаям использования, где это применимо, независимо от уровня опыта автора вызывающего кода. Я уже видел циклы, в которых отсутствует возможность короткого замыкания или выполнения избыточных операций, которые не нужны для конкретного случая использования.
Другим аспектом является информация, необходимая для выполнения определенных оптимизаций. Stream API построен на интерфейсе Spliterator
, который может предоставлять характеристики исходных данных, например, это позволяет выяснить, имеет ли данные значимый порядок, который необходимо сохранить для определенных операций, или он уже предварительно отсортирован, к естественному порядку или с конкретным компаратором. Он также может предоставить ожидаемое количество элементов, в качестве приблизительного или точного, когда это предсказуемо.
Метод, получающий произвольный Collection
, для реализации алгоритма с обычным циклом, будет трудно выяснить, есть ли такие характеристики. List
подразумевает значимый порядок, тогда как Set
обычно нет, если только это не SortedSet
или LinkedHashSet
, тогда как последний является конкретным классом реализации, а не интерфейсом. Таким образом, при проверке всех известных группировок все еще могут отсутствовать реализации сторонних производителей со специальными контрактами, которые нельзя выразить с помощью предварительно определенного интерфейса.
Конечно, начиная с Java 8, вы могли бы самостоятельно приобрести Spliterator
, чтобы изучить эти характеристики, но это изменило бы ваше циклическое решение на нетривиальную вещь, а также подразумевало бы повторение работы, уже проделанной с Stream API.
Существует также еще одно интересное отличие между решениями Stream на основе Spliterator
и обычными циклами, использующими Iterator
при итерации по чему-то другому, чем массив. Шаблон должен вызывать hasNext
на итераторе, за которым следует next
, если hasNext
не вернул false
. Но контракт Iterator
не предписывает эту модель. Вызывающая сторона может вызывать next
без hasNext
, даже несколько раз, когда известно, что она успешна (например, вы уже знаете размер коллекции). Кроме того, вызывающий абонент может вызывать hasNext
несколько раз без next
в случае, если вызывающий абонент не запомнил результат предыдущего вызова.
Как следствие, реализации Iterator
должны выполнять избыточные операции, например, условие цикла эффективно проверяется дважды, один раз в hasNext
, чтобы вернуть boolean
, и один раз в next
, чтобы бросить NoSuchElementException
, когда не выполняется. Часто hasNext
должен выполнить фактическую операцию обхода и сохранить результат в экземпляре Iterator
, чтобы гарантировать, что результат остается действительным до следующего вызова next
. Операция next
, в свою очередь, должна проверить, был ли такой обход уже произошел или он должен выполнить саму операцию. На практике оптимизатор горячих точек может или не может устранить накладные расходы, налагаемые конструкцией Iterator
.
В отличие от Spliterator
имеет единственный метод обхода boolean tryAdvance(Consumer<? super T> action)
, который выполняет фактическую операцию , а возвращает наличие элемента. Это значительно упрощает логику цикла. Существует даже void forEachRemaining(Consumer<? super T> action)
для операций без короткого замыкания, что позволяет фактической реализации обеспечить всю логику зацикливания. Например, в случае ArrayList
операция закончится простым циклом подсчета индексов, обеспечивающим простой доступ к массиву.
Вы можете сравнить такой дизайн, например, с readLine()
из BufferedReader
, который выполняет операцию и возвращает null
после последнего элемента, или find()
регулярного выражения Matcher
, который выполняет поиск, обновляет состояние сопоставителя и возвращает состояние успеха.
Но влияние таких конструктивных различий трудно предсказать в среде с оптимизатором, специально разработанным для выявления и устранения избыточных операций. Вывод заключается в том, что есть некоторые возможности для решений на основе Stream оказаться еще быстрее, в то время как это зависит от многих факторов, будут ли они когда-либо реализованы в конкретном сценарии. Как было сказано в начале, обычно это не меняет общей сложности времени, о чем было бы более важно беспокоиться.