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


464

Що з переліченого є кращою практикою в Java 8?

Java 8:

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

Java 7:

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

У мене є безліч циклів, які можна "спростити" лямбдами, але чи справді є якась перевага їх використання? Чи покращило б це їх ефективність та читабельність?

EDIT

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


10
Немає реальної переваги у виконанні одного над іншим. Перший варіант - це щось, натхнене FP (про яке зазвичай говорять як про "приємніший" та "зрозумілий" спосіб висловити свій код). Насправді - це скоріше "стильове" питання.
Євген Лой

5
@Dwb: у цьому випадку це не стосується. forEach не визначається як паралельний або щось подібне, тому ці дві речі є семантично рівнозначними. Звичайно, можливо реалізувати паралельну версію forEach (і вона вже може бути присутнім у стандартній бібліотеці), і в такому випадку синтаксис вираження лямбда буде дуже корисним.
AardvarkSoup

8
@AardvarkSoup Екземпляр, за яким викликається forEach, є Stream ( 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. Що ви насправді зробили - це поставити два твердження на один рядок.
Том Хотін - тайклін

7
Ще один момент, який слід врахувати, - це обмежена здатність Java для зйомки змінної. За допомогою Stream.forEach () ви не можете оновити локальні змінні, оскільки їх захоплення робить їх остаточними, а це означає, що ви можете мати поведінку в лямбдах forEach (якщо ви не готові до деякої потворності, наприклад використання змінних стану класу).
RedGlyph

Відповіді:


510

Кращою практикою є використання 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 () + лямбда в тій же мірі, як звичайні петлі, особливо зараз, коли лямбди є новими. Під оптимізацією я маю на увазі не накладні витрати на виклик лямбда (що мало), а на складний аналіз та перетворення, які виконує сучасний компілятор JIT на запущеному коді.

  • Якщо вам потрібен паралелізм, користуватися ExecutorService, можливо, набагато швидше і не набагато складніше . Потоки є одночасно автоматичними (читайте: не знаю багато про вашу проблему) і використовують спеціалізовану (читайте: неефективну для загального випадку) паралелізаційну стратегію ( рекурсивне розкладання вил-приєднання ).

  • Робить налагодження більш заплутаним через вкладену ієрархію викликів і, не дай бог, паралельне виконання. У відладчику можуть виникнути проблеми із відображенням змінних із оточуючого коду, і такі речі, як покрокове проходження, можуть працювати не так, як очікувалося.

  • Потоки загалом складніше кодувати, читати та налагоджувати . Насправді це стосується складних « вільних » API в цілому. Поєднання складних одиночних висловлювань, важке використання генеричних даних та відсутність проміжних змінних змовляють створювати заплутані повідомлення про помилки та фрустрацію налагодження. Замість того, щоб "у цього методу немає перевантаження для типу X", ви отримуєте повідомлення про помилку ближче до "десь ви переплутали типи, але ми не знаємо, де і як". Аналогічно, ви не можете переступити і вивчити речі в налагоджувачі так легко, як коли код розбитий на кілька операторів, а проміжні значення зберігаються у змінних. Нарешті, читання коду та розуміння типів та поведінки на кожному етапі виконання може бути нетривіальним.

  • Стирчить, як біль у великому пальці . Мова Java вже має твердження для кожного. Чому замінити його на виклик функції? Навіщо заохочувати ховати побічні ефекти десь у виразах? Навіщо заохочувати недбалі однолінійки? Регулярне змішування для кожного та нового для кожного волі-неволі - це поганий стиль. Код повинен говорити в ідіомах (шаблони, які швидко зрозуміти через їх повторення), і чим менше ідіом використовується, тим чіткіший код, і менше часу витрачається на вирішення, яку ідіому використовувати (великий витрата часу на перфекціоністів, як я! ).

Як бачите, я не є великим шанувальником forEach (), за винятком випадків, коли це має сенс.

Особливо образливим для мене є той факт, що Streamвін не реалізує Iterable(незважаючи на те, що він фактично має метод iterator) і не може бути використаний для кожного, лише за допомогою forEach (). Я рекомендую вводити потоки в Iterables за допомогою (Iterable<T>)stream::iterator. Кращою альтернативою є використання StreamEx, який виправляє ряд проблем Stream API, включаючи реалізацію Iterable.

Це forEach()було корисно для наступного:

  • Атомічно ітерація над синхронізованим списком . До цього список, створений за допомогою, Collections.synchronizedList()був атомарним щодо таких речей, як get or set, але не був безпечним для потоків під час ітерації.

  • Паралельне виконання (з використанням відповідного паралельного потоку) . Це заощаджує вам кілька рядків коду порівняно з використанням ExecutorService, якщо ваша проблема відповідає припущенням щодо продуктивності, вбудованим у потоки та розповсюджувачі.

  • Конкретні контейнери, які , як і синхронізований список, виграють від контролю над ітерацією (хоча це в основному теоретично, якщо люди не можуть навести більше прикладів)

  • Більш чіткий виклик однієї функції за допомогою forEach()та аргументації методу (тобто list.forEach (obj::someMethod)). Однак майте на увазі бали за перевіреними винятками, складнішою налагодженням та зменшенням кількості ідіом, які ви використовуєте під час написання коду.

Статті, які я використав для довідок:

EDIT: Схоже, деякі оригінальні пропозиції щодо лямбда (наприклад, http://www.javac.info/closures-v06a.html ) вирішили деякі згадані мною проблеми (додаючи, звичайно, власні ускладнення).


95
"Навіщо заохочувати ховати побічні ефекти десь у виразах?" неправильне питання. Функціонал forEachіснує для заохочення функціонального стилю, тобто використання виразів без побічних ефектів. Якщо ви зіткнулися з ситуацією, ситуація forEachне справляється зі своїми побічними ефектами, ви повинні відчути, що ви не використовуєте правильний інструмент для роботи. Тоді проста відповідь, це тому, що ваше почуття правильне, тому для цього залишайтеся на кожному циклі. Класична forпетля не застаріла ...
Холгер

17
@Holger Як можна forEachзастосовувати без побічних ефектів?
Олександр Дубінський

14
Гаразд, я був недостатньо точним, forEachце єдиний потік, призначений для побічних ефектів, але це не для побічних ефектів, як ваш приклад код, підрахунок є типовою reduceоперацією. Як правило, я б запропонував тримати кожну операцію, яка маніпулює локальними змінними або впливатиме на керуючий потік (включаючи обробку винятків) у класичному forциклі. Щодо початкового питання, я думаю, проблема випливає з того, що хтось використовує потік, де достатньо простого forциклу над джерелом потоку. Використовуйте потік, де forEach()працює лише
Холгер

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інтерфейс, це вже не відповідає дійсності.


4
Слайди інженера-оракула, на які він посилається ( blogs.oracle.com/darcy/resource/Devoxx/… ), не згадують про паралелізм у цих лямбдаських виразах. Паралелізм може виникати в рамках методів масового збору, таких як map& fold, які насправді не пов'язані з лямбдами.
Томас Юнгблут

1
Насправді не представляється, що код ОП виграє від автоматичного паралелізму тут (тим більше, що немає гарантії, що його буде). Ми насправді не знаємо, що таке "mIrc", але "приєднатися" насправді не здається чимось, що може бути погашено поза межами порядку.
Євген Лой

10
Stream#forEachі Iterable#forEachце не одне і те ж. ОП просить о Iterable#forEach.
gvlasov

2
Я використовував стиль UPDATEX, оскільки змінилися в специфікації між часом запитання і часом оновлення відповіді. Без історії відповіді це було б ще більш заплутаним, я думав.
mschenk74

1
Чи не може хто-небудь пояснити мені, чому ця відповідь не є дійсною, якщо joinsвона реалізується Iterableзамість Stream? З кількох речей, які я прочитав, ОП повинні бути в змозі зробити, joins.stream().forEach((join) -> mIrc.join(mSession, join));і joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));якщо joinsреалізаціяIterable
Blueriver

