Java 8 Iterable.forEach () vs boucle foreach

466

Lequel des éléments suivants est une meilleure pratique dans 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é?

ÉDITER

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 considération lors de la comparaison, mais y a-t-il autre chose à considérer?

nebkat
la source
10
Il n'y a aucun réel avantage de performance l'un par rapport à l'autre. La première option est quelque chose d'inspiré par FP (qui est communément parlé comme une manière plus "agréable" et "claire" d'exprimer votre code). En réalité - c'est plutôt une question de "style".
Eugene Loy
5
@Dwb: dans ce cas, ce n'est pas pertinent. forEach n'est pas défini comme étant parallèle ou quelque chose comme ça, donc ces deux choses sont sémantiquement équivalentes. Bien sûr, il est possible d'implémenter une version parallèle de forEach (et une pourrait déjà être présente dans la bibliothèque standard), et dans ce cas, la syntaxe d'expression lambda serait très utile.
AardvarkSoup
8
@AardvarkSoup L'instance sur laquelle forEach est appelée est un Stream ( lambdadoc.net/api/java/util/stream/Stream.html ). Pour demander une exécution parallèle, on pourrait écrire join.parallel (). ForEach (...)
mschenk74
13
Est-ce joins.forEach((join) -> mIrc.join(mSession, join));vraiment une "simplification" de for (String join : joins) { mIrc.join(mSession, join); }? Vous avez augmenté le nombre de signes de ponctuation de 9 à 12, afin de masquer le type de join. Ce que vous avez vraiment fait, c'est de mettre deux déclarations sur une seule ligne.
Tom Hawtin - tackline
7
Un autre point à considérer est la capacité limitée de capture variable de Java. Avec Stream.forEach (), vous ne pouvez pas mettre à jour les variables locales car leur capture les rend finales, ce qui signifie que vous pouvez avoir un comportement avec état dans le lambda forEach (sauf si vous êtes prêt pour une laideur telle que l'utilisation de variables d'état de classe).
RedGlyph

Réponses:

512

La meilleure pratique consiste à utiliser for-each. En plus de violer le principe Keep It Simple, Stupid , le nouveau modèle forEach()présente au moins les lacunes suivantes:

  • Impossible d'utiliser des 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;
    }
  • Impossible de gérer les exceptions vérifiées . Il n'est en fait pas interdit aux lambdas de lancer des exceptions vérifiées, mais les interfaces fonctionnelles courantes comme Consumern'en déclarent aucune. Par conséquent, tout code qui lève des exceptions vérifiées doit les encapsuler dans try-catchou 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 tripes deforEach()

  • Contrôle de débit limité . A returndans un lambda est égal à a continuedans un pour chacun, mais il n'y a pas d'équivalent à a break. Il est également difficile de faire des choses comme des valeurs de retour, un court-circuit ou des drapeaux (ce qui aurait un peu allégé les choses si ce n'était pas une violation de la règle des variables non finales ). "Ce n'est pas seulement une optimisation, mais critique lorsque vous considérez 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 tous sauf 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 de verrous, de volatils et d'autres aspects particulièrement désagréables de l'exécution multithread traditionnelle). Tout bug sera difficile à trouver.

  • Cela pourrait 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 parle pas de la surcharge de l'appel de lambdas (qui est petite), mais de l'analyse et de la transformation sophistiquées que le compilateur JIT moderne effectue sur l'exécution de code.

  • Si vous avez besoin du parallélisme, il est probablement beaucoup plus rapide et pas beaucoup plus difficile d'utiliser un ExecutorService . Les flux sont à la fois automagiques (lire: ne connaissent pas grand-chose à 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 à jointure en fourche ).

  • Rend le débogage plus confus , en raison de la hiérarchie des appels imbriqués et, Dieu nous en préserve, de l'exécution parallèle. Le débogueur peut avoir des problèmes d'affichage des variables du code environnant, et des choses comme la procédure pas à pas 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'instructions simples complexes, l'utilisation intensive de génériques et le manque de variables intermédiaires concourent à produire des messages d'erreur déroutants et à contrecarrer 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 où 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, la lecture du code et la compréhension des types et des comportements à chaque étape de l'exécution peuvent être non triviales.

  • Sort comme un pouce endolori . Le langage Java possède déjà l'instruction for-each. Pourquoi le remplacer par un appel de fonction? Pourquoi encourager à cacher des effets secondaires quelque part dans les expressions? Pourquoi encourager les one-liners peu maniables? Mélanger régulièrement pour chacun et nouveau pour chaque bon gré mal gré est un mauvais style. Le code doit parler en idiomes (des schémas qui sont faciles à comprendre en raison de leur répétition), et moins les idiomes sont utilisés, plus le code est clair et moins de temps est consacré à décider quel idiome utiliser (une grosse perte de temps 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.

