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

java for-loop java-8 java-stream

330237 просмотра

8 ответа

4022 Репутация автора

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

Java 8:

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

Java 7:

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

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

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

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

Автор: nebkat Источник Размещён: 19.05.2013 01:57

Ответы (8)


169 плюса

3299 Репутация автора

Преимущество учитывается, когда операции могут выполняться параллельно. (См. 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 Размещён: 19.05.2013 02:05

5 плюса

9435 Репутация автора

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

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

Это, наверное, самый заметный аспект. 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. Людей часто раздражает, когда они видят, что одни и те же вещи (итерации над коллекциями) выполняются различными способами / стилями в одной и той же кодовой базе, и, похоже, это так.

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

Автор: Eugene Loy Размещён: 19.05.2013 04:31

13 плюса

16047 Репутация автора

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 Размещён: 19.05.2013 06:00

461 плюса

14167 Репутация автора

Решение

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

Автор: Aleksandr Dubinsky Размещён: 24.11.2013 04:45

100 плюса

6952 Репутация автора

При чтении этого вопроса у вас может сложиться впечатление, что 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, безусловно, останутся хорошей практикой (во избежание злоупотребления термином « лучшая практика ») в 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();
            }
        }
    }
    

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

Автор: Balder Размещён: 19.03.2014 10:02

9 плюса

1140 Репутация автора

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 мс

Автор: Assaf Размещён: 15.09.2014 07:39

5 плюса

16234 Репутация автора

Одно из ограничений наиболее функционального функционала 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 Размещён: 29.07.2015 05:55

3 плюса

431 Репутация автора

Преимущество метода 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);
                }
            });
        }
    }
    
Автор: Hardik Patel Размещён: 23.12.2017 02:59
Вопросы из категории :
32x32