112

Читаючи це запитання, можна Iterable#forEachскласти враження, що в поєднанні з лямбда-виразами це ярлик / заміна для написання традиційного для кожного циклу. Це просто неправда. Цей код з ОП:

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, він зробить код більш читабельним, оскільки ви одразу отримуєте цю додаткову інформацію про код.

Традиційні для кожного циклу, безумовно, залишаться хорошою практикою (щоб уникнути зайвого використання терміну " найкраща практика ") на Java. Але це не означає, що це Iterable#forEachслід вважати поганою практикою або поганим стилем. Завжди є хорошою практикою використовувати правильний інструмент для виконання роботи, і це включає змішування традиційних для кожного циклу Iterable#forEach, де це має сенс.

Оскільки Iterable#forEachв цій темі вже обговорювались мінуси , ось чому, можливо, ви хочете скористатися Iterable#forEach:

  • Щоб зробити ваш код більш явним: Як описано вище, ви Iterable#forEach можете зробити свій код більш явним і читабельним у деяких ситуаціях.

  • Щоб зробити свій код більш розширюваним та підтримуваним: Використання функції в якості тіла циклу дозволяє замінити цю функцію різними реалізаціями (див. Стратегічний шаблон ). Ви можете, наприклад, легко замінити лямбда-вираз викликом методу, який може бути перезаписаний підкласами:

    joins.forEach(getJoinStrategy());

    Тоді ви можете надати стратегії за замовчуванням, використовуючи enum, який реалізує функціональний інтерфейс. Це не тільки робить ваш код більш розширюваним, але також збільшує ремонтопридатність, оскільки він роз’єднує реалізацію циклу від оголошення циклу.

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

  • Оптимізація критичного для продуктивності коду: На відміну від деяких тверджень у цій темі, Iterable#forEach вона вже забезпечує кращу ефективність, ніж традиційна для кожного циклу, принаймні при використанні ArrayList та запуску точки доступу в режимі "-client". Незважаючи на те, що цей приріст ефективності невеликий і незначний для більшості випадків використання, є ситуації, коли ця додаткова ефективність може змінити значення. Наприклад, керівники бібліотеки, безумовно, захочуть оцінити, чи потрібно замінити деякі існуючі варіанти циклу Iterable#forEach.

    Щоб підкріпити це твердження фактами, я зробив декілька мікро-показників з 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перевершує традиційний цикл для 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?. Так, на мою думку, це часто трапляється, - розрізнення цих циклів як функцій є природним наслідком.
