Parmi les propositions suivantes, laquelle est la 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é?
EDIT
Je vais aussi étendre cette question à des méthodes plus longues. Je sais que vous ne pouvez pas revenir ou casser la fonction parent d'un lambda et que cela devrait également être pris en compte lorsque vous les comparez, mais y a-t-il autre chose à considérer?
La meilleure pratique consiste à utiliser for-each
. En plus de violer le principe Keep It Simple, Stupid , le forEach()
nouvellement créé comporte au moins les défauts suivants:
Impossible d'utiliser des variables non finales . Donc, un code comme celui-ci ne peut pas être transformé en 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 . Il n’est pas interdit aux Lambda de générer des exceptions vérifiées, mais les interfaces fonctionnelles courantes telles que Consumer
n'en déclarent aucune. Par conséquent, tout code générant des exceptions vérifiées doit les envelopper dans try-catch
ou Throwables.propagate()
. Mais même si vous faites cela, il n’est pas toujours clair ce qui se passe avec l’exception levée. Il pourrait être avalé quelque part dans les tripes de forEach()
Contrôle de flux limité . 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 des valeurs de retour, un court-circuit ou définir des indicateurs (ce qui aurait allégé un peu les choses si ce n’était pas une violation de la pas de variable non finale règle). "Ce n'est pas simplement une optimisation, mais il est essentiel de considérer que certaines séquences (comme la lecture des lignes d'un fichier) peuvent avoir des effets secondaires ou que vous pouvez avoir une séquence infinie."
Peut s'exécuter en parallèle , ce qui est une chose horrible, 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 de verrous, de composants 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 optimiser forEach () + lambda de la même manière que les boucles simples, en particulier maintenant que les lambdas sont nouveaux. Par "optimisation", je ne parle pas de la surcharge d'appeler lambdas (ce qui est petit), mais de l'analyse et de 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 d’utiliser un service d’exécution . Les flux sont à la fois automagiques (lecture: ne connaissent pas beaucoup votre problème) et utilisent une stratégie de parallélisation spécialisée (lecture: inefficace pour le cas général) ( décomposition récursive fork-join ).
Rend le débogage plus déroutant , en raison de la hiérarchie des appels imbriqués et, sauf erreur, de l'exécution parallèle. Le débogueur peut avoir des problèmes d'affichage des variables du code environnant et des opérations telles que la procédure pas à pas risquent de ne pas fonctionner correctement.
Les flux en général sont plus difficiles à coder, à lire et à déboguer . En fait, cela est vrai des API "" fluentes "==" complexes en général. La combinaison d'instructions simples complexes, l'utilisation intensive de génériques et le manque de variables intermédiaires contribuent à produire des messages d'erreur confus et à empêcher 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 gâché les types, mais nous ne savons ni où ni comment". De même, vous ne pouvez pas parcourir et examiner les éléments d'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 du comportement à chaque étape de l’exécution peuvent être non triviales.
Sticks out comme un pouce endolori . Le langage Java a déjà l'instruction for-each. Pourquoi le remplacer par un appel de fonction? Pourquoi encourager les effets secondaires cachés quelque part dans les expressions? Pourquoi encourager les one-liners peu maniables? Mélanger de façon régulière pour tous et de nouveaux pourChaque bon à rien est mauvais style. Code devrait parler dans les idiomes (modèles 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 pris pour décider quel idiome utiliser (une perte de temps considérable pour des perfectionnistes comme moi! ).
Comme vous pouvez le constater, je ne suis pas un grand fan de forEach (), sauf dans les cas où cela a du sens.
Le fait que Stream
n'implémente pas Iterable
(malgré le fait que la méthode iterator
) soit réellement choquante et ne peut pas être utilisé dans un for-each, uniquement avec un forEach (). Je recommande de convertir Streams dans Iterables avec (Iterable<T>)stream::iterator
. Une meilleure alternative consiste à utiliser StreamEx qui résout un certain nombre de problèmes liés à l'API Stream, notamment la mise en œuvre de Iterable
.
Ceci dit, forEach()
est utile pour:
Itération atomique sur une liste synchronisée . Auparavant, 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 évite 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, tirent profit du contrôle de l'itération (bien que ce soit en grande partie théorique à moins que les gens ne puissent donner plus d'exemples)
Appeler plus proprement une seule fonction 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 relatifs aux exceptions vérifiées, au 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 pour référence:
EDIT: Ressemble à certaines des propositions originales pour 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).
L'avantage vient 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)
L’avantage principal 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 de manière 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'interface Java.util.Stream
. Si joint n'implémente que l'interface Java.util.Iterable
, ce n'est plus vrai.
En lisant cette question, on peut avoir l’impression que Iterable#forEach
, associé aux expressions lambda, constitue un raccourci/un remplacement pour l’écriture d’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 comme un raccourci pour l'écriture
for (String join : joins) {
mIrc.join(mSession, join);
}
et ne devrait certainement pas être utilisé de cette façon. Au lieu de cela, il s’agit d’un raccourci (bien que ce soit et non identique) pour écrire
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 ni le flux de contrôle, et 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#forEach
de la sémantique d'une boucle for-each traditionnelle. Une fois que l’on s’habitue à la syntaxe de Iterable#forEach
, le code devient 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 surutilisé " meilleure pratique ") en Java. Mais cela ne signifie pas que Iterable#forEach
devrait être considéré comme une mauvaise pratique ou un mauvais style. Il est toujours bon d’utiliser le bon outil pour faire le travail, ce qui implique de mélanger les boucles traditionnelles pour-chaque avec Iterable#forEach
, lorsque cela se justifie.
Puisque les inconvénients de Iterable#forEach
ont déjà été discutés dans ce fil de discussion, voici quelques raisons pour lesquelles vous voudrez probablement utiliser Iterable#forEach
:
Pour rendre votre code plus explicite: Comme décrit ci-dessus, Iterable#forEach
peut créer votre code plus explicite et lisible dans certaines situations.
Pour rendre votre code plus extensible et maintenable: Utiliser une fonction en tant que corps d'une boucle vous permet de remplacer cette fonction par différentes implémentations (voir Modèle de stratégie ). Vous pourriez par exemple remplacez facilement 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 en utilisant un enum, qui implémente l'interface fonctionnelle. Cela rend non seulement votre code plus extensible, mais augmente également la facilité de maintenance, car il dissocie l'implémentation de la boucle de la déclaration de la boucle.
Pour rendre votre code plus débogable: Séparer l'implémentation de la boucle de la déclaration peut également faciliter le débogage, car vous pourriez avoir une implémentation de débogage spécialisée, qui affiche 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é , que décore l'implémentation réelle de la fonction.
Pour optimiser le code essentiel à la performance: Contrairement à certaines assertions de ce fil, Iterable#forEach
fait fournit déjà de meilleures performances qu'une boucle for-each traditionnelle, du 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 dans la plupart des cas d'utilisation, il existe des situations dans lesquelles 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 par Iterable#forEach
.
Pour appuyer cette affirmation sur des faits, j'ai effectué 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 est exécuté avec "-client", Iterable#forEach
surpasse la boucle for traditionnelle sur une liste de tableaux, mais reste plus lent que l'itération directe sur un tableau. Lorsque vous utilisez "-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#forEach
en parallèle en utilisant stream , 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é optionnelle . En parcourant votre liste avec list.parallelStream().forEach(...);
, vous dites explicitement: Cette boucle prend en charge l'exécution en 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 optionnelle 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 répond pas à vos besoins, personne ne vous empêche de fournir votre propre implémentation. Vous pourriez par exemple fournissez 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.
forEach()
peut être implémenté pour être plus rapide que pour chaque boucle, car iterable connaît le meilleur moyen d'itérer ses éléments, par opposition à la méthode standard d'itérateur. Donc, la différence est 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 for-each 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 prendre en compte deux frais généraux en utilisant forEach()
, l'un fabriquant l'objet lambda, l'autre invoquant la méthode lambda. Ils ne sont probablement pas significatifs.
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.
TL; DR: List.stream().forEach()
était le plus rapide.
J'ai senti que je devrais ajouter mes résultats d'itération de benchmarking. J'ai adopté une approche très simple (pas de cadre d'analyse comparative) et mesuré 5 méthodes différentes:
for
List.forEach()
List.stream().forEach()
List.parallelStream().forEach
private List<Integer> list;
private final int size = 1_000_000;
public MyClass(){
list = new ArrayList<>();
Random Rand = new Random();
for (int i = 0; i < size; ++i) {
list.add(Rand.nextInt(size * 50));
}
}
private void doIt(Integer i) {
i *= 2; //so it won't get JITed out
}
La liste de cette classe doit être itérée et appliquer une doIt(Integer i)
à tous ses membres, à chaque fois via une méthode différente. Dans la classe Main, j’exécute trois fois la méthode testée pour réchauffer la machine virtuelle Java. J'exécute ensuite la méthode de test 1000 fois en additionnant le temps nécessaire pour chaque méthode d'itération (à l'aide de System.nanoTime()
). Ensuite, je divise cette somme par 1000 et voici 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, avec Java version 1.8.0_05.
for
for(int i = 0, l = list.size(); i < l; ++i) {
doIt(list.get(i));
}
temps d'exécution: 4.21 ms
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
L’une des limitations les plus fonctionnelles de forEach
est l’absence de prise en charge des exceptions vérifiées.
Un solution de contournement possible consiste à remplacer le terminal forEach
par une ancienne 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 courantes 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 générer des exceptions CHECKED depuis les flux Java 8?
Je sens que je dois prolonger un peu mon commentaire ...
À propos du paradigme\style
C'est probablement l'aspect le plus notable. FP est devenu populaire grâce à ce que vous pouvez éviter en évitant les effets secondaires. Je ne vais pas approfondir ce que vous pouvez en retirer comme avantages et inconvénients, car cela n’est pas lié à la question.
Cependant, je dirai que l'itération utilisant Iterable.forEach est inspirée de FP et qu'elle a plutôt pour effet d'amener davantage de FP à Java (ironiquement, je dirais qu’il n’ya pas beaucoup d’utilité pour forEach en pure FP, puisqu’il ne fait qu’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, l'utilisation d'Iterable.forEach over foreach (...) ne présente aucun avantage notable.
Selon les officiels docs sur Iterable.forEach :
Exécute l'action indiquée sur le contenu de l'itérable dans l'ordre où 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 assez clair qu’il n’y aura pas de parallélisme implicite. En ajouter un serait une violation du LSP.
Maintenant, il y a des "collections de parallèles" qui sont promises dans Java 8, mais pour travailler avec celles dont vous avez besoin, expliquez-moi et mettez un peu plus de soin à les utiliser (voir la réponse de mschenk74).
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).
UPDATE: n'est peut-être pas si évident et un peu étiré en un coup d'œil, mais il existe une autre facette du point de vue du style et de la lisibilité.
Tout d'abord, les vieux forloops sont simples et vieux. Tout le monde les connaît déjà.
Deuxièmement, et plus important encore, vous voudrez probablement utiliser Iterable.forChaque avec les lambdas à une ligne. Si "corps" devient plus lourd - ils ont tendance à ne pas être lisible. Vous avez 2 options à partir d’ici: utiliser les classes internes (beurk) ou utiliser un vieux forloop. Les gens sont souvent agacés quand ils voient les mêmes choses (itératines sur les collections) appliquées à 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. Cela dépend des personnes travaillant sur le code.
L'avantage de la méthode Java 1.8 forEach à la version 1.7 améliorée La boucle for améliorée est que, lors de l'écriture de code, vous ne pouvez vous concentrer que sur la logique métier.
la méthode forEach prend l'objet Java.util.function.Consumer comme argument, donc il est utile de disposer de notre logique métier à un emplacement distinct, que vous pouvez le réutiliser à tout moment.
Regardez l'extrait ci-dessous,
Ici, j'ai créé une nouvelle classe qui remplacera la méthode accept class de la classe grand public, 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);
}
});
}
}