Как решить между лямбда-итерацией и нормальной петлей?

С момента появления Java 8 я действительно подключился к лямбдам и начал использовать их, когда это было возможно, в основном, чтобы привыкнуть к ним. Одним из наиболее распространенных способов использования является то, что мы хотим перебирать и действовать на коллекцию объектов, и в этом случае я либо прибегаю к forEach, либо stream(). Я редко пишу старый цикл for(T t : Ts), и я почти забыл о for(int i = 0.....).

Однако мы обсуждали это с моим руководителем на днях, и он сказал мне, что лямбды не всегда являются лучшим выбором и могут иногда мешать работе. Из лекции, которую я видел по этой новой функции, у меня появилось ощущение, что итерации лямбда всегда полностью оптимизированы компилятором и будут (всегда?) Лучше, чем голые итерации, но он просит отличиться. Это правда? Если да, то как мне отличить лучшее решение в каждом сценарии?

P.S: Я не говорю о случаях, когда рекомендуется применять parallelStream. Очевидно, что они будут быстрее.

Ответ 1

Это зависит от конкретной реализации.

Обычно метод forEach и forEach loop over Iterator обычно имеют довольно сходную производительность, поскольку они используют аналогичный уровень абстракции. stream() обычно медленнее (часто на 50-70%), поскольку он добавляет еще один уровень, обеспечивающий доступ к базовой коллекции.

Преимущества stream() обычно - это возможная parallelism и легкая цепочка операций с множеством многоразовых, предоставляемых JDK.

Ответ 2

Производительность зависит от множества факторов, которые трудно предсказать. Обычно, мы бы сказали, если ваш руководитель утверждает, что есть проблема с производительностью, ваш руководитель отвечает за объяснение, какая проблема.

Одна вещь, которую кто-то может бояться, заключается в том, что за кулисами класс создается для каждого сайта создания лямбда (с текущей реализацией), поэтому, если рассматриваемый код выполняется только один раз, это можно считать отходами ресурсов. Это согласуется с тем фактом, что лямбда-выражения имеют более высокие издержки инициализации в качестве обычного императивного кода (мы не сравнимы с внутренними классами здесь), поэтому внутри инициализаторов классов, которые запускаются только один раз, вы можете избежать его. Это также соответствует тому факту, что вы никогда не используете параллельные потоки в инициализаторах классов, поэтому это потенциальное преимущество здесь не доступно.

Для обычного, часто исполняемого кода, который может быть оптимизирован JVM, эти проблемы не возникают. Как вы правильно предположили, классы, созданные для лямбда-выражений, получают одинаковое обращение (оптимизация) как другие классы. В этих местах вызов forEach в коллекциях несет потенциал более эффективного, чем цикл for.

Временные экземпляры объектов, созданные для выражения Iterator или лямбда, незначительны, однако, возможно, стоит отметить, что цикл foreach всегда создает экземпляр Iterator, тогда как lambda выражение не всегда делает. В то время как реализация default Iterable.forEach создаст Iterator, некоторые из наиболее часто используемых коллекций воспользуются возможностью для предоставления специализированной реализации, в первую очередь ArrayList.

ArrayList s forEach - это в основном цикл for по массиву без Iterator. Затем он вызовет метод accept Consumer, который будет сгенерированным классом, содержащим тривиальное делегирование к синтетическому методу, содержащему код вашего выражения лямбда. Чтобы оптимизировать весь цикл, горизонт оптимизатора должен охватывать цикл ArrayList s над массивом (общая идиома, распознаваемая оптимизатором), синтетический метод accept, содержащий тривиальное делегирование и метод, содержащий ваш фактический код.

Напротив, при повторении в том же списке с использованием цикла foreach создается реализация Iterator, содержащая логику итерации ArrayList, распространяющуюся на два метода: hasNext() и next() и переменные экземпляра Iterator. Цикл будет многократно вызывать метод hasNext() для проверки конечного условия (index<size) и next(), который будет проверять условие перед возвратом элемента, так как нет гарантии, что вызывающий абонент корректно вызывает hasNext() до next(). Разумеется, оптимизатор способен удалить это дублирование, но для этого требуется больше усилий, чем не иметь его в первую очередь. Таким образом, чтобы получить такую ​​же производительность метода forEach, горизонт оптимизаторов должен охватывать ваш код цикла, нетривиальную реализацию hasNext() и нетривиальную реализацию next().

Аналогичные вещи могут относиться и к другим коллекциям, имеющим специализированную реализацию forEach. Это также относится к потоковым операциям, если источник предоставляет специализированную реализацию Spliterator, которая не распространяет логику итерации на два метода, таких как Iterator.

Итак, если вы хотите обсудить технические аспекты for каждого против forEach(…), вы можете использовать эту информацию.

Но, как сказано, эти аспекты описывают только потенциальные аспекты производительности, так как работа оптимизатора и другие аспекты среды выполнения могут полностью изменить результат. Я считаю, что, как правило, чем меньше тело/действие цикла, тем более подходящим является метод forEach. Это прекрасно согласуется с рекомендацией избегать слишком длинных лямбда-выражений.