Java 8 Iterable.forEach () vs foreach loop

Какая из следующих является лучшей практикой в ​​Java 8?

Java 8:

joins.forEach(join -> mIrc.join(mSession, join)); 

Java 7:

 for (String join : joins) { mIrc.join(mSession, join); } 

У меня много циклов, которые могут быть «упрощены» с помощью lambdas, но действительно ли есть какое-то преимущество в их использовании, включая производительность и удобочитаемость?

РЕДАКТИРОВАТЬ

Я также продолжу этот вопрос до более длинных методов – я знаю, что вы не можете возвратить или разбить родительскую функцию из lambda, и об этом следует упомянуть, если они будут сравниваться, но есть ли что-нибудь еще, что нужно рассмотреть?

Преимущество возникает, когда операции могут выполняться параллельно. (См. http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and – раздел о внутренней и внешней итерации)

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

  • Если вы хотите, чтобы ваш цикл выполнялся параллельно, вы могли просто написать

      joins.parallelStream().forEach(join -> mIrc.join(mSession, join)); 

    Вам нужно будет написать дополнительный код для обработки streamов и т. Д.

Примечание. Для моего ответа я предположил, что присоединяется к интерфейсу java.util.Stream . Если join реализует только интерфейс java.util.Iterable это уже не так.

Лучшая практика – использовать for-each . Помимо нарушения принципа Keep It Simple, Stupid , новый метод forEach() имеет по крайней мере следующие недостатки:

  • Невозможно использовать не конечные переменные . Таким образом, код, подобный приведенному ниже, не может быть превращен в forIach lambda:

     Object prev = null; for(Object curr : list) { if( prev != null ) foo(prev, curr); prev = curr; } 
  • Не удается обработать проверенные исключения . Lambdas на самом деле запрещено запрещать проверенные исключения, но обычные функциональные интерфейсы, такие как Consumer , не объявляют. Поэтому любой код, который выдает проверенные исключения, должен обернуть их в try-catch или Throwables.propagate() . Но даже если вы это сделаете, не всегда ясно, что происходит с выброшенным исключением. Его можно было проглотить где-то в кишках forEach()

  • Ограниченный контроль streamа . return в lambda равен continue в каждом для каждого, но нет эквивалента break . Также трудно делать такие вещи, как возвращаемые значения, короткое замыкание или установить флаги (что немного облегчило бы ситуацию, если бы это не было нарушением правила не конечных переменных ). «Это не просто оптимизация, но и критическая, когда вы считаете, что некоторые последовательности (например, чтение строк в файле) могут иметь побочные эффекты, или вы можете иметь бесконечную последовательность».

  • Может выполняться параллельно , что является ужасным, ужасным для всех, кроме 0.1% вашего кода, который нужно оптимизировать. Любой параллельный код необходимо продумать (даже если он не использует блокировки, летучие компоненты и другие особенно неприятные аспекты традиционного многопоточного исполнения). Любая ошибка будет трудно найти.

  • Это может повредить производительность , потому что JIT не может оптимизировать forEach () + lambda в той же степени, что и обычные петли, особенно теперь, когда лямбды новы. Под «оптимизацией» я не имею в виду накладные расходы на вызов lambdas (что мало), но на сложный анализ и преобразование, которые выполняет современный JIT-компилятор при запуске кода.

  • Если вам нужен параллелизм, вероятно, гораздо быстрее и не намного сложнее использовать ExecutorService . Потоки являются как автоматическими (читайте: не знаете много о вашей проблеме), и используйте специализированную страtagsю распараллеливания (чтение: неэффективность для общего случая) ( рекурсивное разложение fork-join ).

  • Делает отладку более запутанной из-за иерархии вложенных вызовов и, бог, запрещает параллельное выполнение. Отладчик может иметь проблемы с отображением переменных из окружающего кода, и такие вещи, как пошаговое выполнение, могут работать не так, как ожидалось.

  • Потокам в целом сложнее кодировать, читать и отлаживать . На самом деле это относится к сложным « беглым » API в целом. Комбинация сложных одиночных утверждений, интенсивное использование дженериков и отсутствие промежуточных переменных заставляют создавать путаные сообщения об ошибках и отлаживать отладку. Вместо того, чтобы «этот метод не имеет перегрузки для типа X», вы получаете сообщение об ошибке ближе к «где-то вы испортили типы, но мы не знаем, где и как». Точно так же вы не можете проходить и анализировать вещи в отладчике так же легко, как когда код разбит на несколько операторов, а промежуточные значения сохраняются в переменных. Наконец, чтение кода и понимание типов и поведения на каждом этапе исполнения могут быть нетривиальными.

  • Выливается как больной палец . Язык Java уже имеет оператор for-each. Зачем заменить его вызовом функции? Зачем поощрять скрывать побочные эффекты где-то в выражениях? Зачем поощрять громоздкие однострочники? Смешение регулярных для каждого и нового forEach волей-неволей – плохой стиль. Кодекс должен говорить в идиомах (шаблоны, которые быстро осмысливаются из-за их повторения), и чем меньше идиом используется, тем понятнее код и меньше времени тратится на решение, какую идиому использовать (большой тайм-стоп для таких перфекционистов, как я! ).

