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
, которые можно "упростить" с помощью лямбд, но действительно ли есть какое-то преимущество в их использовании? Улучшается ли производительность и читаемость кода при их использовании?
ИЗМЕНЕНИЕ
Я также хотел бы расширить этот вопрос на более длинные методы. Я знаю, что вы не можете использовать return
или break
для выхода из родительской функции из лямбды, и это также стоит учитывать при сравнении, но есть ли что-то еще, что следует учитывать?
4 ответ(ов)
Преимущество проявляется, когда операции могут выполняться параллельно. (См. 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
, это уже не будет правдой.
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/ для сравнения внутренних и внешних итераций в различных сценариях.
TL;DR: List.stream().forEach()
оказался самым быстрым вариантом.
Я решил поделиться своими результатами бенчмаркинга различных методов итерации. Я использовал довольно простой подход (без фреймворков для бенчмаркинга) и протестировал 5 различных методов:
- Классический
for
- Классический
foreach
List.forEach()
List.stream().forEach()
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()
оказался самым эффективным методом.
Преимущество метода 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);
}
});
}
}
Этот подход позволяет вам удобно структурировать код, отделяя бизнес-логику от самой итерации, что делает его более читаемым и упрощает поддержку.
Java 8: Преобразование List<V> в Map<K, V>
Java 8: Уникальные элементы по свойству (Distinct by property)
Найти первый элемент по предикату
Есть ли краткий способ итерации по стриму с индексами в Java 8?
Почему не стоит использовать Optional в аргументах в Java 8?