web-dev-qa-db-fra.com

Pourquoi filter () après que flatMap () ne soit "pas complètement" paresseux dans les flux Java?

J'ai l'exemple de code suivant:

System.out.println(
       "Result: " +
        Stream.of(1, 2, 3)
                .filter(i -> {
                    System.out.println(i);
                    return true;
                })
                .findFirst()
                .get()
);
System.out.println("-----------");
System.out.println(
       "Result: " +
        Stream.of(1, 2, 3)
                .flatMap(i -> Stream.of(i - 1, i, i + 1))
                .flatMap(i -> Stream.of(i - 1, i, i + 1))
                .filter(i -> {
                    System.out.println(i);
                    return true;
                })
                .findFirst()
                .get()
);

La sortie est la suivante:

1
Result: 1
-----------
-1
0
1
0
1
2
1
2
3
Result: -1

De là, je vois que dans le premier cas, stream se comporte vraiment paresseusement - nous utilisons findFirst() donc une fois que nous avons le premier élément, notre filtrage lambda n'est pas invoqué. Cependant, dans le deuxième cas qui utilise flatMaps, nous voyons que malgré le premier élément qui remplit la condition de filtre est trouvé (c'est juste n'importe quel premier élément car lambda retourne toujours vrai) d'autres contenus du flux sont toujours alimentés par la fonction de filtrage .

J'essaie de comprendre pourquoi il se comporte comme ça plutôt que d'abandonner après le calcul du premier élément comme dans le premier cas. Toute information utile serait appréciée.

70
Vadym S. Khondar

TL; DR, cela a été résolu dans JDK-8075939 et corrigé dans Java 10 (et rétroporté sur Java 8 = dans JDK-8225328 ).

En regardant dans l'implémentation (ReferencePipeline.Java), Nous voyons la méthode [ link ]

@Override
final void forEachWithCancel(Spliterator<P_OUT> spliterator, Sink<P_OUT> sink) {
    do { } while (!sink.cancellationRequested() && spliterator.tryAdvance(sink));
}

qui sera invoqué pour l'opération findFirst. La chose spéciale à prendre en compte est la sink.cancellationRequested() qui permet de terminer la boucle lors de la première correspondance. Comparer avec [ link ]

@Override
public final <R> Stream<R> flatMap(Function<? super P_OUT, ? extends Stream<? extends R>> mapper) {
    Objects.requireNonNull(mapper);
    // We can do better than this, by polling cancellationRequested when stream is infinite
    return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                 StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT | StreamOpFlag.NOT_SIZED) {
        @Override
        Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
            return new Sink.ChainedReference<P_OUT, R>(sink) {
                @Override
                public void begin(long size) {
                    downstream.begin(-1);
                }

                @Override
                public void accept(P_OUT u) {
                    try (Stream<? extends R> result = mapper.apply(u)) {
                        // We can do better that this too; optimize for depth=0 case and just grab spliterator and forEach it
                        if (result != null)
                            result.sequential().forEach(downstream);
                    }
                }
            };
        }
    };
}

La méthode pour faire avancer un élément finit par appeler forEach sur le sous-flux sans possibilité de terminaison antérieure et le commentaire au début de la méthode flatMap parle même de cette fonctionnalité absente.

Puisque c'est plus qu'une optimisation car cela implique que le code casse simplement lorsque le sous-flux est infini, j'espère que les développeurs prouveront bientôt qu'ils "peuvent faire mieux que ça"…


Pour illustrer les implications, tandis que Stream.iterate(0, i->i+1).findFirst() fonctionne comme prévu, Stream.of("").flatMap(x->Stream.iterate(0, i->i+1)).findFirst() finira dans une boucle infinie.

En ce qui concerne la spécification, la plupart se trouvent dans le

chapitre "Opérations de flux et pipelines" de la spécification du paquet :

Les opérations intermédiaires renvoient un nouveau flux. Ils sont toujours paresseux;

