Что из следующего является лучшей практикой в Java 8?
Java 8:
joins.forEach(join -> mIrc.join(mSession, join));
Java 7:
for (String join : joins) {
mIrc.join(mSession, join);
}
У меня есть много циклов for, которые можно «упростить» с помощью лямбд, но есть ли у них преимущество? Будет ли это улучшить их производительность и удобочитаемость?
РЕДАКТИРОВАТЬ
Я также расширю этот вопрос на более длинные методы. Я знаю, что вы не можете вернуть или сломать родительскую функцию из лямбды, и это также следует учитывать при сравнении, но есть ли что-то еще, что следует учитывать?
java
for-loop
java-8
java-stream
nebkat
источник
источник
joins.forEach((join) -> mIrc.join(mSession, join));
действительно "упрощение"for (String join : joins) { mIrc.join(mSession, join); }
? Вы увеличили количество знаков препинания с 9 до 12, чтобы скрыть типjoin
. То, что вы действительно сделали, это поместили два утверждения в одну строку.Ответы:
Лучшей практикой является использование
for-each
. Помимо нарушения принципа Keep It Simple, Stupid , у новомодногоforEach()
есть по крайней мере следующие недостатки:Не могу использовать не финальные переменные . Таким образом, код, подобный следующему, не может быть превращен в лямбду forEach:
Не могу обработать проверенные исключения . Лямбды на самом деле не запрещают генерировать проверенные исключения, но такие обычные функциональные интерфейсы, как
Consumer
не объявляют их. Поэтому любой код, который выбрасывает проверенные исключения, должен заключать их вtry-catch
илиThrowables.propagate()
. Но даже если вы это сделаете, не всегда понятно, что происходит с брошенным исключением. Это может быть проглочено где-то в кишкахforEach()
Ограниченный контроль потока . A
return
в лямбде равно acontinue
в for-each, но нет эквивалента abreak
. Также сложно сделать такие вещи, как возвращаемые значения, короткое замыкание или установить флаги (что могло бы немного облегчить ситуацию, если бы это не было нарушением правила отсутствия нефинальных переменных ). «Это не просто оптимизация, а критическое, если учесть, что некоторые последовательности (например, чтение строк в файле) могут иметь побочные эффекты или бесконечную последовательность».Может выполняться параллельно , что ужасно, ужасно для всех, кроме 0,1% кода, который необходимо оптимизировать. Любой параллельный код должен быть продуман (даже если он не использует блокировки, изменчивость и другие особенно неприятные аспекты традиционного многопоточного исполнения). Любую ошибку будет сложно найти.
Может ухудшить производительность , потому что JIT не может оптимизировать forEach () + lambda в той же степени, что и обычные циклы, особенно теперь, когда лямбды являются новыми. Под «оптимизацией» я имею в виду не накладные расходы при вызове лямбда-выражений (которые малы), а сложный анализ и преобразование, которые современный JIT-компилятор выполняет при выполнении кода.
Если вам нужен параллелизм, возможно, гораздо быстрее и не намного сложнее использовать ExecutorService . Потоки оба являются автоматическими (читай: мало что знаешь о твоей проблеме) и используют специализированную (читай: неэффективная для общего случая) стратегию распараллеливания ( рекурсивная декомпозиция fork-join ).
Делает отладку более запутанной из-за вложенной иерархии вызовов и, не дай бог, параллельного выполнения. Отладчик может иметь проблемы с отображением переменных из окружающего кода, и такие вещи, как пошаговое выполнение, могут работать не так, как ожидалось.
Потоки в целом сложнее кодировать, читать и отлаживать . На самом деле, это справедливо для сложных « беглых » API в целом. Сочетание сложных отдельных операторов, интенсивное использование обобщенных выражений и отсутствие промежуточных переменных способствуют получению запутанных сообщений об ошибках и затрудняют отладку. Вместо «этот метод не перегружен для типа X» вы получаете сообщение об ошибке ближе к «где-то вы перепутали типы, но мы не знаем, где и как». Точно так же вы не можете пройтись и исследовать вещи в отладчике так же легко, как когда код разбит на несколько операторов, а промежуточные значения сохранены в переменных. Наконец, чтение кода и понимание типов и поведения на каждом этапе выполнения может быть нетривиальным.
Торчит как больной палец . В языке Java уже есть оператор for-each. Зачем заменять его вызовом функции? Зачем поощрять скрывать побочные эффекты где-то в выражениях? Зачем поощрять громоздкие однострочники? Смешивание обычного для каждого и нового для каждого волей-неволей - плохой стиль. Код должен говорить на идиомах (шаблоны, которые легко понять благодаря их повторению), и чем меньше идиом используется, тем яснее код и тратится меньше времени на решение, какую идиому использовать (большая трата времени для перфекционистов, таких как я! ).
Как видите, я не большой поклонник forEach (), за исключением случаев, когда это имеет смысл.
Особенно оскорбительным для меня является тот факт, что
Stream
он не реализуетIterable
(несмотря на наличие методаiterator
) и не может использоваться для каждого, только с forEach (). Я рекомендую приводить Streams в Iterables с помощью(Iterable<T>)stream::iterator
. Лучшей альтернативой является использование StreamEx, который устраняет ряд проблем API-интерфейса Stream, включая реализациюIterable
.Тем не менее,
forEach()
полезно для следующего:Атомная перебор по синхронизированному списку . До этого список, сгенерированный с помощью,
Collections.synchronizedList()
был атомарным по отношению к таким вещам, как get или set, но не был потокобезопасным при итерации.Параллельное выполнение (используя соответствующий параллельный поток) . Это сэкономит вам несколько строк кода по сравнению с использованием ExecutorService, если ваша проблема соответствует предположениям о производительности, встроенным в Streams и Spliterators.
Конкретные контейнеры, которые , как и синхронизированный список, выигрывают от контроля над итерациями (хотя это в значительной степени теоретически, если люди не могут привести больше примеров)
Более простой вызов одной функции с помощью
forEach()
аргумента и ссылки на метод (т. Е.list.forEach (obj::someMethod)
). Однако имейте в виду пункты о проверенных исключениях, более сложной отладке и сокращении числа идиом, которые вы используете при написании кода.Статьи, которые я использовал для справки:
РЕДАКТИРОВАТЬ: Похоже, что некоторые из оригинальных предложений для лямбда (например, http://www.javac.info/closures-v06a.html ) решили некоторые из проблем, которые я упомянул (конечно, добавляя свои собственные сложности).
источник
forEach
предназначена для поощрения функционального стиля, то есть использования выражений без побочных эффектов. Если вы столкнетесь с ситуацией, когдаforEach
ваши побочные эффекты не будут работать должным образом, у вас должно появиться ощущение, что вы не используете правильный инструмент для своей работы. Тогда простой ответ заключается в том, что ваше чувство верно, поэтому оставайтесь в цикле «для каждого». Классическаяfor
петля не стала устаревшей ...forEach
использовать без побочных эффектов?forEach
это единственная потоковая операция, предназначенная для побочных эффектов, но это не для побочных эффектов, как ваш пример кода, подсчет является типичнойreduce
операцией. Я бы предложил, как правило, сохранить каждую операцию, которая манипулирует локальными переменными или должна влиять на поток управления (включая обработку исключений) в классическомfor
цикле. Что касается исходного вопроса, я думаю, проблема связана с тем, что кто-то использует поток, где простойfor
цикл над источником потока будет достаточно. Используйте поток, гдеforEach()
работает толькоforEach
которые подойдут ?for
петлями.Преимущество учитывается, когда операции могут выполняться параллельно. (См. Http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - раздел о внутренней и внешней итерации)
Основным преимуществом с моей точки зрения является то, что реализация того, что должно быть сделано в цикле, может быть определена без необходимости решать, будет ли он выполняться параллельно или последовательно
Если вы хотите, чтобы ваш цикл выполнялся параллельно, вы можете просто написать
Вам придется написать дополнительный код для обработки потоков и т. Д.
Примечание: для моего ответа я предполагал присоединиться к реализации
java.util.Stream
интерфейса. Если в соединениях реализован толькоjava.util.Iterable
интерфейс, это больше не так.источник
map
&fold
, которые на самом деле не связаны с лямбдами.Stream#forEach
иIterable#forEach
не одно и то же. ОП спрашивает оIterable#forEach
.joins
он реализуетсяIterable
вместоStream
? Из нескольких вещей, которые я прочитал, OP должен быть в состоянии сделать,joins.stream().forEach((join) -> mIrc.join(mSession, join));
иjoins.parallelStream().forEach((join) -> mIrc.join(mSession, join));
еслиjoins
реализуетIterable
При чтении этого вопроса у вас может сложиться впечатление, что
Iterable#forEach
в сочетании с лямбда-выражениями это ярлык / замена для написания традиционного цикла for-each. Это просто неправда. Этот код из ОП:это не предназначен в качестве ярлыка для записи
и, конечно, не должны использоваться таким образом. Вместо этого он предназначен как ярлык (хотя это не совсем то же самое) для записи
И это как замена для следующего кода Java 7:
Замена тела цикла на функциональный интерфейс, как в приведенных выше примерах, делает ваш код более явным: вы говорите, что (1) тело цикла не влияет на окружающий код и поток управления, и (2) Тело цикла может быть заменено другой реализацией функции, не затрагивая окружающий код. Неспособность получить доступ к неконечным переменным внешней области видимости не является недостатком функций / лямбда-выражений, это особенность, которая отличает семантику
Iterable#forEach
от семантики традиционного цикла for-each. Как только человек привыкает к синтаксисуIterable#forEach
, он делает код более читабельным, потому что вы сразу получаете эту дополнительную информацию о коде.Традиционные циклы for-each, безусловно, останутся хорошей практикой (во избежание чрезмерного использования термина " best Practice ") в Java. Но это не значит, что это
Iterable#forEach
следует считать плохой практикой или плохим стилем. Это всегда хорошая практика, использовать правильный инструмент для выполнения работы, и это включает в себя смешивание традиционных для каждого цикла сIterable#forEach
, где это имеет смысл.Поскольку недостатки
Iterable#forEach
уже обсуждались в этой теме, вот несколько причин, по которым вы, возможно, захотите использоватьIterable#forEach
:Чтобы сделать ваш код более явным: Как описано выше,
Iterable#forEach
может сделать ваш код более явным и читаемым в некоторых ситуациях.Чтобы сделать ваш код более расширяемым и обслуживаемым: Использование функции в качестве тела цикла позволяет заменить эту функцию различными реализациями (см. Шаблон стратегии ). Например, вы можете легко заменить лямбда-выражение на вызов метода, который может быть перезаписан подклассами:
Затем вы можете предоставить стратегии по умолчанию, используя enum, который реализует функциональный интерфейс. Это не только делает ваш код более расширяемым, но и повышает удобство сопровождения, потому что оно отделяет реализацию цикла от объявления цикла.
Чтобы сделать ваш код более отлаживаемым: Отделение реализации цикла от объявления также может упростить отладку, поскольку у вас может быть специальная реализация отладки, которая выводит отладочные сообщения без необходимости загромождать ваш основной код
if(DEBUG)System.out.println()
. Реализация отладки может быть, например, делегатом , который украшает фактическую реализацию функции.Для оптимизации производительности критически важных код: В отличие от некоторых утверждений в этой теме,
Iterable#forEach
это уже обеспечивает более высокую производительность , чем традиционный для-каждого цикла, по крайней мере при использовании ArrayList и работает в режиме Hotspot «-client». Хотя это повышение производительности является небольшим и незначительным для большинства случаев использования, существуют ситуации, когда эта дополнительная производительность может иметь значение. Например, сопровождающие библиотеки наверняка захотят оценить, следует ли заменить некоторые из их существующих реализаций циклаIterable#forEach
.Чтобы подкрепить это утверждение фактами, я сделал несколько микро-тестов с Caliper . Вот тестовый код (необходим последний Caliper от git):
И вот результаты:
При запуске с -client
Iterable#forEach
превосходит традиционный цикл for над ArrayList, но все же медленнее, чем прямая итерация по массиву. При работе с "-server" производительность всех подходов примерно одинакова.Для обеспечения дополнительной поддержки параллельного выполнения: здесь уже было сказано, что возможность выполнения функционального интерфейса
Iterable#forEach
параллельно с использованием потоков , безусловно, является важным аспектом. ПосколькуCollection#parallelStream()
не гарантирует, что цикл на самом деле выполняется параллельно, следует рассматривать это как дополнительную функцию. Перебирая свой список с помощьюlist.parallelStream().forEach(...);
, вы явно говорите: этот цикл поддерживает параллельное выполнение, но не зависит от него. Опять же, это особенность, а не дефицит!Отодвигая решение о параллельном выполнении от вашей реальной реализации цикла, вы разрешаете необязательную оптимизацию вашего кода, не влияя на сам код, что хорошо. Кроме того, если реализация параллельного потока по умолчанию не соответствует вашим потребностям, никто не мешает вам предоставить собственную реализацию. Например, вы можете предоставить оптимизированную коллекцию в зависимости от базовой операционной системы, от размера коллекции, от числа ядер и от некоторых настроек:
Приятно то, что вашей реализации цикла не нужно знать или заботиться об этих деталях.
источник
forEach
иfor-each
на основе некоторых критериев относительно природы тела цикла. Мудрость и дисциплина, чтобы следовать таким правилам, являются отличительной чертой хорошего программиста. Такие правила также являются его проклятием, потому что окружающие его люди либо не следуют им, либо не соглашаются. Например, используя проверенные и непроверенные исключения. Эта ситуация кажется еще более нюансированной. Но, если тело «не влияет на код окружения или управление потоком», не лучше ли выделить его как функцию?But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?
, Да, на мой взгляд, это часто будет так - выделение этих циклов как функций является естественным следствием.Iterable#forEach
до Java 8, только из-за увеличения производительности. В рассматриваемом проекте есть один основной цикл, похожий на игровой цикл, с неопределенным числом вложенных подциклов, где клиенты могут подключать участников цикла как функции. Такая структура программного обеспечения значительно выигрывает отIteable#forEach
.forEach()
может быть реализован, чтобы быть быстрее, чем для каждого цикла, потому что итерируемый знает лучший способ итерации своих элементов, в отличие от стандартного способа итератора. Таким образом, разница заключается в петле внутри или петле снаружи.Например,
ArrayList.forEach(action)
может быть просто реализовано какв отличие от цикла для каждого, который требует много лесов
Однако нам также необходимо учесть две накладные расходы, используя
forEach()
один: создание лямбда-объекта, другой вызов лямбда-метода. Они, вероятно, не значимы.см. также http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ для сравнения внутренних и внешних итераций для различных вариантов использования.
источник
void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}
это более элегантно, чем внешняя итерация, и вы можете решить, как лучше синхронизироватьString.join
методах (хорошо, неправильное соединение) - это «Количество элементов, которые вряд ли стоит накладных расходов Arrays.stream». поэтому они используют шикарный цикл.TL; DR :
List.stream().forEach()
был самым быстрым.Я чувствовал, что должен добавить свои результаты из итераций бенчмаркинга. Я выбрал очень простой подход (без тестовых сред) и протестировал 5 различных методов:
for
List.forEach()
List.stream().forEach()
List.parallelStream().forEach
процедура и параметры тестирования
Список в этом классе должен быть повторен, и некоторые должны
doIt(Integer i)
применяться ко всем его членам, каждый раз с помощью другого метода. в Главном классе я запускаю проверенный метод три раза, чтобы разогреть JVM. Затем я запускаю тестовый метод 1000 раз, суммируя время, необходимое для каждого итерационного метода (используяSystem.nanoTime()
). После этого я делю эту сумму на 1000, и это результат, среднее время. пример:Я запустил это на 4-ядерном процессоре i5 с версией Java 1.8.0_05
классический
for
время выполнения: 4,21 мс
классический foreach
время выполнения: 5,95 мс
List.forEach()
время выполнения: 3,11 мс
List.stream().forEach()
время выполнения: 2,79 мс
List.parallelStream().forEach
время выполнения: 3,6 мс
источник
System.out.println
показываете эти данные наивно, тогда все результаты бесполезны.System.nanoTime()
. Если вы прочитаете ответ, вы увидите, как это было сделано. Я не думаю, что это делает бесполезным видение, так как это относительный вопрос. Меня не волнует, насколько хорошо работает определенный метод, мне важно, насколько он эффективен по сравнению с другими методами.Я чувствую, что мне нужно немного расширить свой комментарий ...
О парадигме \ стиле
Это, наверное, самый заметный аспект. FP стал популярным благодаря тому, что вы можете избежать побочных эффектов. Я не буду углубляться в то, какие плюсы и минусы вы можете получить от этого, так как это не связано с вопросом.
Однако я скажу, что итерация с использованием Iterable.forEach основана на FP и скорее является результатом добавления большего количества FP в Java (как ни странно, я бы сказал, что forEach в чистом FP не имеет большого смысла, поскольку он ничего не делает, кроме как представляет побочные эффекты).
В конце я бы сказал, что это скорее вопрос вкуса \ стиля \ парадигмы, в котором вы сейчас пишете.
О параллелизме.
С точки зрения производительности нет никаких обещанных заметных преимуществ от использования Iterable.forEach по сравнению с foreach (...).
Согласно официальным документам на Iterable.forEach :
... т.е. документы в значительной степени ясно, что не будет никакого неявного параллелизма. Добавление одного будет нарушением LSP.
Теперь есть «параллельные коллекции», которые обещаны в Java 8, но для работы с ними вам нужно быть более явным и уделить особое внимание их использованию (см., Например, ответ mschenk74).
КСТАТИ: в этом случае будет использоваться Stream.forEach , и это не гарантирует, что фактическая работа будет выполняться в параллель (зависит от базовой коллекции).
ОБНОВЛЕНИЕ: может быть не так очевидно и немного растянуто, но есть еще один аспект стиля и читабельности.
Прежде всего - старые добрые петли бывают простыми и старыми. Все уже знают их.
Второе, и более важное - вы, вероятно, хотите использовать Iterable.forEach только с однострочными лямбдами. Если «тело» становится тяжелее - они, как правило, не очень читабельны. У вас есть 2 варианта отсюда - использовать внутренние классы (yuck) или использовать обычный старый forloop. Людей часто раздражает, когда они видят, что одни и те же вещи (итерации над коллекциями) выполняются различными способами / стилями в одной и той же кодовой базе, и, похоже, это так.
Опять же, это может или не может быть проблемой. Зависит от людей, работающих над кодом.
источник
collection.forEach(MyClass::loopBody);
Одно из ограничений наиболее функциональных возможностей
forEach
- отсутствие поддержки проверенных исключений.Один из возможных способов - заменить терминал
forEach
простым старым циклом foreach:Вот список наиболее популярных вопросов с другими обходными путями при проверке исключений в лямбдах и потоках:
Java 8 Лямбда-функция, которая выдает исключение?
Java 8: лямбда-потоки, фильтрация по методам с исключением
Как я могу выбросить CHECKED исключения из потоков Java 8?
Java 8: Обязательная проверка проверенных исключений в лямбда-выражениях. Почему обязательно, а не необязательно?
источник
Преимущество метода Java 1.8 forEach перед 1.7 Enhanced for loop заключается в том, что при написании кода вы можете сосредоточиться только на бизнес-логике.
Метод forEach принимает объект java.util.function.Consumer в качестве аргумента, поэтому он помогает хранить нашу бизнес-логику в отдельном месте, чтобы вы могли использовать ее в любое время.
Посмотрите на ниже фрагмент,
Здесь я создал новый класс, который будет переопределять метод класса приема из класса Consumer, где вы можете добавить дополнительную функциональность, Больше, чем Итерация .. !!!!!!
источник