Как вы можете видеть, я не большой поклонник forEach (), за исключением случаев, когда это имеет смысл.

Особенно оскорбительным для меня является тот факт, что Stream не реализует Iterable (несмотря на то, что на самом деле имеет iterator метода) и не может использоваться в for-each, только с forEach (). Я рекомендую передавать streamи в Iterables с помощью (Iterable)stream::iterator . Лучшей альтернативой является использование StreamEx, который исправляет ряд проблем Stream API, включая реализацию Iterable .

Тем не менее, forEach() полезен для следующего:

  • Атомно итерация по синхронизированному списку . До этого список, сгенерированный с помощью Collections.synchronizedList() был атомарным в отношении таких вещей, как get или set, но не был поточно-безопасным при повторе.

  • Параллельное выполнение (с использованием соответствующего параллельного streamа) . Это избавит вас от нескольких строк кода и использования ExecutorService, если ваша проблема соответствует предположениям производительности, встроенным в streamи и разделители.

  • Конкретные контейнеры, которые , подобно синхронизированному списку, выигрывают от контроля за итерацией (хотя это в значительной степени теоретическое, если только люди не могут привести больше примеров)

  • Вызов одной функции более чисто с помощью forEach() и аргумента ссылки метода (т. list.forEach (obj::someMethod) ). Однако имейте в виду пункты проверенных исключений, более сложную отладку и уменьшайте количество идиом, которые вы используете при написании кода.

Статьи, которые я использовал для справки:

  • Все о Java 8
  • Итерация внутри и снаружи (как указывает другой плакат)