Un fait particulièrement choquant pour moi est le fait qu'il Streamn'implémente pas Iterable(bien qu'il ait réellement une méthode iterator) et ne puisse pas être utilisé dans un for-each, uniquement avec un forEach (). Je recommande de diffuser des flux dans Iterables avec (Iterable<T>)stream::iterator. Une meilleure alternative consiste à utiliser StreamEx qui résout un certain nombre de problèmes d'API Stream, y compris la mise en œuvre Iterable.

Cela dit, forEach()est utile pour les éléments suivants:

  • 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 performances intégrées dans Streams et Spliterators.

  • 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 à moins que les gens puissent apporter plus d'exemples)

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

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

EDIT: On dirait que certaines des propositions originales pour les lambdas (telles que 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).

Aleksandr Dubinsky
la source
95
"Pourquoi encourager à cacher des effets secondaires quelque part dans les expressions?" est la mauvaise question. Le fonctionnel forEachest là pour encourager le style fonctionnel, c'est-à-dire utiliser des expressions sans effets secondaires. Si vous rencontrez la situation, le the forEachne fonctionne pas bien avec vos effets secondaires, vous devriez avoir le sentiment que vous n'utilisez pas le bon outil pour le travail. Ensuite, la réponse est simple, c'est parce que votre sentiment est bon, alors restez à la boucle pour chaque pour cela. La forboucle classique n'est pas devenue obsolète…
Holger
17
@Holger Comment peut- forEachon l'utiliser sans effets secondaires?
Aleksandr Dubinsky
14
D'accord, je n'étais pas assez précis, forEachc'est la seule opération de flux destinée aux effets secondaires, mais pas aux effets secondaires comme votre exemple de code, le comptage est une reduceopération typique . Je suggère, en règle générale, de conserver chaque opération qui manipule des variables locales ou qui influencera le flux de contrôle (y compris la gestion des exceptions) dans une forboucle classique . En ce qui concerne la question d'origine, je pense que le problème vient du fait que quelqu'un utilise un flux où une simple forboucle sur la source du flux serait suffisante. Utilisez un flux où forEach()fonctionne uniquement
Holger
8
@Holger À quel exemple d'effets secondaires forEachconviendrait-il?
Aleksandr Dubinsky
29
Quelque chose qui traite chaque élément individuellement et n'essaye pas de muter les variables locales. Par exemple, manipuler les éléments eux-mêmes ou les imprimer, les écrire / les envoyer dans un fichier, un flux réseau, etc. Ce n'est pas un problème pour moi si vous remettez en question ces exemples et ne voyez aucune application pour cela; le filtrage, le mappage, la réduction, la recherche et (dans une moindre mesure) la collecte sont les opérations préférées d'un flux. Le forEach me semble une commodité pour la liaison avec les API existantes. Et pour les opérations parallèles, bien sûr. Ceux-ci ne fonctionneront pas avec les forboucles.
Holger
169

L'avantage est pris 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 - 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 du code supplémentaire pour la gestion des threads, etc.

Remarque: Pour ma réponse, j'ai supposé que les jointures implémentaient l' java.util.Streaminterface. Si join implémente uniquement l' java.util.Iterableinterface, cela n'est plus vrai.

mschenk74
la source
4
Les diapositives d'un ingénieur oracle auquel il fait référence ( blogs.oracle.com/darcy/resource/Devoxx/… ) ne mentionnent pas le parallélisme au sein de ces expressions lambda. Le parallélisme peut se produire dans les méthodes de collecte en bloc comme map& foldqui ne sont pas vraiment liées aux lambdas.
Thomas Jungblut
1
Il ne semble pas vraiment que le code OP bénéficiera ici du parallélisme automatique (d'autant plus qu'il n'y a aucune garantie qu'il y en aura un). Nous ne savons pas vraiment ce qu'est "mIrc", mais "join" ne semble pas vraiment être quelque chose qui peut être exécuté hors service.
Eugene Loy
11
Stream#forEachet ce Iterable#forEachn'est pas la même chose. OP demande Iterable#forEach.
gvlasov
2
J'ai utilisé le style UPDATEX car il y a eu des changements dans les spécifications entre le moment où la question a été posée et le moment où la réponse a été mise à jour. Sans l'historique de la réponse, ce serait encore plus déroutant, pensais-je.
mschenk74
1
Quelqu'un pourrait-il m'expliquer pourquoi cette réponse n'est pas valide si la joinsmise en œuvre Iterableau lieu de Stream? D'après deux ou joins.stream().forEach((join) -> mIrc.join(mSession, join));joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));joinsIterable
trois
112

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

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

