Java 8 Itérable.forEach () vs boucle foreach


Lequel des éléments suivants est la meilleure pratique en Java 8?

Java 8:

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

Java 7:

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

J'ai beaucoup de boucles for qui pourraient être "simplifiées" avec les lambdas, mais y a-t-il vraiment un avantage à les utiliser? Cela améliorerait-il leurs performances et leur lisibilité?

MODIFIER

Je vais également étendre cette question à des méthodes plus longues. Je sais que vous ne pouvez pas retourner ou casser la fonction parent d'un lambda et cela devrait également être pris en compte considération lors de la comparaison, mais y a-t-il autre chose à considérer?

Author: Rafael, 2013-05-19

8 answers

L'avantage entre en compte lorsque les opérations peuvent être exécutées en parallèle. (Voir http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and {[5] } - la section sur l'itération interne et externe)

  • Le principal avantage de mon point de vue est que l'implémentation de ce qui doit être fait dans la boucle peut être définie sans avoir à décider si elle sera exécutée en parallèle ou séquentielle

  • Si vous voulez que votre boucle soit exécutée dans parallèlement, vous pouvez simplement écrire

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

    Vous devrez écrire du code supplémentaire pour la gestion des threads, etc.

Remarque: Pour ma réponse, j'ai supposé des jointures implémentant l'interface java.util.Stream. Si joins implémente uniquement l'interface java.util.Iterable, ce n'est plus vrai.

 157
Author: mschenk74, 2018-06-29 07:40:06

