5

Java 8 Iterable.forEach() против цикла foreach: что выбрать?

16

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

Java 8:

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

Java 7:

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

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

ИЗМЕНЕНИЕ

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

4 ответ(ов)

1

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

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

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

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

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

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

0

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

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

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

в то время как цикл for-each требует гораздо большего количества вспомогательного кода:

Iterator iter = list.iterator();
while(iter.hasNext()) {
    Object next = iter.next();
    // делаем что-то с `next`
}

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

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

0

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; // чтобы это не оптимизировалось компилятором
}

Список в этом классе будет итерироваться, и к каждому элементу будет применяться метод 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);

Я провел тестирование на процессоре i5 с 4 ядрами, с версией 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 мс

Таким образом, исходя из полученных данных, List.stream().forEach() оказался самым эффективным методом.

0

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

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

Посмотрите на приведённый ниже фрагмент кода:

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

    @Override
    public void accept(Integer o) {
        System.out.println("Здесь вы также можете добавить свою бизнес-логику, которая будет работать с итерацией, и вы сможете переиспользовать её." + o);
    }
}

public class ForEachConsumer {

    public static void main(String[] args) {

        // Создаём простой ArrayList.
        ArrayList<Integer> aList = new ArrayList<>();
        for (int i = 1; i <= 10; i++) aList.add(i);

        // Вызов forEach с кастомизированным итератором.
        MyConsumer consumer = new MyConsumer();
        aList.forEach(consumer);

        // Использование лямбда-выражения для Consumer. (Функциональный интерфейс)
        Consumer<Integer> lambda = (Integer o) -> {
            System.out.println("Используя лямбда-выражение для итерации и выполнения чего-то еще (BI).. " + o);
        };
        aList.forEach(lambda);

        // Использование анонимного внутреннего класса.
        aList.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer o) {
                System.out.println("Вызов с анонимным внутренним классом " + o);
            }
        });
    }
}

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

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь