Итератор против streamа Java 8

Чтобы воспользоваться широким спектром методов запросов, включенных в java.util.stream из Jdk 8, я пытаюсь разработать модели доменов, где геттеры отношения с * множественностью (с нулем или более экземплярами) возвращают Stream , а не Iterable или Iterator .

Мое сомнение заключается в том, что с Stream по сравнению с Iterator возникают дополнительные накладные расходы?

Итак, есть ли какой-либо недостаток компрометации моей модели домена с Stream ?

Или вместо этого, должен ли я всегда возвращать Iterator или Iterable и оставлять конечному пользователю решение выбрать, использовать ли stream, или нет, путем преобразования этого iteratorа с StreamUtils ?

Обратите внимание, что возврат Collection не является допустимым, потому что в этом случае большинство отношений являются ленивыми и неизвестными.

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

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

Хотя существует множество соображений, которые могут сделать stream медленнее, чем, например, или быстрее, чем какая-либо другая форма обхода в любом конкретном случае, есть некоторые факторы, указывающие на streamи, – это преимущество производительности, когда оно рассчитывает – на большие наборы данных.

Есть некоторые дополнительные фиксированные накладные расходы на запуск создания Stream по сравнению с созданием Iterator – еще несколько объектов, прежде чем вы начнете вычислять. Если ваш dataset большой, это не имеет значения; это небольшая стоимость запуска, амортизируемая по множеству вычислений. (И если ваш dataset невелик, это, вероятно, также не имеет значения, потому что, если ваша программа работает с небольшими наборами данных, производительность, как правило, не относится к вашей проблеме №1.) Если это имеет значение, когда вы собираетесь параллельно; любое время, потраченное на создание трубопровода, переходит в серийную долю закона Амдала; если вы посмотрите на реализации, мы упорно работаем, чтобы подсчитать объект во время настройки streamа, но я был бы рад, чтобы найти способы, чтобы уменьшить его, как и оказывает непосредственное влияние на безубыточность набора данных размера, где параллельно начинает победу над последовательный.

Но, что более важно, чем фиксированная стоимость запуска, это стоимость доступа к каждому элементу. Здесь streamи фактически выигрывают – и часто выигрывают большие, которые некоторые могут удивить. (В наших тестах производительности мы обычно видим streamовые конвейеры, которые могут превзойти их for-loop над Collection коллегами.) И для этого есть простое объяснение: у Spliterator есть принципиально более низкие затраты на доступ к элементам, чем Iterator , даже последовательно. На это есть несколько причин.

  1. Протокол Iterator существенно менее эффективен. Это требует вызова двух методов для получения каждого элемента. Кроме того, поскольку iteratorы должны быть надежными для таких вещей, как call next() без hasNext() или hasNext() несколько раз без next() , оба этих метода обычно должны выполнять некоторую защитную кодировку (и, как правило, более стойкую и разветвленную) что добавляет неэффективности. С другой стороны, даже медленный способ пересечения разделителя ( tryAdvance ) не имеет такого бремени. (Это еще хуже для параллельных структур данных, поскольку двойственность next / hasNext является фундаментальной, а реализации Iterator должны делать больше работы для защиты от одновременных модификаций, чем реализация Spliterator .)

  2. Spliterator также предлагает «ускоренную» итерацию – forEachRemaining – которая может использоваться большую часть времени (сокращение, forEach), что дополнительно уменьшает накладные расходы итерационного кода, который опосредует доступ к внутренним структурам данных. Это также имеет тенденцию очень хорошо встраиваться, что, в свою очередь, повышает эффективность других оптимизаций, таких как движение кода, ограничение проверки и т. Д.

  3. Кроме того, обход через Spliterator как правило, имеет намного меньше кучи, чем с Iterator . С помощью Iterator каждый элемент вызывает одну или несколько записей кучи (если Iterator может быть сканирован с помощью анализа эвакуации, а его поля подняты в регистры.) Среди других проблем это вызывает активность маркерной карты GC, что приводит к конкуренции в строке кэша для марок карт. С другой стороны, Spliterators имеют тенденцию к меньшему состоянию, а промышленная сила forEachRemaining реализует склонность записывать что-либо в кучу до конца обхода, вместо этого сохраняя свое итерационное состояние в локалях, которые, естественно, отображаются на регистры, что приводит к уменьшению памяти автобусной активности.

Резюме: не волнуйтесь, будьте счастливы. Spliterator – лучший Iterator , даже без параллелизма. (Их вообще просто проще писать и сложнее ошибиться).

Давайте сравним общую операцию итерации по всем элементам, считая, что источником является ArrayList . Тогда есть три стандартных способа достижения этого:

  • Collection.forEach

     final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); } 
  • Iterator.forEachRemaining

     final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } 
  • Stream.forEach который в конечном итоге Spliterator.forEachRemaining

     if ((i = index) >= 0 && (index = hi) <= a.length) { for (; i < hi; ++i) { @SuppressWarnings("unchecked") E e = (E) a[i]; action.accept(e); } if (lst.modCount == mc) return; } 

Как вы можете видеть, внутренний цикл кода реализации, в котором эти операции заканчиваются, в основном то же самое, итерация по индексам и прямое чтение массива и передача элемента к Consumer .

Подобные вещи применимы ко всем стандартным наборам JRE, все они адаптировали реализации для всех способов сделать это, даже если вы используете оболочку только для чтения. В последнем случае Stream API будет даже немного выигрывать, Collection.forEach должен быть вызван в режиме только для чтения, чтобы делегировать исходную коллекцию forEach . Точно так же iterator должен быть завернут для защиты от попыток вызвать метод remove() . Напротив, spliterator() может напрямую возвращать Spliterator оригинальной коллекции, поскольку он не поддерживает модификацию. Таким образом, stream представления только для чтения точно такой же, как stream исходной коллекции.

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

Вопрос в том, какой вывод следует сделать из этого. Вы все еще можете вернуть представление обертки только для чтения в исходную коллекцию, поскольку вызывающий объект все еще может вызывать stream().forEach(…) для прямой итерации в контексте исходной коллекции.

Поскольку производительность на самом деле не отличается, вам следует сосредоточиться на дизайне более высокого уровня, как описано в разделе «Должен ли я возвращать коллекцию или stream?».