, La meilleure pratique consiste à utiliser les for-each. En plus de violer le Keep It Simple, Stupid principe, le new-fangled forEach() a au moins les lacunes suivantes:

  • Impossible d'utiliser des variables non finales. Donc, le code comme le suivant ne peut pas être transformé en un forEach lambda:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • Impossible de gérer les exceptions vérifiées . Les lambdas ne sont pas réellement interdits de lancer des exceptions vérifiées, mais fonctionnels communs les interfaces comme Consumer n'en déclarent pas. Par conséquent, tout code qui lève des exceptions vérifiées doit les envelopper dans try-catch ou Throwables.propagate(). Mais même si vous faites cela, ce qui arrive à l'exception levée n'est pas toujours clair. Il pourrait être avalé quelque part dans les entrailles de forEach()

  • Contrôle de flux limité . Un return dans un lambda est égal à un continue dans un chacun, mais il n'y a pas d'équivalent à break. Il est également difficile de faire des choses comme des valeurs de retour, un court-circuit ou set flags (ce qui aurait un peu allégé les choses, si ce n'était pas une violation de la règle no non-final variables). "Ce n'est pas seulement une optimisation, mais critique quand on considère que certaines séquences (comme la lecture des lignes dans un fichier) peuvent avoir des effets secondaires, ou vous pouvez avoir une séquence infinie."

  • Pourrait s'exécuter en parallèle , ce qui est une chose horrible, horrible pour tout le monde, sauf le 0.1% de votre code qui doit être optimisé. Tout parallèle le code doit être réfléchi (même s'il n'utilise pas de verrous, de volatiles et d'autres aspects particulièrement désagréables de l'exécution multithread traditionnelle). Tout bug sera difficile à trouver.

  • Peut nuire aux performances , car le JIT ne peut pas optimiser forEach()+lambda dans la même mesure que les boucles simples, surtout maintenant que les lambdas sont nouveaux. Par" optimisation", je ne veux pas dire la surcharge de l'appel lambdas (qui est petit), mais à l'analyse et à la transformation sophistiquées que le compilateur JIT moderne effectue sur le code en cours d'exécution.

  • Si vous avez besoin de parallélisme, il est probablement beaucoup plus rapide et pas beaucoup plus difficile à utiliser un ExecutorService. Les flux sont à la fois automagiques (lire: je ne sais pas grand-chose sur votre problème) et utilisent une stratégie de parallélisation spécialisée (lire: inefficace pour le cas général) (décomposition récursive fork-join).

  • Rend le débogage plus de confusion, parce que de la hiérarchie des appels imbriqués et, à dieu ne plaise, de l'exécution parallèle. Le débogueur peut avoir des problèmes pour afficher les variables du code environnant, et des choses comme step-through peuvent ne pas fonctionner comme prévu.

  • Les flux en général sont plus difficiles à coder, lire et déboguer . En fait, cela est vrai des API complexes "fluent" en général. La combinaison d'énoncés simples complexes, l'utilisation intensive de génériques et l'absence de variables intermédiaires conspirent pour produire messages d'erreur confus et frustrer le débogage. Au lieu de "cette méthode n'a pas de surcharge pour le type X", vous obtenez un message d'erreur plus proche de " quelque part, vous avez foiré les types, mais nous ne savons pas où ni comment."De même, vous ne pouvez pas parcourir et examiner les choses dans un débogueur aussi facilement que lorsque le code est divisé en plusieurs instructions et que les valeurs intermédiaires sont enregistrées dans des variables. Enfin, lire le code et comprendre les types et le comportement à chaque étape de l'exécution peut être non-trivial.

  • Colle dehors comme un pouce endolori. Le langage Java a déjà l'instruction for-each. Pourquoi le remplacer par un appel de fonction? Pourquoi encourager à cacher les effets secondaires quelque part dans les expressions? Pourquoi encourager les one-liners difficiles à manier? Mélange régulier pour-chaque et nouveau forEach bon gré mal gré est mauvais style. Le code doit parler en idiomes (modèles rapides à comprendre en raison de leur répétition), et moins il y a d'idiomes utilisés, plus le code est clair et moins le temps est long passé à décider quel idiome utiliser (un gros drain pour les perfectionnistes comme moi!).

Comme vous pouvez le voir, je ne suis pas un grand fan de forEach() sauf dans les cas où cela a du sens.

Particulièrement offensant pour moi est le fait que Stream n'implémente pas Iterable (malgré la méthode iterator) et ne peut pas être utilisé dans un for-each, seulement avec un forEach(). Je recommande de lancer des flux en Itérables avec (Iterable<T>)stream::iterator. Une meilleure alternative consiste à utiliser StreamEx qui corrige un certain nombre de problèmes d'API de flux, y compris l'implémentation de Iterable.

Cela dit, forEach() est utile pour ce qui suit:

  • Itération atomique sur une liste synchronisée. Avant cela, une liste générée avec Collections.synchronizedList() était atomique par rapport à des choses comme get ou set, mais n'était pas thread-safe lors de l'itération.

  • Exécution parallèle (en utilisant un flux parallèle approprié). Cela vous permet d'économiser quelques lignes de code par rapport à l'utilisation d'un ExecutorService, si votre problème correspond aux hypothèses de performance intégrées dans les flux et les Spliterators.

  • Les conteneurs spécifiques qui , comme la liste synchronisée, bénéficient du contrôle de l'itération (bien que cela soit en grande partie théorique, sauf si les gens peuvent apporter plus d'exemples)

  • Appeler une seule fonction plus proprement en utilisant forEach() et un argument de référence de méthode (c'est-à-dire list.forEach (obj::someMethod)). Cependant, gardez à l'esprit les points sur exceptions vérifiées, débogage plus difficile et réduction du nombre d'idiomes que vous utilisez lors de l'écriture de code.

Articles que j'ai utilisés pour référence:

EDIT: Ressemble à certaines des propositions originales pour les lambdas (telles que http://www.javac.info/closures-v06a.html ) a résolu certains des problèmes que j'ai mentionnés (tout en ajoutant leurs propres complications, bien sûr).

 401
Author: Aleksandr Dubinsky, 2017-04-12 07:32:04

En lisant cette question, on peut avoir l'impression que Iterable#forEach en combinaison avec des expressions lambda est un raccourci/remplacement pour écrire une boucle traditionnelle pour chaque boucle. Ce n'est tout simplement pas vrai. Ce code de l'OP:

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

Est pas conçu comme un raccourci pour écrire

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

Et ne devrait certainement pas être utilisé de cette manière. Au lieu de cela, il est conçu comme un raccourci (bien que ce soit pas exactement la même chose) pour écrire

joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

Et il est en remplacement du code Java 7 suivant:

final Consumer<T> c = new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
};
for (T t : joins) {
    c.accept(t);
}

Remplacer le corps d'une boucle par une interface fonctionnelle, comme dans les exemples ci-dessus, rend votre code plus explicite: Vous dites que (1) le corps de la boucle n'affecte pas le code environnant et le flux de contrôle, et (2) le corps de la boucle peut être remplacé par une implémentation différente de la fonction, sans affecter le code environnant. Ne pas pouvoir accéder à des variables non définitives de la portée extérieure n'est pas un déficit de fonctions / lambdas, c'est une caractéristique qui distingue la sémantique de Iterable#forEach de la sémantique d'une boucle for-each traditionnelle. Une fois que l'on s'habitue à la syntaxe de Iterable#forEach, cela rend le code plus lisible, car vous obtenez immédiatement ces informations supplémentaires sur le code.

Les boucles for-each traditionnelles resteront certainement bonne pratique (pour éviter le terme galvaudé " meilleure pratique") en Java. Mais cela ne signifie pas que {[7] } devrait être considéré mauvaise pratique ou mauvais style. C'est toujours une bonne pratique, d'utiliser le bon outil pour faire le travail, et cela inclut le mélange des boucles traditionnelles for-each avec Iterable#forEach, où cela a du sens.

Puisque les inconvénients de Iterable#forEach ont déjà été discutés dans ce fil, voici quelques raisons pour lesquelles vous voudrez probablement utiliser Iterable#forEach:

  • Afin De rendre votre code plus explicite: tel Que décrit ci-dessus, Iterable#forEach peut - rendre votre code plus explicite et lisible dans certains situation.

  • Pour rendre votre code plus extensible et maintenable: L'utilisation d'une fonction comme corps d'une boucle vous permet de remplacer cette fonction par différentes implémentations (voir Strategy Pattern ). Vous pouvez par exemple facilement remplacer l'expression lambda par un appel de méthode, qui peut être écrasé par des sous-classes:

    joins.forEach(getJoinStrategy());
    

    Ensuite, vous pouvez fournir des stratégies par défaut en utilisant une énumération, qui implémente l'interface fonctionnelle. Cela rend non seulement votre code plus extensible, il augmente également la maintenabilité car il découple l'implémentation de la boucle de la déclaration de boucle.

  • Pour rendre votre code plus débogable: Séparer l'implémentation de la boucle de la déclaration peut également rendre le débogage plus facile, car vous pourriez avoir une implémentation de débogage spécialisée, qui imprime les messages de débogage, sans avoir besoin d'encombrer votre code principal avec if(DEBUG)System.out.println(). L'implémentation de débogage pourrait par exemple être une delegate , que décore l'implémentation de la fonction réelle.

  • Pour optimiser le code critique des performances: Contrairement à certaines des assertions de ce thread, Iterable#forEach fournit-il déjà de meilleures performances qu'une boucle for-each traditionnelle, au moins lors de l'utilisation de ArrayList et de l'exécution de Hotspot en mode "- client". Bien que cette amélioration des performances soit faible et négligeable pour la plupart des cas d'utilisation, il existe des situations où cette performance supplémentaire peut faire une différence. Par exemple, les responsables de bibliothèque voudront certainement évaluer si certaines de leurs implémentations de boucle existantes doivent être remplacées par Iterable#forEach.

    À propos des faits, j'ai fait quelques micro-benchmarks avec Étrier. Voici le code de test (le dernier étrier de git est nécessaire):

    @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++;
                }
            }
        }
    }
    

    Et voici les résultats:

    Quand s'exécutant avec" - client", Iterable#forEach surpasse la boucle for traditionnelle sur une ArrayList, mais est toujours plus lent que d'itérer directement sur un tableau. Lors de l'exécution avec "-server", les performances de toutes les approches sont à peu près les mêmes.

  • Pour fournir un support optionnel pour l'exécution parallèle: Il a déjà été dit ici, que la possibilité d'exécuter l'interface fonctionnelle de Iterable#forEachen parallèle en utilisant streams, est certainement un aspect important. Depuis Collection#parallelStream()ne garantit pas que la boucle est réellement exécutée en parallèle, il faut considérer cela comme une fonctionnalité facultative. En itérant sur votre liste avec list.parallelStream().forEach(...);, vous dites explicitement: Cette boucle prend en charge l'exécution parallèle, mais elle n'en dépend pas. Encore une fois, c'est une fonctionnalité et non un déficit!

    En éloignant la décision d'exécution parallèle de votre implémentation de boucle réelle, vous autorisez l'optimisation facultative de votre code, sans affecter le code lui-même, ce qui est une bonne chose. De plus, si l'implémentation de flux parallèle par défaut ne correspond pas à vos besoins, personne ne vous empêche de fournir votre propre implémentation. Vous pouvez par exemple fournir une collection optimisée en fonction du système d'exploitation sous-jacent, de la taille de la collection, du nombre de cœurs et de certains paramètres de préférence:

    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();
            }
        }
    }
    

    La bonne chose ici est que votre implémentation de boucle n'a pas besoin de savoir ou de s'en soucier détail.

 94
