it-swarm.com.ru

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

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

Java 8:

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

Java 7:

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

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

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

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

421
nebkat

Лучше всего использовать 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()

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

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

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

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

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

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

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

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

Особенно оскорбительным для меня является тот факт, что Stream не реализует Iterable (несмотря на то, что фактически имеет метод iterator) и не может использоваться в for-each, только с 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 ) решил некоторые из упомянутых мной проблем (добавив, конечно, свои сложности).

452
Aleksandr Dubinsky

Преимущество учитывается, когда операции могут выполняться параллельно. (См. 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, это больше не так.

168
mschenk74

При чтении этого вопроса у вас может сложиться впечатление, что 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 от семантика традиционной для каждого цикла. Как только человек привыкает к синтаксису Iterable#forEach, он делает код более читабельным, потому что вы сразу получаете эту дополнительную информацию о коде.

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

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

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

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

    joins.forEach(getJoinStrategy());
    

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

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

  • Чтобы оптимизировать критичный для производительности код: Вопреки некоторым утверждениям в этой теме, Iterable#forEach уже делает (---) обеспечить лучшую производительность, чем традиционный цикл for-each, по крайней мере при использовании 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();
            }
        }
    }
    

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

99
Balder

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

Например, 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/ для сравнения внутренних и внешних итераций для различных вариантов использования.

12
ZhongYu

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

8
Assaf

Одним из самых приятных функциональных ограничений 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: Обязательная проверка проверенных исключений в лямбда-выражениях. Почему обязательно, а не необязательно?

4
Vadzim

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4
Eugene Loy

Преимущество метода 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);
                }
            });
        }
    }
    
2
Hardik Patel