РЕДАКТИРОВАТЬ: Похоже, что некоторые из первоначальных предложений для lambda (например, http://www.javac.info/closures-v06a.html ) решили некоторые из затронутых мной проблем (при этом, конечно, добавление их собственных осложнений).

При чтении этого вопроса можно получить впечатление, что Iterable#forEach в сочетании с lambda-выражениями является ярлыком / заменой для записи традиционного для каждого цикла. Это просто неправда. Этот код из OP:

 joins.forEach(join -> mIrc.join(mSession, join)); 

не является ярлыком для написания

 for (String join : joins) { mIrc.join(mSession, join); } 

и, безусловно, не следует использовать таким образом. Вместо этого он предназначен как ярлык (хотя это не совсем то же самое) для написания

 joins.forEach(new Consumer() { @Override public void accept(T join) { mIrc.join(mSession, join); } }); 

И это заменяет следующий код Java 7:

 final Consumer c = new Consumer() { @Override public void accept(T join) { mIrc.join(mSession, join); } }; for (T t : joins) { c.accept(t); } 

Замена тела цикла функциональным интерфейсом, как в приведенных выше примерах, делает ваш код более явным: вы говорите, что (1) тело цикла не влияет на окружающий код и stream управления, и (2) тело цикла может быть заменено другой реализацией функции, не затрагивая окружающий код. Невозможность доступа к не конечным переменным внешней области не является дефицитом функций / lambdas, это функция, которая отличает семантику Iterable#forEach от семантики традиционного для каждого цикла. Как только вы привыкнете к синтаксису Iterable#forEach , он делает код более читаемым, потому что вы сразу получаете эту дополнительную информацию о коде.

Традиционные для каждой петли, безусловно, останутся хорошей практикой (чтобы избежать злоупотребления термином « лучшая практика ») на Java. Но это не означает, что Iterable#forEach следует считать плохой практикой или плохим стилем. Всегда хорошая практика – использовать правильный инструмент для выполнения этой работы, и это включает в себя смешивание традиционных для каждого цикла с Iterable#forEach , где это имеет смысл.

Поскольку недостатки Iterable#forEach уже обсуждались в этом streamе, вот несколько причин, почему вы, вероятно, захотите использовать Iterable#forEach :

  • Чтобы сделать ваш код более явным: как описано выше, Iterable#forEach может сделать ваш код более явным и читаемым в некоторых ситуациях.

  • Чтобы сделать ваш код более расширяемым и поддерживаемым: использование функции как тела цикла позволяет заменить эту функцию различными реализациями (см. Шаблон страtagsи ). Например, вы можете легко заменить выражение lambda вызовом метода, который может быть перезаписан подclassами:

     joins.forEach(getJoinStrategy()); 

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

  • Чтобы сделать ваш код более отлажимым : отключение реализации цикла из декларации также облегчает отладку, поскольку вы можете иметь специальную реализацию отладки, которая выводит отладочные сообщения без необходимости загромождать ваш основной код с помощью if(DEBUG)System.out.println() . Реализация отладки может быть, например, делегатом , который украшает реализацию фактической функции.

  • Чтобы оптимизировать критически важный для производительности код: В отличие от некоторых утверждений в этом streamе, Iterable#forEach уже обеспечивает лучшую производительность, чем традиционный для каждого цикла, по крайней мере, при использовании ArrayList и запуска Hotspot в режиме «-клиент». Хотя это увеличение производительности мало и незначительно для большинства случаев использования, бывают ситуации, когда эта дополнительная производительность может иметь значение. Например, разработчики библиотек обязательно захотят оценить, если некоторые из существующих реализаций цикла должны быть заменены на Iterable#forEach .

    Чтобы подтвердить это утверждение фактами, я сделал несколько микро-тестов с помощью Caliper . Вот тестовый код (требуется последний калибр от git):

     @VmOptions("-server") public class Java8IterationBenchmarks { public static class TestObject { public int result; } public @Param({"100", "10000"}) int elementCount; ArrayList list; TestObject[] array; @BeforeExperiment public void setup(){ list = new ArrayList<>(elementCount); for (int i = 0; i < elementCount; i++) { list.add(new TestObject()); } array = list.toArray(new TestObject[list.size()]); } @Benchmark public void timeTraditionalForEach(int reps){ for (int i = 0; i < reps; i++) { for (TestObject t : list) { t.result++; } } return; } @Benchmark public void timeForEachAnonymousClass(int reps){ for (int i = 0; i < reps; i++) { list.forEach(new Consumer() { @Override public void accept(TestObject t) { t.result++; } }); } return; } @Benchmark public void timeForEachLambda(int reps){ for (int i = 0; i < reps; i++) { list.forEach(t -> t.result++); } return; } @Benchmark public void timeForEachOverArray(int reps){ for (int i = 0; i < reps; i++) { for (TestObject t : array) { t.result++; } } } } 

    И вот результаты:

    • Результаты для -клиента
    • Результаты для -server

    При работе с «-client» Iterable#forEach превосходит традиционный цикл for через ArrayList, но все еще медленнее, чем прямое повторение массива. При работе с «-сервером» производительность всех подходов примерно одинакова.

  • Предоставить дополнительную поддержку для параллельного выполнения: здесь уже сказано, что возможность выполнения функционального интерфейса Iterable#forEach параллельно с использованием streamов является, безусловно, важным аспектом. Поскольку Collection#parallelStream() не гарантирует, что цикл фактически выполняется параллельно, необходимо учитывать это необязательную функцию. Итерируя по списку с list.parallelStream().forEach(...); , вы явно говорите: этот цикл поддерживает параллельное выполнение, но он не зависит от него. Опять же, это особенность, а не дефицит!

    Перемещая решение для параллельного выполнения в сторону от вашей фактической реализации цикла, вы разрешаете опциональную оптимизацию своего кода, не затрагивая сам код, что хорошо. Кроме того, если реализация параллельного streamа по умолчанию не соответствует вашим потребностям, никто не мешает вам выполнять вашу собственную реализацию. Например, вы можете предоставить оптимизированную коллекцию в зависимости от базовой операционной системы, размера коллекции, количества ядер и некоторых настроек предпочтений:

     public abstract class MyOptimizedCollection implements Collection{ private enum OperatingSystem{ LINUX, WINDOWS, ANDROID } private OperatingSystem operatingSystem = OperatingSystem.WINDOWS; private int numberOfCores = Runtime.getRuntime().availableProcessors(); private Collection delegate; @Override public Stream parallelStream() { if (!System.getProperty("parallelSupport").equals("true")) { return this.delegate.stream(); } switch (operatingSystem) { case WINDOWS: if (numberOfCores > 3 && delegate.size() > 10000) { return this.delegate.parallelStream(); }else{ return this.delegate.stream(); } case LINUX: return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator()); case ANDROID: default: return this.delegate.stream(); } } } 

    Приятно, что ваша реализация цикла не должна знать или заботиться об этих деталях.