… La paresse permet également d'éviter d'examiner toutes les données lorsque cela n'est pas nécessaire; pour les opérations telles que "rechercher la première chaîne de plus de 1 000 caractères", il suffit d'examiner juste assez de chaînes pour en trouver une qui présente les caractéristiques souhaitées sans examiner toutes les chaînes disponibles à partir de la source. (Ce comportement devient encore plus important lorsque le flux d'entrée est infini et pas simplement volumineux.)

De plus, certaines opérations sont réputées court-circuitage opérations. Une opération intermédiaire court-circuite si, lorsqu'elle est présentée avec une entrée infinie, elle peut produire un flux fini en conséquence. Une opération de terminal court-circuite si, lorsqu'elle est présentée avec une entrée infinie, elle peut se terminer en temps fini. Une opération de court-circuit dans le pipeline est une condition nécessaire, mais non suffisante, pour que le traitement d'un flux infini se termine normalement en un temps fini.

Il est clair qu’une opération de court-circuit ne garantit pas une fin de durée limitée, par ex. lorsqu'un filtre ne correspond à aucun élément, le traitement ne peut pas se terminer, mais une implémentation qui ne prend en charge aucune terminaison dans un temps fini en ignorant simplement la nature de court-circuit d'une opération est loin de la spécification.

54
Holger

Les éléments du flux d'entrée sont consommés paresseusement un par un. Le premier élément, 1, est transformé par les deux flatMaps dans le flux -1, 0, 1, 0, 1, 2, 1, 2, 3, de sorte que le flux entier correspond uniquement au premier élément d'entrée. Les flux imbriqués sont matérialisés avec impatience par le pipeline, puis aplatis, puis alimentés à l'étape filter. Cela explique votre sortie.

Ce qui précède ne découle pas d'une limitation fondamentale, mais cela rendrait probablement les choses beaucoup plus compliquées pour obtenir une paresse complète pour les flux imbriqués. Je soupçonne que ce serait encore plus difficile de le rendre performant. À titre de comparaison, les séquences paresseuses de Clojure obtiennent une autre couche d'emballage pour chacun de ces niveaux d'imbrication. En raison de cette conception, les opérations peuvent même échouer avec StackOverflowError lorsque l'imbrication est exercée à l'extrême.

16
Marko Topolnik

En ce qui concerne la rupture avec des sous-flux infinis, le comportement de flatMap devient encore plus surprenant lorsque l'on lance une opération de court-circuit intermédiaire (par opposition au terminal).

Alors que ce qui suit fonctionne comme prévu, l'impression de la séquence infinie d'entiers

Stream.of("x").flatMap(_x -> Stream.iterate(1, i -> i + 1)).forEach(System.out::println);

le code suivant n'imprime que le "1", mais ne se termine pas not:

Stream.of("x").flatMap(_x -> Stream.iterate(1, i -> i + 1)).limit(1).forEach(System.out::println);

Je ne peux pas imaginer une lecture de la spécification dans laquelle ce n'était pas un bug.

8
Sebastian

Dans ma bibliothèque gratuite StreamEx j'ai présenté les collecteurs de court-circuit. Lors de la collecte d'un flux séquentiel avec un collecteur en court-circuit (comme MoreCollectors.first() ) exactement un élément est consommé de la source. En interne, il est implémenté de manière assez sale: en utilisant une exception personnalisée pour rompre le flux de contrôle. En utilisant ma bibliothèque, votre échantillon pourrait être réécrit de cette façon:

System.out.println(
        "Result: " +
                StreamEx.of(1, 2, 3)
                .flatMap(i -> Stream.of(i - 1, i, i + 1))
                .flatMap(i -> Stream.of(i - 1, i, i + 1))
                .filter(i -> {
                    System.out.println(i);
                    return true;
                })
                .collect(MoreCollectors.first())
                .get()
        );

Le résultat est le suivant:

-1
Result: -1
5
Tagir Valeev

Malheureusement, .flatMap() n'est pas paresseux. Cependant, une solution de contournement flatMap personnalisée est disponible ici: Pourquoi .flatMap () est si inefficace (non paresseux) dans Java 8 et Java 9

0
Tet

Je suis d'accord avec d'autres personnes, c'est un bug ouvert à JDK-8075939 . Et comme ce n'est toujours pas réglé plus d'un an plus tard. Je voudrais vous recommander: AbacusUtil

N.println("Result: " + Stream.of(1, 2, 3).peek(N::println).first().get());

N.println("-----------");

N.println("Result: " + Stream.of(1, 2, 3)
                        .flatMap(i -> Stream.of(i - 1, i, i + 1))
                        .flatMap(i -> Stream.of(i - 1, i, i + 1))
                        .peek(N::println).first().get());

// output:
// 1
// Result: 1
// -----------
// -1
// Result: -1

Divulgation : Je suis le développeur d'AbacusUtil.

0
user_3380739