Бальдер

2
Щодо питання продуктивності - я думаю, це дуже залежить від характеру циклу. У проекті, над яким я працюю, я використовував петлі у стилі функції, подібні до Iterable#forEachJava 8, лише через підвищення продуктивності. Розглянутий проект має один основний цикл, подібний до ігрового циклу, із невизначеною кількістю вкладених під-циклів, де клієнти можуть підключати учасників циклу як функції. Така структура програмного забезпечення значно виграє Iteable#forEach.
Бальдер

7
В самому кінці моєї критики є речення: "Код повинен говорити в фразеологіях, і чим менше ідіом буде використано, тим чіткіший код, і менше часу витрачається на рішення, яку ідіому використовувати". Я почав глибоко цінувати цей момент, коли перейшов з C # на Java.
Олександр Дубінський,

7
Це поганий аргумент. Ви можете використовувати його для виправдання всього, що ви хочете: чому ви не повинні використовувати цикл, тому що цикл у той час є досить хорошим, і це одна ідіома, що менше. Чорт забирай, навіщо використовувати будь-який цикл, перемикач або спробувати / 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/ для порівняння внутрішніх / зовнішніх ітерацій для різних випадків використання.


8
чому ітерабельний спосіб знає найкращий спосіб, але ітератор цього не робить?
mschenk74

2
суттєвої різниці немає, але додатковий код не потрібен, щоб відповідати інтерфейсу ітератора, що може бути дорожче.
ЧжунЮ

1
@ zhong.j.yu, якщо ви реалізуєте Collection, ви все одно реалізуєте Iterable. Отже, немає коду накладних витрат з точки зору "додавання більше коду для реалізації відсутніх методів інтерфейсу", якщо це ваша думка. Як говорив mschenk74, схоже, немає причин, чому ви не можете налаштувати ітератор, щоб знати, як найкращим чином переглядати колекцію. Я погоджуюся, що для створення ітератора можуть бути накладні витрати, але, якщо серйозно, то це зазвичай так дешево, що можна сказати, що вони мають нульову вартість ...
Євген Лой

