Java 8 Iterable.forEach () против цикла foreach

466

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

Java 8:

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

Java 7:

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

У меня есть много циклов for, которые можно «упростить» с помощью лямбд, но есть ли у них преимущество? Будет ли это улучшить их производительность и удобочитаемость?

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

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

nebkat
источник
10
Там нет реального преимущества производительности одного над другим. Первый вариант - это что-то, вдохновленное FP (о котором обычно говорят как о более «хорошем» и «понятном» способе выражения вашего кода). На самом деле - это скорее «стилевой» вопрос.
Евгений Лой
5
@Dwb: в данном случае это не актуально. forEach не определяется как параллельный или что-то подобное, поэтому эти две вещи семантически эквивалентны. Конечно, можно реализовать параллельную версию forEach (и она может уже присутствовать в стандартной библиотеке), и в таком случае синтаксис лямбда-выражений будет очень полезен.
AardvarkSoup
8
@AardvarkSoup Экземпляр, для которого вызывается forEach, является потоком ( lambdadoc.net/api/java/util/stream/Stream.html ). Чтобы запросить параллельное выполнение, можно написать joins.parallel (). ForEach (...)
mschenk74
13
Это joins.forEach((join) -> mIrc.join(mSession, join));действительно "упрощение" for (String join : joins) { mIrc.join(mSession, join); }? Вы увеличили количество знаков препинания с 9 до 12, чтобы скрыть тип join. То, что вы действительно сделали, это поместили два утверждения в одну строку.
Том Хотин - tackline
7
Другим важным моментом является возможность захвата переменных в Java. С Stream.forEach () вы не можете обновлять локальные переменные, так как их захват делает их окончательными, что означает, что вы можете иметь поведение с состоянием в лямбде forEach (если вы не готовы к некоторым уродствам, таким как использование переменных состояния класса).
RedGlyph

Ответы:

512

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

  • Не могу использовать не финальные переменные . Таким образом, код, подобный следующему, не может быть превращен в лямбду forEach:

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

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

  • Может выполняться параллельно , что ужасно, ужасно для всех, кроме 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 ) решили некоторые из проблем, которые я упомянул (конечно, добавляя свои собственные сложности).

Александр Дубинский
источник
95
«Зачем поощрять скрывать побочные эффекты где-то в выражениях?» это неправильный вопрос. Функциональность forEachпредназначена для поощрения функционального стиля, то есть использования выражений без побочных эффектов. Если вы столкнетесь с ситуацией, когда forEachваши побочные эффекты не будут работать должным образом, у вас должно появиться ощущение, что вы не используете правильный инструмент для своей работы. Тогда простой ответ заключается в том, что ваше чувство верно, поэтому оставайтесь в цикле «для каждого». Классическая forпетля не стала устаревшей ...
Хольгер
17
@ Holger Как можно forEachиспользовать без побочных эффектов?
Александр Дубинский
14
Хорошо, я не был достаточно точен, forEachэто единственная потоковая операция, предназначенная для побочных эффектов, но это не для побочных эффектов, как ваш пример кода, подсчет является типичной reduceоперацией. Я бы предложил, как правило, сохранить каждую операцию, которая манипулирует локальными переменными или должна влиять на поток управления (включая обработку исключений) в классическом forцикле. Что касается исходного вопроса, я думаю, проблема связана с тем, что кто-то использует поток, где простой forцикл над источником потока будет достаточно. Используйте поток, где forEach()работает только
Holger
8
@Holger Что является примером побочных эффектов, forEachкоторые подойдут ?
Александр Дубинский
29
Что-то, что обрабатывает каждый элемент отдельно и не пытается изменить локальные переменные. Например, манипулирование самими элементами или их печать, запись / отправка их в файл, сетевой поток и т. Д. Для меня это не проблема, если вы подвергаете сомнению эти примеры и не видите никакого приложения для этого; фильтрация, отображение, уменьшение, поиск и (в меньшей степени) сбор являются предпочтительными операциями потока. ForEach выглядит для меня удобным для связи с существующими API. И для параллельных операций, конечно. Это не будет работать с forпетлями.
Хольгер
169

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

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

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

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

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

Примечание: для моего ответа я предполагал присоединиться к реализации java.util.Streamинтерфейса. Если в соединениях реализован только java.util.Iterableинтерфейс, это больше не так.