n'est pas conçu comme un raccourci pour écrire

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

et ne doit certainement pas être utilisé de cette façon. Au lieu de cela, il est conçu comme un raccourci (bien qu'il ne soit pas exactement le même) pour écrire

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

Et c'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);
}

Le remplacement du 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 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 aux variables non finales de la portée externe n'est pas un déficit de fonctions / lambdas, c'est une caractéristique qui distingue la sémantique de Iterable#forEachde la sémantique d'une boucle traditionnelle pour chaque boucle. 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 traditionnelles pour chaque resteront certainement bonne pratique (pour éviter le terme galvaudé de " meilleure pratique ") en Java. Mais cela ne signifie pas que cela Iterable#forEachdevrait ê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 de boucles traditionnelles pour chaque avec Iterable#forEach, lorsque cela est logique.

Depuis les inconvénients de Iterable#forEach ont déjà été discutés dans ce fil, voici quelques raisons, pourquoi vous voudrez probablement 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: utilisation d'une fonction comme corps d'une boucle vous permet de remplacer cette fonction par différentes implémentations (voir Modèle 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());

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

  • Pour rendre votre code plus déboguable: La séparation de 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 affiche les messages de débogage, sans avoir à encombrer votre code principal avec if(DEBUG)System.out.println(). L'implémentation de débogage pourrait par exemple être un délégué , qui décore l'implémentation de la fonction réelle.

  • Pour optimiser le code critique pour les performances: contrairement à certaines des affirmations de ce thread, Iterable#forEach fournit déjà de meilleures performances qu'une boucle traditionnelle pour chaque boucle, au moins lors de l'utilisation d'ArrayList et de l'exécution de Hotspot en mode "-client". Bien que cette amélioration des performances soit faible et négligeable dans la plupart des cas d'utilisation, il existe des situations où ces performances supplémentaires peuvent faire la 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 parIterable#forEach .

    Pour étayer cette déclaration avec des faits, j'ai fait quelques micro-repères avec Caliper . Voici le code de test (le dernier Caliper 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:

    Lorsqu'il s'exécute avec "-client", Iterable#forEachsurpasse la boucle for traditionnelle sur une liste de tableaux, mais est toujours plus lent que l'itération directe 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 Iterable#forEachen parallèle en utilisant des flux , est certainement un aspect important. Puisque 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 parcourant 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 caractéristique 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 connaître ou de se soucier de ces détails.