forEach() может быть реализована быстрее, чем для каждого цикла, потому что итеративный знает лучший способ повторить его элементы, в отличие от стандартного iteratorа. Таким образом, разница – это внутренняя или внутренняя петля.

Например, ArrayList.forEach(action) может быть просто реализовано как

 for(int i=0; i 

в отличие от каждого цикла, который требует много лесов

 Iterator iter = list.iterator(); while(iter.hasNext()) Object next = iter.next(); do something with `next` в Iterator iter = list.iterator(); while(iter.hasNext()) Object next = iter.next(); do something with `next` 

Однако нам также нужно учитывать две накладные расходы, используя forEach() , один из которых делает lambda-объект, а другой вызывает метод lambda. Они, вероятно, не значительны.

см. также http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ для сравнения внутренних / внешних итераций для разных вариантов использования.

TL; DR : List.stream().forEach() был самым быстрым.

Я чувствовал, что должен добавить свои результаты с эталонной оценки. Я сделал очень простой подход (без базовых показателей) и сравнил 5 различных методов:

  1. classический for
  2. classический foreach
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

процедура и параметры тестирования

 private List list; private final int size = 1_000_000; public MyClass(){ list = new ArrayList<>(); Random rand = new Random(); for (int i = 0; i < size; ++i) { list.add(rand.nextInt(size * 50)); } } private void doIt(Integer i) { i *= 2; //so it won't get JITed out } 

Список в этом classе должен быть повторен и иметь некоторый doIt(Integer i) применяемый ко всем его членам, каждый раз с помощью другого метода. в основном classе я запускаю проверенный метод три раза, чтобы разогреть JVM. Затем я запускаю метод тестирования 1000 раз, суммируя время, необходимое для каждого итерационного метода (используя System.nanoTime() ). После этого я делю эту сумму на 1000, и это результат, среднее время. пример:

 myClass.fored(); myClass.fored(); myClass.fored(); for (int i = 0; i < reps; ++i) { begin = System.nanoTime(); myClass.fored(); end = System.nanoTime(); nanoSum += end - begin; } System.out.println(nanoSum / reps); 

Я запускал это на i5 4-ядерном процессоре, с версией java версии 1.8.0_05

classический for

 for(int i = 0, l = list.size(); i < l; ++i) { doIt(list.get(i)); } 

время выполнения: 4,21 мс

classический foreach

 for(Integer i : list) { doIt(i); } 

время выполнения: 5,95 мс

List.forEach()

 list.forEach((i) -> doIt(i)); 

время выполнения: 3.11 мс

List.stream().forEach()

 list.stream().forEach((i) -> doIt(i)); 

время выполнения: 2,79 мс

List.parallelStream().forEach

 list.parallelStream().forEach((i) -> doIt(i)); 

время выполнения: 3,6 мс

Я чувствую, что мне нужно немного расширить свой комментарий …

О парадигме \ стиле

Это, наверное, самый заметный аспект. FP стал популярным благодаря тому, что вы можете избежать побочных эффектов. Я не буду углубляться в то, что профи \ минусы вы можете получить от этого, так как это не связано с вопросом.

Тем не менее, я скажу, что итерация с использованием Iterable.forEach вдохновлена ​​FP и, скорее, результатом приведения большего количества FP в Java (по иронии судьбы, я бы сказал, что для чистого EE в чистом FP ​​нет большого смысла, поскольку он ничего не делает, кроме введения побочные эффекты).

В конце я бы сказал, что это скорее вопрос вкуса \ стиль \ парадигма, который вы сейчас пишете.

О параллелизме.

С точки зрения производительности нет обещанных заметных преимуществ от использования Iterable.forEach over foreach (…).

Согласно официальным документам на Iterable.forEach :

Выполняет данное действие над содержимым Iterable, в элементах порядка происходит при итерации, пока все элементы не обработаны, или действие не выдает исключение.

… то есть документы в значительной степени ясно, что не будет подразумеваемого параллелизма. Добавление одного будет нарушением LSP.

Теперь есть «параллельные коллекции», которые обещаны в Java 8, но для работы с теми, которые вам нужны для меня более явными, и уделять особое внимание их использованию (см. Например, ответ mschenk74).

BTW: в этом случае Stream.forEach будет использоваться, и это не гарантирует, что фактическая работа будет выполнена в параллельном режиме (зависит от базовой коллекции).

ОБНОВЛЕНИЕ: может быть, это не так очевидно и немного растянуто с первого взгляда, но есть еще один аспект перспективы стиля и читаемости.

Прежде всего – простые старые ловушки простые и старые. Все уже знают их.

Во-вторых, и что более важно – вы, вероятно, захотите использовать Iterable.forEach только с однострочным lambdas. Если «тело» становится тяжелее – они, как правило, не являются такими читаемыми. У вас есть 2 варианта отсюда – используйте внутренние classы (yuck) или используйте обычный старый forloop. Люди часто раздражаются, когда видят одни и те же вещи (итератины над коллекциями), делаются различные варианты / стили в одной и той же кодовой базе, и это похоже на то, что происходит.

Опять же, это может быть или не быть проблемой. Зависит от людей, работающих с кодом.

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

Одним из возможных решений является замена терминала forEach на простой старый цикл foreach:

  Stream stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty()); Iterable iterable = stream::iterator; for (String s : iterable) { fileWriter.append(s); } 

Вот список наиболее популярных вопросов с другими обходными решениями по проверке обработки исключений в lambdas и streams:

Функция Java 8 Lambda, которая генерирует исключение?

Java 8: Лямбда-streamи, Фильтр по методу с исключением

Как я могу удалить CHECKED исключения из внутренних streamов Java 8?

Java 8: Обязательная проверка обработки исключений в lambda-выражениях. Почему обязательный, а не факультативный?

Преимущество Java 1.8 forEach method более 1.7 Enhanced for loop заключается в том, что при написании кода вы можете сосредоточиться только на бизнес-логике.

forEach принимает объект java.util.function.Consumer как аргумент, поэтому он помогает в том, чтобы наша бизнес-логика находилась в отдельном месте, которое вы можете повторно использовать в любое время.

Посмотрите ниже fragment,

  • Здесь я создал новый class, который переопределит метод classа accept из classа Consumer, где вы можете добавить дополнительную функциональность, Больше, чем Итерация .. !!!!!!

     class MyConsumer implements Consumer{ @Override public void accept(Integer o) { System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o); } } public class ForEachConsumer { public static void main(String[] args) { // Creating simple ArrayList. ArrayList aList = new ArrayList<>(); for(int i=1;i<=10;i++) aList.add(i); //Calling forEach with customized Iterator. MyConsumer consumer = new MyConsumer(); aList.forEach(consumer); // Using Lambda Expression for Consumer. (Functional Interface) Consumer lambda = (Integer o) ->{ System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o); }; aList.forEach(lambda); // Using Anonymous Inner Class. aList.forEach(new Consumer(){ @Override public void accept(Integer o) { System.out.println("Calling with Anonymous Inner Class "+o); } }); } } 

Here are some excerpts from the famous book, Effective Java (Third Edition) (Items 45 and 46). Indeed I would replace every loop of my code with forEach operation until I read these:

Overusing streams makes programs hard to read and maintain
When you start using streams, you may feel the urge to convert all your loops into streams, but resist the urge. While it may be possible, it will likely harm the readability and maintainability of your code base.

[…] The problem stems from the fact that this code is doing all its work in a terminal forEach operation, using a lambda that mutates external state […]. A forEach operation that does anything more than present the result of the computation performed by a stream is a “bad smell in code,” as is a lambda that mutates state.

Java programmers know how to use for-each loops, and the forEach terminal operation is similar. But the forEach operation is among the least powerful of the terminal operations and the least stream-friendly. It’s explicitly iterative, and hence not amenable to parallelization. The forEach operation should be used only to report the result of a stream computation, not to perform the computation. Occasionally, it makes sense to use forEach for some other purpose, such as adding the results of a stream computation to a preexisting collection.

  • Где GOTO: EOF возвращается?
  • В .NET, какой цикл работает быстрее, «for» или «foreach»?
  • Interesting Posts

    Настройка сети на Virtualbox Guest для доступа к VPN-хосту

    Как вы нарисуете линию в многозначной среде в R?

    Android onConfigurationChanged не вызывается

    Разница между строкой replace () и replaceAll ()

    Получение связанного списка в базе данных MySQL

    Удаление элементов из коллекции в java при итерации по ней

    максимальное значение целого числа

    Кросс-платформенный способ изменения приоритета Java-процесса

    Какой инструмент (Fedora) Linux можно использовать для просмотра моего рабочего стола в одной сети с моего ноутбука?

    Запускает ли Windows кэширование кэшей в режиме ожидания?

    Windows 8 быстро уменьшает громкость звука программы

    Преобразование списка кортежей в словарь

    Автоматическое увеличение в MongoDB для сохранения последовательности уникального идентификатора пользователя

    Как проверить, существует ли NSDate между двумя другими NSDates

    Полноэкранный просмотр без растягивания видео

    Давайте будем гением компьютера.