mschenk74
источник
4
На слайдах инженера-оракула, на которого он ссылается ( blogs.oracle.com/darcy/resource/Devoxx/… ), не упоминается параллелизм в этих лямбда-выражениях. Параллелизм может происходить в методах массового сбора, таких как map& fold, которые на самом деле не связаны с лямбдами.
Томас Юнгблут
1
На самом деле не кажется, что код OP выиграет от автоматического параллелизма здесь (тем более, что нет гарантии, что он будет). Мы на самом деле не знаем, что такое «mIrc», но «присоединение» на самом деле не похоже на то, что можно исключить из строя.
Евгений Лой
11
Stream#forEachи Iterable#forEachне одно и то же. ОП спрашивает о Iterable#forEach.
Гвласов
2
Я использовал стиль UPDATEX, поскольку в спецификации произошли изменения между моментом, когда был задан вопрос, и временем, когда ответ был обновлен. Без истории ответа это было бы еще более запутанным, как я думал.
mschenk74
1
Может ли кто-нибудь объяснить мне, почему этот ответ недействителен, если joinsон реализуется Iterableвместо Stream? Из нескольких вещей, которые я прочитал, OP должен быть в состоянии сделать, joins.stream().forEach((join) -> mIrc.join(mSession, join));и joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));если joinsреализуетIterable
Blueriver
112

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

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

это не предназначен в качестве ярлыка для записи

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

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

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

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

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

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

Традиционные циклы for-each, безусловно, останутся хорошей практикой (во избежание чрезмерного использования термина " best Practice ") в Java. Но это не значит, что это Iterable#forEachследует считать плохой практикой или плохим стилем. Это всегда хорошая практика, использовать правильный инструмент для выполнения работы, и это включает в себя смешивание традиционных для каждого цикла с Iterable#forEach, где это имеет смысл.

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

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

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

    joins.forEach(getJoinStrategy());

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

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

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

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

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> 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<TestObject>() {
                    @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++;
                }
            }
        }
    }

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

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

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

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

    public abstract class MyOptimizedCollection<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> 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();
            }
        }
    }

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

Бальдр
источник
5
У вас есть интересное мнение в этой дискуссии и поднять ряд вопросов. Я постараюсь обратиться к ним. Вы предлагаете переключаться между forEachи for-eachна основе некоторых критериев относительно природы тела цикла. Мудрость и дисциплина, чтобы следовать таким правилам, являются отличительной чертой хорошего программиста. Такие правила также являются его проклятием, потому что окружающие его люди либо не следуют им, либо не соглашаются. Например, используя проверенные и непроверенные исключения. Эта ситуация кажется еще более нюансированной. Но, если тело «не влияет на код окружения или управление потоком», не лучше ли выделить его как функцию?
Александр Дубинский
4
Спасибо за подробные комментарии Александр. But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?, Да, на мой взгляд, это часто будет так - выделение этих циклов как функций является естественным следствием.
Balder
2
Что касается проблемы с производительностью - я думаю, это очень сильно зависит от характера цикла. В проекте, над которым я работаю, я использовал циклы в стиле функций, подобные тем, что были Iterable#forEachдо Java 8, только из-за увеличения производительности. В рассматриваемом проекте есть один основной цикл, похожий на игровой цикл, с неопределенным числом вложенных подциклов, где клиенты могут подключать участников цикла как функции. Такая структура программного обеспечения значительно выигрывает от Iteable#forEach.
Балдер
7
В самом конце моей критики есть предложение: «Код должен говорить идиомами, и чем меньше идиом используется, тем яснее код и тем меньше времени тратится на решение, какую идиому использовать». Я начал глубоко ценить этот момент, когда я переключился с C # на Java.
Александр Дубинский
7
Это плохой аргумент. Вы можете использовать это, чтобы оправдать все, что вы хотите: почему вы не должны использовать цикл for, потому что цикл while достаточно хорош, и это на одну идиому меньше. Черт, зачем использовать любой цикл, переключатель или оператор try / catch, когда goto может сделать все это и даже больше.
Тапичу
13

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

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

for(int i=0; i<size; i++)
    action.accept(elements[i])

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

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

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

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

