mai
2014
Java Quiz #46 : flagada stream
Maintenant que la rage et la fureur de Devoxx sont retombées, je vous propose de résoudre un petit quiz facile sur Java 8 !
A votre avis, qu'affiche le code suivant ?
Attention, ne trichez pas : interdiction d'exécuter le code avant d'avoir trouvé ! Mais pouvez consulter la documentation de la classe IntStream.
public class Quiz { public static void main(String[] args) { IntStream.rangeClosed(1, 10) .map(i -> i++) .forEach(System.out::println); } }
Solution
Comme certains commentateurs avisés l'ont découvert, ce fragment de code n'affiche pas les nombres de 2 à 11, mais de 1 à 10. Pourquoi donc ?
C'est l'API Stream (Java 8+) qui est utilisée ici. Pour rappel, un Stream est une sorte de pipeline dans lequel on injecte des données, lesquelles peuvent être manipulées en cours de route à l'aide d'opérations de type map, reduce ou filter, et sont finalement récupérées ou utilisées en bout de chaîne.
Etudions la séquence proposée par le quiz.
- Un Stream est créé, spécialisé dans la manipulation d'entiers (
IntStream
) - La méthode
rangeClosed()
y injecte successivement les nombres de 1 à 10, inclus. - La méthode
map()
applique une transformation (exprimée sous la forme d'une expression lambda) sur chacun des entiers, produisant ainsi de nouvelles valeurs envoyées dans la suite du pipeline. - Enfin, la méthode
forEach()
applique la méthodeSystem.out.println()
(exprimée sous la forme d'une référence de méthode) sur chacune de ces nouvelles valeurs, ayant pour effet de les afficher à l'écran.
L'opération de transformation appliquée étant i -> i++
, on s'attendrait logiquement à ce que chacun des nombres en entrée du stream soit incrémenté, et à ce les nombres de 2 à 11 soient affichés. Or, ce n'est pas le cas. Que se passe-t-il donc ? Où est le piège ? Les lambdas seraient-elles buggées ?
En réalité, sous cet habillage tout neuf, fait de streams et de lambdas, se cache en réalité un quiz bien connu, lié à l'opérateur d'incrémentation postfix ++
!
Comme son nom l'indique, l'opérateur postfix est appliqué après que le statement complet est évalué.
Dans le cas de notre lambda, cela veut dire que pour tout nombre i
passé en entrée, l'expression i++
est évaluée en deux temps :
- Premièrement, la variable locale
i
est évaluée comme résultat de la lambda, et envoyée immédiatement dans la suite du stream ; - Puis, dans un second temps, cette variable locale
i
est incrémentée. Mais il est alors trop tard ! L'incrément est donc perdu, et ce sont donc les nombres originaux, de 1 à 10, qui s'affichent à la fin.
Ce comportement peut être également observé dans le code suivant, qui fonctionne depuis Java 1.0 :
public class LostIncrement { public static void main(String[] args) { System.out.println(increment(42)); } public static int increment(int i) { return i++; } }
Ce code affiche évidemment 42, et non pas 43 comme on pourrait le penser.
En conclusion, ne vous laissez pas troubler par les nouveautés de Java 8 ! Les lambdas, références de méthodes et méthodes par défaut apporteront sûrement leur lot de problèmes (qui pourront donner lieu à des quiz !), mais n'oubliez pas que les bons vieux quirks de Java sont toujours présents !
A bientôt !
Commentaires
Je dirais 1 à 10.
En mettant une classe anonyme à la place de la lambda, ça parait évident.
Je dirais que le code va afficher les entiers de 2 à 11, chacun sur une ligne.
2
3
4
5
6
7
8
9
10
11 ?
2
3
...
11
?
Soyons joueur, et partant du principe qu'il y a forcément un truc chelou dans le puzzler
ce code va afficher "1,2,3..,10"
parce que i++ incrémente i après l'opération courante (qui dans le cas de la closure est en gros un return) et donc retourne juste la valeur initiale.
En théorie ( et en scala ) ça devrait afficher
2 3 4 5 6 7 8 9 10 11
cependant vu que l'évaluation est lazy et que ça ne serait pas un quizz si il n'y avait pas de piège. Le piège c'est le i++ au lieu de i+1, en postfix ça évalue a la valeur d'origine avant de faire l'incrément donc je pense que ça affiche en fait
1 2 3 4 5 6 7 8 9 2 10
( j'ai volontairement omis les les retours ligne de mes réponses pour garder une taille raisonnable mais le println afficherait une colone plutôt qu'une ligne)
maintenant je n'ai qu'une hate : aller vérifier mes hypothèses :D
Je dirais que ça doit afficher :
2
3
4
5
6
7
8
9
10
11
Après s'il y a un piège, il y a de forte change que je tombe dedans...
Je dirais que ça va afficher les nombres de 1 à 10 puisque l'incrément postfixé est exécuté après avoir retourné la valeur.
C'est le même problème que dans ce code :
public class Quiz2 {
}
Qui va afficher 1.
Pour résoudre le problème il faudrait utiliser i -> ++i
Les nombres de 2 à 11 sans garantie sur l'ordre d'affichage car forEach parrallélise l'exécution
Alors je ne suis pas sûr du tout mais au ressenti je dirais :
2
3
4
5
6
7
8
9
10
11
Rah que c'est fourbe ! J'ai perdu...
Il me semble que l'incrément est exécuté *après* le statement (le "return i" implicite de la lambda)
Du coup, le map ne sert à rien dans ce cas : on devrait afficher la liste des entiers de 1 à 10
Pour faire ce qui était voulu (enfin ce que *je pense* qui était voulu, à savoir afficher les nombres de 2 à 11), remplacer le `i -> i+ +` par un `i -> + +i` ou, en bien plus compréhensible (avec autant de caractères) : `i -> i+1` :)
PS : dans les commentaires, c'est compliqué de mettre "+ +" accolés : considéré comme "je veux souligner le contenu" :)
1
..
10
`return i++` renvoie `i`, pas `i+1`
Je peux me planter mais je crois que le map(i -> i++) retourne toujours i.
Du coup ca donne la liste des entiers de 1 à 10.
Il faudrait utiliser map(i->++i) pour avoir la liste de 2 à 11.
2
3
4
5
6
7
8
9
10
11
1 2 3 4 5 6 7 8 9 10
Je dirais que cela afficher les chiffres de 1 à 10.
Je pense que le i++ est inutile, car cela ne modifie que la variable i de l'expression lambda, en retournant la valeur initial. Donc le map() ne fait rien et est équivalent à map(i -> i)
a++
Alors...
C'est du rangeClosed(), donc on va de 1 à 10.
A i, on associe la valeur retournée par un i++, donc i.
On affiche tout ça sur la sortie standard.
Donc je dirais :
1
2
3
4
5
6
7
8
9
10
Ma réponse :
2
3
4
5
6
7
8
9
10
11
Naïvement, je dirais
1
2
3
...
10
(mais c'est trop simple pour être ça, non ?)
Ca affiche :
1
2
3
4
5
6
7
8
9
10
Car l'exécution de la méthode map(i -> i) produit un stream avec les mêmes valeurs (en effet i retourne i).
Si l'on avait voulu avoir une suite 2, 3, ...11, il aurait fallu "map(i -> ++i)" par exemple.
2 3 4 5 6 7 8 9 10 11
2
3
4
5
6
7
8
9
10
11
Salut,
On aura un affichage qui peut surprendre. Certes, les chiffres de 1 à 10 seront affichés, mais l'ordre n'est pas garanti car map(i -> i++) ignore quelque fois les forEach du stream .... si j'ai bien compris la doc.
Bonne journée
A mon avis :
2
3
...
11
Moi je sais ! Moi je sais ! Moi je sais !
(à répéter en boucle... 10 fois ? ;-) Oups !)
... Java 8 n'est la que pour embrouiller :-)
@Benoit
Apres lecture de la doc, forEach respecte le caractère séquentiel/parallèle du stream.
Etant donné que rangeClosed retourne un stream séquentiel, l'affichage devrait être dans l'ordre.