Balder
la source
5
Vous avez un point de vue intéressant dans cette discussion et soulevez un certain nombre de points. Je vais essayer de les aborder. Vous proposez de basculer entre forEachet en for-eachfonction de certains critères concernant la nature du corps de boucle. La sagesse et la discipline pour suivre de telles règles sont la marque d'un bon programmeur. Ces règles sont également son fléau, car les gens autour de lui ne les respectent pas ou ne sont pas d'accord. Par exemple, en utilisant des exceptions cochées et non cochées. Cette situation semble encore plus nuancée. Mais, si le corps "n'affecte pas le code surround ou le contrôle de flux", n'est-il pas mieux pris en compte comme fonction?
Aleksandr Dubinsky
4
Merci pour les commentaires détaillés Aleksandr. But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?. Oui, ce sera souvent le cas à mon avis - la factorisation de ces boucles en tant que fonctions est une conséquence naturelle.
Balder
2
En ce qui concerne le problème de performances - je suppose que cela dépend beaucoup de la nature de la boucle. Dans un projet sur lequel je travaille, j'utilise des boucles de style fonction similaires à celles d' Iterable#forEachavant Java 8 uniquement en raison de l'augmentation des performances. Le projet en question a une boucle principale similaire à une boucle de jeu, avec un nombre indéfini de sous-boucles imbriquées, où les clients peuvent brancher les participants de la boucle en tant que fonctions. Une telle structure logicielle en bénéficie grandement Iteable#forEach.
Balder
7
Il y a une phrase tout à la fin de ma critique: "Le code devrait parler en idiomes, et moins les idiomes sont utilisés, plus le code est clair et moins de temps est consacré à décider quel idiome utiliser". J'ai commencé à apprécier profondément ce point lorsque je suis passé de C # à Java.
Aleksandr Dubinsky
7
C'est un mauvais argument. Vous pouvez l'utiliser pour justifier tout ce que vous voulez: pourquoi vous ne devriez pas utiliser une boucle for, car une boucle while est assez bonne et c'est un idiome de moins. Heck, pourquoi utiliser une instruction loop, switch ou try / catch alors que goto peut faire tout cela et plus encore.
tapichu
13

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 à l'itérateur standard. La différence est donc une boucle interne ou une 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 pour chaque qui nécessite beaucoup d'échafaudages

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 crée l'objet lambda, l'autre appelle la méthode lambda. Ils ne sont probablement pas significatifs.

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

ZhongYu
la source
8
pourquoi l'itérable connaît-il le meilleur moyen mais pas l'itérateur?
mschenk74
2
aucune différence essentielle, mais du code supplémentaire est nécessaire pour se conformer à l'interface de l'itérateur, ce qui peut être plus coûteux.
ZhongYu
1
@ zhong.j.yu si vous implémentez Collection, vous implémentez également Iterable de toute façon. Donc, il n'y a pas de surcharge de code en termes d '"ajout de code pour implémenter les méthodes d'interface manquantes", si tel est votre point. Comme l'a dit mschenk74, il semble qu'il n'y ait aucune raison pour laquelle vous ne pouvez pas modifier votre itérateur pour savoir comment itérer sur votre collection de la meilleure façon possible. Je suis d'accord qu'il pourrait y avoir des frais généraux pour la création d'itérateurs, mais sérieusement, ces choses généralement si bon marché, que vous pouvez dire qu'elles ont un coût nul ...
Eugene Loy
4
par exemple itérer un arbre:, void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}c'est plus élégant que l'itération externe, et vous pouvez décider de la meilleure façon de synchroniser
ratchet freak
1
Curieusement, le seul commentaire dans les String.joinméthodes (ok, mauvaise jointure) est "Nombre d'éléments ne valant probablement pas la surcharge Arrays.stream". ils utilisent donc une boucle chic pour.
Tom Hawtin - tackline
7

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

Je pensais que je devais ajouter mes résultats de l'itération de référence. J'ai adopté une approche très simple (pas de frameworks de benchmarking) et j'ai testé 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 de cette classe doit être itérée et doIt(Integer i)appliquée à tous ses 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 machine virtuelle Java. Je lance ensuite la méthode de test 1000 fois en additionnant le temps qu'il faut pour 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);

Je l'ai exécuté sur un processeur i5 4 cœurs, avec la version java 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

foreach classique

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

Assaf
la source
23
Comment obtenez-vous ces chiffres? Quel cadre de référence utilisez-vous? Si vous n'en utilisez aucun et que System.out.printlnvous affichez simplement ces données naïvement, tous les résultats sont inutiles.
Luiggi Mendoza
2
Pas de cadre. J'utilise System.nanoTime(). Si vous lisez la réponse, vous verrez comment cela a été fait. Je ne pense pas que cela le rend inutile car il s'agit d'une question relative . Je me fiche de la qualité d'une certaine méthode, je me soucie de son efficacité par rapport aux autres méthodes.
Assaf
31
Et c'est le but d'un bon micro benchmark. Comme vous n'avez pas satisfait à ces exigences, les résultats sont inutiles.
Luiggi Mendoza
6
Je peux recommander à la place de connaître JMH, c'est ce qui est utilisé pour Java lui-même et met beaucoup d'efforts pour obtenir des chiffres corrects: openjdk.java.net/projects/code-tools/jmh
dsvensson
1
Je suis d'accord avec @LuiggiMendoza. Il n'y a aucun moyen de savoir si ces résultats sont cohérents ou valides. Dieu sait combien de repères j'ai fait qui continue de rapporter des résultats différents, en particulier en fonction de l'ordre d'itération, de la taille et de ce qui ne l'est pas.
mmm
6

