Opérations de flux intermédiaires non évaluées sur le nombre

33

Il semble que j'ai du mal à comprendre comment Java compose les opérations de flux dans un pipeline de flux.

Lors de l'exécution du code suivant

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

La console imprime uniquement 4. L' StringBuilderobjet a toujours la valeur "".

Lorsque j'ajoute l'opération de filtrage: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

La sortie devient:

4
1234

Comment cette opération de filtrage apparemment redondante modifie-t-elle le comportement du pipeline de flux composé?

atalantus
la source
2
Intéressant !!!
uneq95
3
J'imagine que c'est un comportement spécifique à l'implémentation; c'est peut-être parce que le premier flux a une taille connue, mais pas le second, et la taille détermine si les opérations intermédiaires sont exécutées.
Andy Turner
Par intérêt, que se passe-t-il si vous inversez le filtre et la carte?
Andy Turner
Ayant programmé un peu à Haskell, ça sent un peu comme une évaluation paresseuse en cours ici. Une recherche google est revenue, les streams ont en effet une certaine paresse. Serait-ce le cas? Et sans filtre, si java est suffisamment intelligent, il n'est pas nécessaire d'exécuter réellement le mappage.
Frederik
@AndyTurner Il donne le même résultat, même en cas d'inversion
uneq95

Réponses:

39

L' count()opération de terminal, dans ma version du JDK, finit par exécuter le code suivant:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

S'il y a une filter()opération dans le pipeline d'opérations, la taille du flux, qui est connue initialement, ne peut plus être connue (car filterpourrait rejeter certains éléments du flux). Ainsi le ifbloc n'est pas exécuté, les opérations intermédiaires sont exécutées et le StringBuilder est ainsi modifié.

D'un autre côté, si vous n'avez que map()dans le pipeline, le nombre d'éléments dans le flux est garanti identique au nombre initial d'éléments. Ainsi, le bloc if est exécuté et la taille est retournée directement sans évaluer les opérations intermédiaires.

Notez que le lambda passé à map()viole le contrat défini dans la documentation: il est censé être une opération sans interférence et sans état, mais il n'est pas sans état. Donc, avoir un résultat différent dans les deux cas ne peut pas être considéré comme un bug.

JB Nizet
la source
Parce que flatMap()peut-être être en mesure de changer le nombre d'éléments, était-ce la raison pour laquelle il était initialement impatient (maintenant paresseux)? Donc, l'alternative serait d'utiliser forEach()et de compter séparément si, map()dans sa forme actuelle, il viole le contrat, je suppose.
Frederik
3
En ce qui concerne flatMap, je ne pense pas. C'était, AFAIK, parce que c'était plus simple au départ pour le rendre impatient. Oui, utiliser un flux, avec map (), pour produire des effets secondaires est une mauvaise idée.
JB Nizet
Auriez-vous une suggestion sur la façon d'obtenir la sortie complète 4 1234sans utiliser le filtre supplémentaire ou produire des effets secondaires dans l'opération map ()?
atalantus le
1
int count = array.length; String result = String.join("", array);
JB Nizet
1
ou vous pouvez utiliser forEach si vous voulez vraiment utiliser un StringBuilder, ou vous pouvez utiliserCollectors.joining("")
njzk2
19

Dans jdk-9, il était clairement documenté dans les documents java

L'élimination des effets secondaires peut également être surprenante. À l'exception des opérations de terminal pourEach et forEachOrdered, les effets secondaires des paramètres de comportement peuvent ne pas toujours être exécutés lorsque l'implémentation de flux peut optimiser l'exécution des paramètres de comportement sans affecter le résultat du calcul. (Pour un exemple spécifique, voir la note API documentée sur l' opération de comptage .)

Remarque sur l'API:

Une implémentation peut choisir de ne pas exécuter le pipeline de flux (séquentiellement ou en parallèle) si elle est capable de calculer le nombre directement à partir de la source de flux. Dans de tels cas, aucun élément source ne sera traversé et aucune opération intermédiaire ne sera évaluée. Les paramètres comportementaux avec des effets secondaires, qui sont fortement déconseillés à l'exception des cas inoffensifs tels que le débogage, peuvent être affectés. Par exemple, considérez le flux suivant:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

Le nombre d'éléments couverts par la source de flux, une liste, est connu et l'opération intermédiaire, jetez un œil, n'injecte pas ou ne supprime pas d'éléments du flux (comme cela peut être le cas pour les opérations flatMap ou de filtrage). Ainsi, le nombre correspond à la taille de la liste et il n'est pas nécessaire d'exécuter le pipeline et, comme effet secondaire, d'imprimer les éléments de la liste.

Dead Pool
la source
0

Ce n'est pas à ça que sert .map. Il est censé être utilisé pour transformer un flux de "Something" en un flux de "Something Else". Dans ce cas, vous utilisez map pour ajouter une chaîne à un Stringbuilder externe, après quoi vous avez un flux de "Stringbuilder", dont chacun a été créé par l'opération de map en ajoutant un numéro au Stringbuilder d'origine.

Votre flux ne fait rien avec les résultats mappés dans le flux, il est donc parfaitement raisonnable de supposer que l'étape peut être ignorée par le processeur de flux. Vous comptez sur les effets secondaires pour faire le travail, ce qui brise le modèle fonctionnel de la carte. Vous seriez mieux servi en utilisant forEach pour ce faire. Faites le décompte en tant que flux distinct entièrement, ou placez un compteur à l'aide d'AtomicInt dans forEach.

Le filtre l'oblige à exécuter le contenu du flux car il doit maintenant faire quelque chose de significatif sur le plan théorique avec chaque élément de flux.

DaveB
la source