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éthode System.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

1. Le mercredi 14 mai 2014, 07:42 par ybonnel

Je dirais 1 à 10.
En mettant une classe anonyme à la place de la lambda, ça parait évident.

2. Le mercredi 14 mai 2014, 07:58 par Sebastien

Je dirais que le code va afficher les entiers de 2 à 11, chacun sur une ligne.

3. Le mercredi 14 mai 2014, 08:01 par Ben

2
3
4
5
6
7
8
9
10
11 ?

4. Le mercredi 14 mai 2014, 08:12 par rolios

2
3
...
11
?

5. Le mercredi 14 mai 2014, 09:35 par ndeloof

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.

6. Le mercredi 14 mai 2014, 09:41 par Jean

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

7. Le mercredi 14 mai 2014, 09:43 par Oni

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...

8. Le mercredi 14 mai 2014, 09:45 par Olivier

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 {

   public static void main(String[] args) {
       System.out.println(lostIncrement());
   }
   
   public static int lostIncrement() {
       int i = 1;
       return i++;
   }

}
Qui va afficher 1.

Pour résoudre le problème il faudrait utiliser i -> ++i

9. Le mercredi 14 mai 2014, 09:51 par Benoit

Les nombres de 2 à 11 sans garantie sur l'ordre d'affichage car forEach parrallélise l'exécution

10. Le mercredi 14 mai 2014, 09:59 par Jabberwock

Alors je ne suis pas sûr du tout mais au ressenti je dirais :
2
3
4
5
6
7
8
9
10
11

11. Le mercredi 14 mai 2014, 10:13 par Pierre Laporte

Rah que c'est fourbe ! J'ai perdu...

12. Le mercredi 14 mai 2014, 10:14 par fcamblor

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" :)

13. Le mercredi 14 mai 2014, 10:23 par David Gageot

1
..
10

`return i++` renvoie `i`, pas `i+1`

14. Le mercredi 14 mai 2014, 10:51 par Vincent Van Steenbergen

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.

15. Le mercredi 14 mai 2014, 11:53 par LudoMeurillon

2
3
4
5
6
7
8
9
10
11

16. Le mercredi 14 mai 2014, 11:59 par mattch69

1 2 3 4 5 6 7 8 9 10

17. Le mercredi 14 mai 2014, 12:55 par adiguba

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++

18. Le mercredi 14 mai 2014, 14:22 par Alban

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

19. Le mercredi 14 mai 2014, 14:58 par Ellène

Ma réponse :
2
3
4
5
6
7
8
9
10
11

20. Le mercredi 14 mai 2014, 15:17 par David

Naïvement, je dirais
1
2
3
...
10

(mais c'est trop simple pour être ça, non ?)

21. Le mercredi 14 mai 2014, 15:34 par OlivierP

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.

22. Le mercredi 14 mai 2014, 18:49 par mrenou

2 3 4 5 6 7 8 9 10 11

23. Le mercredi 14 mai 2014, 19:00 par jbia

2
3
4
5
6
7
8
9
10
11

24. Le jeudi 15 mai 2014, 10:08 par Samir

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

25. Le jeudi 15 mai 2014, 11:54 par Fifan31

A mon avis :

2
3
...
11

26. Le jeudi 15 mai 2014, 23:41 par HollyDays

Moi je sais ! Moi je sais ! Moi je sais !
(à répéter en boucle... 10 fois ? ;-) Oups !)

27. Le vendredi 16 mai 2014, 09:32 par Bezout

... Java 8 n'est la que pour embrouiller :-)

28. Le lundi 19 mai 2014, 18:43 par Deluxe

@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.

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.