4
наприклад, ітерація дерева:, void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}це більш елегантно, ніж зовнішня ітерація, і ви можете вирішити, як найкраще синхронізуватися
храповик виродка

1
Як не дивно, єдиний коментар у String.joinметодах (нормально, неправильне з'єднання) - "Кількість елементів, мабуть, не варто Arrays.stream накладні" тому вони використовують шипучку для циклу.
Том Хотін - тайклін

7

TL; DR : List.stream().forEach()був найшвидшим.

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

  1. класичний for
  2. класичний форекс
  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);

Я керував цим на i5 4-ядерному процесорі, з версією java 1.8.0_05

класичний for

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

час виконання: 4,21 мс

класичний форекс

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

Я відчуваю, що мені потрібно трохи розширити свій коментар ...

Про парадигму \ стиль

Це, мабуть, найпомітніший аспект. ПП стала популярною завдяки тому, що ви можете уникати побічних ефектів. Я не буду заглиблюватися в глиб того, які плюси та мінуси ви можете отримати від цього, оскільки це не пов'язане з питанням.

Однак я скажу, що ітерація за допомогою Iterable.forEach натхненна FP, а скоріше результатом приведення більшої кількості FP на Java (за іронією долі, я б сказав, що ForEach багато не використовує в чистому FP, оскільки це не робить нічого, крім введення побічні ефекти).

Наприкінці я б сказав, що це скоріше питання смаку \ стилю \ парадигми, про який ви зараз пишете.

Про паралелізм.

З точки зору продуктивності, немає обіцяних помітних переваг від використання Iterable.forEach над foreach (...).

Згідно з офіційними документами на Iterable.forEach :

Виконує задану дію над вмістом Iterable, в порядку порядку виникають під час ітерації, поки всі елементи не будуть оброблені або дія не кине виняток.

... тобто в Документах ясна річ, що не буде явного паралелізму. Додавання цього було б порушенням LSP.

Тепер є "колекції паралельних", які обіцяні в Java 8, але для роботи з тими, хто вам потрібен, явніше і вкладіть додатковий догляд, щоб їх використовувати (див. Відповідь mschenk74, наприклад).

BTW: у цьому випадку буде використовуватися Stream.forEach , і це не гарантує, що фактична робота буде виконуватися паралельно (залежить від базової колекції).

ОНОВЛЕННЯ: може бути не настільки очевидним і трохи розтягнутим на перший погляд, але є ще одна грань стилю та перспективності читання.

Перш за все - звичайні старі форліпи - це звичайні та старі. Усі вже їх знають.

По-друге, і що важливіше - ви, мабуть, хочете використовувати Iterable.forБудьте лише з однолінійними лямбдами. Якщо "тіло" стає важчим - вони, як правило, не такі, що читаються. Тут у вас є два варіанти - використовувати внутрішні класи (yuck) або використовувати звичайний старий forloop. Люди часто дратуються, коли вони бачать одні і ті ж речі (ітеративи над колекціями), які робляться різними ваями / стилями в одній і тій же кодовій базі, і це, мабуть, так.

Знову ж таки, це може бути або не бути проблемою. Залежить від людей, які працюють над кодом.


1
Паралелізм не потребує нових "паралельних колекцій". Це просто залежить від того, запитали ви послідовний потік (використовуючи collection.stream ()) або паралельний (використовуючи collection.parallelStream ()).
JB Nizet

@JBNizet Згідно з документами Collection.parallelStream () не гарантує, що реалізація колекції поверне паралельний потік. Я, власне, задаюся питанням, коли це може статися, але, ймовірно, це залежить від колекції.
Євген Лой

домовились. Це також залежить від колекції. Але моя думка полягала в тому, що паралельні петлі передбачення вже були доступні у всіх стандартних колекціях (ArrayList тощо). Не потрібно чекати "паралельних колекцій".
JB Nizet

@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 Lambda, яка кидає виняток?

Java 8: лямбда-потоки, фільтр методом за винятком

Як я можу викидати ПРОВЕРЕНІ винятки з потоків Java 8?

Java 8: Обов’язкові перевірки винятків, оброблені в лямбда-виразах. Чому обов'язкові, а не необов’язкові?


3

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

Метод forEach приймає об’єкт java.util.function.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);
                }
            });
        }
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.