Author: Balder, 2015-01-13 16:58:46

forEach() peut être implémenté pour être plus rapide que pour chaque boucle, car l'itérable connaît la meilleure façon d'itérer ses éléments, par opposition à la manière itérative standard. Donc, la différence est boucle interne ou boucle externe.

Par exemple ArrayList.forEach(action) peut être simplement implémenté comme

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

, par opposition à la boucle for-each, qui nécessite beaucoup d'échafaudage

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

Cependant, nous devons également tenir compte de deux frais généraux en utilisant forEach(), l'un fait l'objet lambda, l'autre est en invoquant le lambda de la méthode. Ils ne sont probablement pas significatifs.

Voir aussi http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ pour comparer des itérations internes / externes pour différents cas d'utilisation.

 11
Author: ZhongYu, 2013-07-11 16:01:45

TL; DR: List.stream().forEach() était le plus rapide.

J'ai estimé que je devais ajouter mes résultats de l'itération d'analyse comparative. J'ai pris une approche très simple (pas de cadres d'analyse comparative) et comparé 5 méthodes différentes:

  1. classique for
  2. classique foreach
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

La procédure et les paramètres d'essai

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
}

La liste de cette classe doit être itérée et certains doIt(Integer i) doivent être appliqués à tous ce sont des membres, à chaque fois via une méthode différente. dans la classe principale, j'exécute la méthode testée trois fois pour réchauffer la JVM. J'exécute ensuite la méthode de test 1000 fois en additionnant le temps nécessaire à chaque méthode d'itération (en utilisant System.nanoTime()). Après cela, je divise cette somme par 1000 et c'est le résultat, le temps moyen. exemple:

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);