Zhongyu
источник
8
почему итератор знает лучший путь, а итератор - нет?
mschenk74
2
нет существенной разницы, но для соответствия интерфейсу итератора требуется дополнительный код, который может быть более дорогостоящим.
ZhongYu
1
@ zhong.j.yu если вы реализуете Collection, вы в любом случае также реализуете Iterable. Таким образом, нет никаких накладных расходов на код с точки зрения «добавления большего количества кода для реализации отсутствующих методов интерфейса», если это ваша точка зрения. Как сказал mschenk74, нет никаких причин, по которым вы не можете настроить свой итератор, чтобы знать, как перебрать вашу коллекцию наилучшим образом. Я согласен с тем, что на создание итераторов могут быть накладные расходы, но, если серьезно, эти вещи обычно настолько дешевы, что можно сказать, что они имеют нулевую стоимость ...
Юджин Лой
4
например, итерация дерева: void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}это более элегантно, чем внешняя итерация, и вы можете решить, как лучше синхронизировать
ratchet freak
1
Как ни странно, единственный комментарий в String.joinметодах (хорошо, неправильное соединение) - это «Количество элементов, которые вряд ли стоит накладных расходов Arrays.stream». поэтому они используют шикарный цикл.
Том Хотин - tackline
7

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

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

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

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

private List<Integer> 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
}

Список в этом классе должен быть повторен, и некоторые должны doIt(Integer i)применяться ко всем его членам, каждый раз с помощью другого метода. в Главном классе я запускаю проверенный метод три раза, чтобы разогреть 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);

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

классический for

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

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

классический 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 мс

Ассаф
источник
23
Как вы получаете эти цифры? Какие рамки для тестирования вы используете? Если вы не используете ничего и просто System.out.printlnпоказываете эти данные наивно, тогда все результаты бесполезны.
Луиджи Мендоса
2
Нет рамок. Я использую System.nanoTime(). Если вы прочитаете ответ, вы увидите, как это было сделано. Я не думаю, что это делает бесполезным видение, так как это относительный вопрос. Меня не волнует, насколько хорошо работает определенный метод, мне важно, насколько он эффективен по сравнению с другими методами.
Ассаф
31
И это цель хорошего микро-теста. Поскольку вы не отвечали таким требованиям, результаты бесполезны.
Луиджи Мендоса
6
Вместо этого я могу порекомендовать познакомиться с JMH, это то, что используется для самой Java и прилагает немало усилий для получения правильных чисел: openjdk.java.net/projects/code-tools/jmh
dsvensson
1
Я согласен с @LuiggiMendoza. Нет никакого способа узнать, что эти результаты являются последовательными или действительными. Бог знает, сколько тестов я выполнил, что постоянно сообщает о разных результатах, особенно в зависимости от порядка итераций, размера и того, что нет.
ммм
6

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

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

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

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

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

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

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

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

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

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

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

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

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

Прежде всего - старые добрые петли бывают простыми и старыми. Все уже знают их.

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

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

Евгений Лой
источник
1
Параллелизм не нуждается в новых «параллельных коллекциях». Это зависит только от того, запросили ли вы последовательный поток (используя collection.stream ()) или параллельный (используя collection.parallelStream ()).
Дж. Б. Низет
@JBNizet Согласно docs Collection.parallelStream () не гарантирует, что реализующая коллекция возвратит поток параллели. Мне действительно интересно, когда это может произойти, но, вероятно, это зависит от сбора.
Евгений Лой
согласовано. Это также зависит от коллекции. Но я хотел сказать, что параллельные циклы foreach уже были доступны со всеми стандартными коллекциями (ArrayList и т. Д.). Не нужно ждать «параллельных сборов».
Дж. Б. Низет
@JBNizet согласен с вашей точкой зрения, но это не совсем то, что я имел в виду под «параллельными коллекциями». Я ссылаюсь на Collection.parallelStream (), который был добавлен в Java 8 как «параллельные коллекции» по аналогии с концепцией Scala, которая делает то же самое. Кроме того, не уверен, как это называется в бите JSR, я видел несколько статей, которые используют ту же терминологию для этой функции Java 8.
Евгений Лой
1
для последнего абзаца вы можете использовать ссылку на функцию:collection.forEach(MyClass::loopBody);
Урод, урод
6

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

Один из возможных способов - заменить терминал forEachпростым старым циклом foreach:

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

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

Java 8 Лямбда-функция, которая выдает исключение?

Java 8: лямбда-потоки, фильтрация по методам с исключением

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

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

Vadzim
источник
3

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

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

Посмотрите на ниже фрагмент,

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

    class MyConsumer implements Consumer<Integer>{
    
        @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<Integer> 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<Integer> 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<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
Хардик Патель
источник