Je sens que je dois étendre un peu mon commentaire ...

À propos du paradigme \ style

C'est probablement l'aspect le plus notable. La FP est devenue populaire en raison de ce que vous pouvez éviter les effets secondaires. Je ne vais pas approfondir les avantages que vous pouvez en retirer, car cela n'est pas lié à la question.

Cependant, je dirai que l'itération utilisant Iterable.forEach est inspirée de 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 d'autre que l'introduction 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 du parallélisme.

Du point de vue des performances, il n'y a aucun avantage notable promis à utiliser Iterable.forEach over foreach (...).

Selon les documents officiels sur Iterable.forEach :

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

... c'est-à-dire que les documents indiquent clairement qu'il n'y aura pas de parallélisme implicite. L'ajout d'un serait une violation de LSP.

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

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

MISE À JOUR: ce 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 lisibilité.

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

Deuxièmement, et plus important - vous voudrez probablement utiliser Iterable.forEach uniquement avec des lambdas à une ligne. Si le «corps» devient plus lourd - ils ont tendance à ne pas être si lisibles. Vous avez 2 options à partir d'ici - utilisez des classes internes (beurk) ou utilisez un ancien forloop ordinaire. Les gens sont souvent ennuyés quand ils voient les mêmes choses (itérations sur les collections) être effectuées dans différents styles / 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. Dépend des personnes travaillant sur le code.

Eugene Loy
la source
1
Le parallélisme n'a pas besoin de nouvelles "collections parallèles". Cela dépend simplement de si vous avez demandé un flux séquentiel (en utilisant collection.stream ()) ou parallèle (en utilisant collection.parallelStream ()).
JB Nizet
@JBNizet Selon docs Collection.parallelStream () ne garantit pas que l'implémentation de la collection retournera un flux parallèle. Je me demande en fait quand cela pourrait arriver, mais cela dépend probablement de la collection.
Eugene Loy
D'accord. Cela dépend aussi de la collection. Mais mon point était que les boucles foreach parallèles étaient déjà disponibles avec toutes les collections standard (ArrayList, etc.). Pas besoin d'attendre les "collections parallèles".
JB Nizet
@JBNizet est d'accord sur votre point, mais ce n'est pas vraiment ce que je voulais dire par "collections parallèles" en premier lieu. Je fais référence à Collection.parallelStream () qui a été ajoutée dans Java 8 en tant que "collections parallèles" par analogie avec le concept de Scala qui fait à peu près la même chose. De plus, je ne sais pas comment cela s'appelle dans le bit de JSR. J'ai vu quelques articles qui utilisent la même terminologie pour cette fonctionnalité Java 8.
Eugene Loy
1
pour le dernier paragraphe, vous pouvez utiliser une référence de fonction:collection.forEach(MyClass::loopBody);
ratchet freak
6

L'une des fonctions les plus agréables forEach les est le manque de prise en charge des exceptions vérifiées.

Une solution de contournement possible consiste à remplacer le terminal forEachpar une ancienne boucle foreach simple:

    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:

Fonction Java 8 Lambda qui lève une exception?

Java 8: Lambda-Streams, filtrer par méthode avec exception

Comment puis-je lever des exceptions CHECKED depuis des flux Java 8?

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

Vadzim
la source
3

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

La méthode forEach prend l'argument java.util.function.Consumer comme argument, ce qui permet d'avoir notre logique métier à un emplacement séparé que vous pouvez réutiliser à tout moment.

Regardez l'extrait ci-dessous,

  • Ici, j'ai créé une nouvelle classe qui remplacera la méthode de classe acceptée de Consumer Class, où vous pouvez ajouter des fonctionnalités supplémentaires, 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);
                }
            });
        }
    }
Hardik Patel
la source