J'ai exécuté cela sur un processeur i5 4 core, avec java version 1.8.0_05

Classique for

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

Temps d'exécution: 4.21 ms

Classique foreach

for(Integer i : list) {
    doIt(i);
}

Temps d'exécution: 5.95 ms

List.forEach()

list.forEach((i) -> doIt(i));

Temps d'exécution: 3.11 ms

List.stream().forEach()

list.stream().forEach((i) -> doIt(i));

Temps d'exécution: 2.79 ms

List.parallelStream().forEach

list.parallelStream().forEach((i) -> doIt(i));

Temps d'exécution: 3,6 ms

 8
Author: Assaf, 2014-09-25 05:16:03

Je sens que je dois prolonger un peu mon commentaire...

à Propos de paradigme\style

C'est probablement l'aspect le plus notable. FP est devenu populaire en raison de ce que vous pouvez obtenir en évitant les effets secondaires. Je ne vais pas approfondir les avantages et les inconvénients que vous pouvez en tirer, car cela n'est pas lié à la question.

Cependant, je dirai que l'itération en utilisant Iterable.forEach est inspiré par FP et résulte plutôt d'apporter plus de FP à Java (ironiquement, je dirais qu'il n'y a pas beaucoup d'utilisation pour forEach en FP pur, car il ne fait rien sauf introduire des effets secondaires).

En fin de compte, je dirais que c'est plutôt une question de goût\style\paradigme dans lequel vous écrivez actuellement.

À propos du parallélisme.

Du point de vue des performances, il n'y a pas d'avantages notables promis à l'utilisation d'Iterable.forEach sur foreach(...).

Selon les documents officiels sur Iterable.forEach:

Effectue l'action donnée sur le contenu de l'Itérable, dans le les éléments d'ordre se produisent lors de l'itération, jusqu'à ce que tous les éléments aient été traité ou l'action lève une exception.

... c'est-à-dire docs assez clair qu'il n'y aura pas de parallélisme implicite. En ajouter un serait une violation LSP.

Maintenant, il y a des "collections parallell" qui sont promises dans Java 8, mais pour travailler avec celles dont vous avez besoin pour moi plus explicite et mettre un soin supplémentaire à les utiliser (voir la réponse de mschenk74 pour exemple).

BTW: dans ce cas, Stream.forEach sera utilisé, et cela ne garantit pas que le travail réel sera effectué en parallell (dépend de la collection sous-jacente).

UPDATE: n'est peut-être pas si évident et un peu étiré en un coup d'œil, mais il y a une autre facette du style et de la perspective de lisibilité.

Tout d'abord - les vieux forloops sont simples et vieux. Tout le monde connaît déjà.

Deuxième, et plus important-vous voulez probablement utilisez Itérable.forEach seulement avec une doublure lambdas. Si "corps" devient plus lourd - ils ont tendance à ne pas être-que lisible. Vous avez 2 options à partir d'ici - utilisez des classes internes (beurk) ou utilisez un vieux forloop. Les gens sont souvent ennuyés quand ils voient les mêmes choses (iteratins sur les collections) faire divers vays/styles dans la même base de code, et cela semble être le cas.

Encore une fois, cela pourrait ou non être un problème. Cela dépend des personnes travaillant sur le code.

 3
Author: Eugene Loy, 2013-05-19 18:59:55

L'une des limitations les plus importantes de la fonctionnalité forEach est le manque de prise en charge des exceptions vérifiées.

Une solution de contournement possible consiste à remplacer le terminal forEach par une vieille boucle 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);
    }

