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

Quelle 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 qui pourraient être "simplifiées" avec lambdas, mais y a-t-il vraiment un avantage à les utiliser? Serait-il améliorer leurs performances et la lisibilité?

MODIFIER

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

380
demandé sur Rafael 2013-05-19 17:57:22

8 réponses

l'avantage est pris en compte lorsque les opérations peuvent être effectuées en parallèle. (Voir http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - la section sur l'itération interne et externe)

  • le principal avantage de mon point de vue est que la mise en œuvre 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 en parallèle, vous pouvez simplement écrire

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

    vous devrez écrire un peu de code supplémentaire pour la manipulation du fil, etc.

Note: pour ma réponse, j'ai supposé qu'il se joint à la mise en œuvre de l'interface java.util.Stream . Si joins n'implémente que l'interface java.util.Iterable , ce n'est plus vrai.

157
répondu mschenk74 2018-06-29 07:40:06

la meilleure pratique consiste à utiliser for-each . Outre la violation du Keep It Simple, stupide principe, le nouveau-fangled forEach() a au moins les déficiences suivantes:

  • on ne peut pas utiliser les variables non finales . Ainsi, un code comme celui-ci ne peut pas être transformé en lambda forEach:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • ne peut pas gérer les exceptions vérifiées . Lambdas ne sont pas en fait interdit de jeter vérifié exceptions, mais les interfaces fonctionnelles communes comme Consumer ne déclarent pas tout. Par conséquent, tout code qui lance des exceptions vérifiées doit les envelopper dans try-catch ou Throwables.propagate() . Mais même si vous faites cela, ce n'est pas toujours clair ce qui arrive à l'exception jetée. Il pourrait être avalé quelque part dans les intestins de forEach()

  • Limitedflow-control . Un return dans un lambda est égal à un continue dans un pour chacun, mais il n'y a pas d'équivalent à un break . Il est également difficile de faire des choses comme les valeurs de retour, court-circuit, ou mettre des drapeaux (qui aurait allégé les choses un peu, si ce n'était une violation de la pas de variables non-finales règle). " ce n'est pas seulement une optimisation, mais critique quand vous considérez que certaines séquences (comme lire les lignes dans un fichier) peuvent avoir des effets secondaires, ou vous pouvez avoir une séquence infinie."

  • pourrait exécuter en parallèle , ce qui est une chose horrible pour tous sauf le 0,1% de votre code qui doit être optimisé. Tout code parallèle doit être réfléchi (même s'il n'utilise pas des serrures, des volatiles, et d'autres aspects particulièrement méchants de multi-threads traditionnels). exécution.) N'importe quel bug sera difficile à trouver.

  • pourrait nuire à la performance , parce que le JIT ne peut pas optimiser forEach()+lambda dans la même mesure que les boucles simples, surtout maintenant que lambdas sont nouveaux. Par "optimisation", Je ne veux pas dire le fait d'appeler lambdas (qui est petit), mais l'analyse et la transformation sophistiquées que le compilateur JIT moderne effectue sur le code courant.

  • si vous avez besoin de parallélisme, il est probablement beaucoup plus rapide et pas beaucoup plus difficile d'utiliser un Exécutorservice . Les flux sont à la fois automatiques (Lire: ne sait pas grand chose sur votre problème) et utilisez une stratégie de parallélisation spécialisée (lire: inefficace pour le cas général) ( fork-join recursive decomposition ).

  • Fait débogage plus confus , en raison de la hiérarchie d'appel imbriquée et, Dieu nous en garde, l'exécution parallèle. Le débogueur peut avoir des problèmes d'affichage de variables du code environnant, et des choses comme le passage par étapes 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 de complexe " fluent " APIs en général. Combinaison des énoncés simples et complexes, une forte utilisation de génériques, et l'absence de variables intermédiaires conspirent pour produire des messages d'erreur confus et frustrer le débogage. Au lieu de "cette méthode n'a pas une surcharge pour le type X", vous obtenez un message d'erreur plus proche de "quelque part, vous foiré les types, mais nous ne savons pas où, ni comment."De la même manière, vous ne pouvez pas passer à travers et examiner les choses dans un débogueur aussi facilement que lorsque le code est divisé en plusieurs instructions, et les valeurs intermédiaires sont sauvegardées à variable. Enfin, la lecture du code et la compréhension des types et du comportement à chaque étape de l'exécution peuvent être non négligeables.

  • colle dehors comme un pouce endolori . Le langage Java possède déjà l'instruction "pour chaque". Pourquoi le remplacer par un appel de fonction? Pourquoi encourager à cacher les effets secondaires quelque part dans les expressions? Pourquoi encourager les monolithes lourds? Le mélange régulier pour-chaque et nouveau forEach willy-nilly est de mauvais style. Code devrait parler dans des expressions idiomatiques (modèles qui sont rapides à comprendre en raison de leur répétition), et le moins d'expressions idiomatiques sont utilisés plus le code est clair et moins de temps est passé à décider quel idiome utiliser (un grand temps-drain pour les perfectionnistes comme moi!).

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

particulièrement offensant pour moi est le fait que Stream ne met pas en œuvre Iterable (bien qu'ayant en fait la méthode iterator ) et ne peut pas être utilisé dans un pour-chacun, seulement avec un forEach (). Je recommande de transformer les flux en itérables avec (Iterable<T>)stream::iterator . Une meilleure alternative est d'utiliser StreamEx qui corrige un certain nombre de problèmes D'API Stream, y compris la mise en œuvre de Iterable .

qui 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 en ce qui concerne 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'enregistrer quelques lignes de code par rapport à L'utilisation d'un Exécutorservice, si votre problème correspond aux hypothèses de performance intégrées dans les flux et Spliterators.

  • conteneurs spécifiques qui , comme la liste synchronisée, bénéficient d'être en contrôle de l'itération (bien que ce soit largement théorique à moins que les gens peuvent apporter plus d'exemples)

  • appelant une fonction unique plus proprement en utilisant forEach() et un argument de référence de méthode (ie, list.forEach (obj::someMethod) ). Cependant, gardez à l'esprit les points sur les exceptions vérifiées, le débogage plus difficile, et la réduction du nombre d'expressions idiomatiques que vous utilisez lorsque vous écrivez du code.

Articles utilisés pour référence:

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

401
répondu 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 l'écriture de

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

et ne doit certainement pas être utilisé de cette manière. Au lieu de cela, il est conçu comme un raccourci (bien qu'il est pas exactement le même) pour l'écriture de

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

et il remplace le 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 un autre mise en œuvre de la fonction, sans affecter le code environnant. Ne pas pouvoir accéder aux variables non finales du champ extérieur 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 traditionnelle pour-chaque. Une fois qu'on s'habitue à la syntaxe de Iterable#forEach , cela rend le code plus lisible, parce que vous obtenez immédiatement ces informations supplémentaires sur le code.

Traditionnelles boucles for-each va certainement rester bonnes pratiques (pour éviter le terme galvaudé de " meilleures pratiques ") en Java. Mais cela ne signifie pas que Iterable#forEach doit être considéré comme une mauvaise pratique ou un mauvais style. C'est toujours une bonne pratique, d'utiliser le bon outil pour faire le travail, et cela comprend le mélange traditionnel pour-chaque boucles avec Iterable#forEach , où il a du sens.

depuis les inconvénients de Iterable#forEach ont déjà été discutés dans ce fil, voici quelques raisons, pourquoi vous pourriez probablement vouloir utiliser Iterable#forEach :

  • pour rendre votre code plus explicite: comme décrit ci-dessus, Iterable#forEach peut rendre votre code plus explicite et lisible dans certaines situations.

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

    joins.forEach(getJoinStrategy());
    

    alors vous pouvez fournir des stratégies par défaut en utilisant un enum, qui implémente l'interface fonctionnelle. Non seulement cela rend votre code plus extensible, il augmente également maintenabilité parce qu'il dissocie l'implémentation de boucle de la déclaration de boucle.

  • pour rendre votre code plus déboguable: séparer l'implémentation de la boucle de la déclaration peut aussi rendre le débogage plus facile, parce que vous pouvez avoir une implémentation de débogage spécialisée, qui imprime des messages de débogage, sans avoir besoin d'encombrer votre code principal avec if(DEBUG)System.out.println() . La mise en œuvre de debug pourrait par exemple être délégué , que décore la mise en œuvre de la fonction réelle.

  • pour optimiser les performances-code critique: contrairement à certaines affirmations dans ce fil, Iterable#forEach ne offrent déjà de meilleures performances qu'un traditionnel pour-chaque boucle, au moins lors de l'utilisation ArrayList et l'exécution Hotspot en mode"- client". Bien que cette performance boost est petit et négligeable pour la plupart des cas d'utilisation, Il ya des situations, où cette performance supplémentaire peut faire une différence. Par exemple: les responsables de bibliothèques voudront certainement évaluer si certaines de leurs implémentations de boucle existantes DEVRAIENT être remplacées par Iterable#forEach .

    À propos des faits, j'ai fait quelques micro-benchmarks avec Étrier . Voici le code d'essai (Caliper le plus récent 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:

    lors de l'exécution avec "- client", Iterable#forEach surpasse le traditionnel pour boucle sur un ArrayList, mais est encore plus lent que l'itération directe sur un tableau. Lors de l'exécution avec "-server", la performance de toutes les approches est à peu près la même.

  • 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#forEach en parallèle en utilisant streams , est certainement un aspect important. Puisque Collection#parallelStream() ne garantit pas, que la boucle est effectivement exécutée en parallèle, on doit considérer cela comme un option caractéristique. Par en répétant sur votre liste avec list.parallelStream().forEach(...); , vous dites explicitement: cette boucle supporte l'exécution parallèle, mais elle ne dépend pas de celle-ci. Encore une fois, c'est une fonction et non un déficit!

    en déplaçant la décision pour l'exécution parallèle loin de votre mise en œuvre de boucle réelle, vous permettez l'optimisation optionnelle de votre code, sans affecter le code lui-même, ce qui est une bonne chose. De plus, si l'implémentation de parallel stream par défaut ne correspond pas à votre besoins, personne ne vous empêche de fournir votre propre mise en œuvre. 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 connaître ou de se soucier de ces détails.

94
répondu Balder 2015-01-13 16:58:46

forEach() peut être mis en œuvre pour être plus rapide que pour-chaque boucle, parce que l'itérable sait la meilleure façon d'itérer ses éléments, par opposition à la façon itérator standard. Donc la différence est boucle interne ou boucle externe.

par exemple ArrayList.forEach(action) peut simplement être mis en œuvre comme

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

par opposition à la boucle pour chaque boucle qui nécessite beaucoup d'échafaudages

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

Cependant, nous aussi besoin de tenir compte de deux frais généraux en utilisant forEach() , l'un fabrique l'objet lambda, l'autre invoque la méthode lambda. Ils ne sont probablement pas significative.

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

11
répondu ZhongYu 2013-07-11 16:01:45

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

j'ai senti que je devrais ajouter mes résultats de l'itération de benchmarking. J'ai adopté une approche très simple (aucun cadre d'analyse comparative) et j'ai comparé 5 méthodes différentes:

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

la procédure de test et les paramètres

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 dans cette classe doit être itérée et avoir une certaine doIt(Integer i) appliquée à tous ses membres, à chaque fois via une méthode différente. dans la classe principale, j'utilise la méthode testée trois fois pour réchauffer la JVM. J'exécute ensuite la méthode d'essai 1000 fois en additionnant le temps qu'elle prend pour chaque méthode d'itération (en utilisant System.nanoTime() ). Après cela est fait, je divisez 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 lancé ceci sur un CPU de base i5 4, avec la version java 1.8.0_05

classique for

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

délai d'exécution: 4.21 ms

foreach classique

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

délai d'exécution: 5,95 ms

List.forEach()

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

délai d'exécution: 3,11 ms

List.stream().forEach()

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

délai d'exécution: 2,79 ms

List.parallelStream().forEach

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

délai d'exécution: 3.6 ms

8
répondu Assaf 2014-09-25 05:16:03

je pense que je dois étendre mon commentaire un peu...

à propos de paradigm\style

C'est probablement l'aspect le plus remarquable. FP est devenu populaire en raison de ce que vous pouvez obtenir en évitant les effets secondaires. Je ne m'attarderai pas sur les avantages et les inconvénients que vous pouvez en tirer, puisque ce n'est pas lié à la question.

cependant, je dirai que l'itération utilisant itérable.forEach est inspiré par FP et plutôt résultat d'apporter plus de FP à Java (ironiquement, je dirais qu'il n'y a pas beaucoup d'utilité pour forEach dans la FP pure, 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 que vous écrivez actuellement.

à Propos de parallélisme.

du point de vue de la performance, il n'y a pas d'avantages notables promis à utiliser Iterable.forEach foreach(...).

selon les documents officiels sur itérable.forEach :

exécute l'action donnée sur le contenu du itérable, dans le les éléments d'ordre se produisent lors de l'itération, jusqu'à ce que tous les éléments ont été traité ou l'action jette une exception.

... c'est à dire docs assez bien clair qu'il n'y aura pas de parallélisme implicite. Additionneur l'une serait une violation de la LSP.

maintenant, il y a des" collections parallell " qui sont promises en Java 8, mais pour travailler avec ceux que vous avez besoin de moi plus explicite et mettre un peu de soin supplémentaire pour les utiliser (voir la réponse de mschenk74 par exemple).

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

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

tout D'abord-plaine vieux forloops sont simples et vieux. Tout le monde sait déjà.

deuxième, et plus important - vous voulez probablement utiliser Iterable.pour l'avant seulement avec lambdas à une doublure. Si le "corps" devient plus lourd, ils ont tendance à être non-lisible. Vous avez 2 options d'ici-utilisez les classes intérieures ou utiliser un vieux forloop. Les gens sont souvent ennuyés quand ils voient les mêmes choses (itératines sur les collections) en train d'être fait 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. Ça dépend des gens qui travaillent sur le code.

3
répondu Eugene Loy 2013-05-19 18:59:55

l 'une des limitations fonctionnelles les plus importantes de forEach est l' absence de prise en charge des exceptions vérifiées.

un contournement possible est de remplacer le terminal forEach par une simple 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 rechange sur la manipulation contrôlée des exceptions dans lambdas et streams:

Java 8 fonction Lambda qui jette exception?

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

Comment puis-je lancer des exceptions vérifiées depuis Java 8 streams?

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

2
répondu Vadzim 2017-05-23 11:33:26

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

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

Avoir regarder ci-dessous l'extrait de code,

  • ici j'ai créé nouvelle classe qui supplantera la méthode accept class de la classe Consumer, où vous pouvez ajouter la fonctionnalité supplémentaire, plus que L'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
répondu Hardik Patel 2017-12-23 14:59:03