Voici la liste des questions les plus populaires avec d'autres solutions de contournement sur la gestion des exceptions vérifiées dans les lambdas et les flux:

Java 8 fonction Lambda qui lève une exception?

Java 8: Lambda-Streams, Filtrer par méthode avec Exception

Comment puis-je lancer des exceptions vérifiées à partir de flux Java 8?

Java 8: Gestion obligatoire des exceptions vérifiées dans les expressions lambda. Pourquoi obligatoire, pas facultatif?

 2
Author: Vadzim, 2017-05-23 11:33:26

L'avantage de la méthode Java 1.8 forEach par rapport à la boucle 1.7 Enhanced for est que lors de l'écriture de code, vous pouvez vous concentrer uniquement sur la logique métier.

ForEach méthode prend java.util.fonction.Objet de consommation comme argument, donc Cela aide à avoir notre logique métier à un endroit séparé que vous pouvez réutiliser à tout moment.

Regardez ci-dessous l'extrait,

  • Ici, j'ai créé une nouvelle classe qui remplacera la méthode accept class de la classe Consumer, où vous pouvez ajouter une fonctionnalité supplémentaire, Plus qu'une itération..!!!!!!

    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);
                }
            });
        }
    }
    
 1
Author: Hardik Patel, 2017-12-23 14:59:03