web-dev-qa-db-fra.com

takeWhile () fonctionne différemment avec flatmap

Je crée des extraits avec takeWhile pour explorer ses possibilités. Lorsqu'il est utilisé avec flatMap, le comportement n'est pas conforme aux attentes. Veuillez trouver l'extrait de code ci-dessous.

String[][] strArray = {{"Sample1", "Sample2"}, {"Sample3", "Sample4", "Sample5"}};

Arrays.stream(strArray)
        .flatMap(indStream -> Arrays.stream(indStream))
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
        .forEach(ele -> System.out.println(ele));

Sortie réelle:

Sample1
Sample2
Sample3
Sample5

Production attendue:

Sample1
Sample2
Sample3

On s’attend à ce que takeWhile s’exécute jusqu’à ce que la condition à l’intérieur devienne vraie. J'ai également ajouté des instructions d'impression dans flatmap pour le débogage. Les flux ne sont renvoyés que deux fois, ce qui est conforme aux attentes.

Cependant, cela fonctionne très bien sans flatmap dans la chaîne.

String[] strArraySingle = {"Sample3", "Sample4", "Sample5"};
Arrays.stream(strArraySingle)
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
        .forEach(ele -> System.out.println(ele));

Sortie réelle:

Sample3

Ici, la sortie réelle correspond à la sortie attendue.

Clause de non-responsabilité: ces extraits servent uniquement à la pratique du code et ne servent aucun cas d'utilisation valide.

Mise à jour: Bug JDK-8193856 : le correctif sera disponible dans JDK 10. Le changement consistera à corriger whileOps Sink :: accept

@Override 
public void accept(T t) {
    if (take = predicate.test(t)) {
        downstream.accept(t);
    }
}

Mise en œuvre modifiée:

@Override
public void accept(T t) {
    if (take && (take = predicate.test(t))) {
        downstream.accept(t);
    }
}
75
Jeevan Varughese

Il s’agit d’un bogue de JDK 9 - de numéro 8193856 :

takeWhile suppose à tort qu'une opération en amont prend en charge et honore l'annulation, ce qui n'est malheureusement pas le cas pour flatMap.

Explication

Si le flux est commandé, takeWhile devrait afficher le comportement attendu. Ce n'est pas tout à fait le cas dans votre code car vous utilisez forEach, ce qui annule l'ordre. Si vous vous en souciez, comme dans cet exemple, vous devriez utiliser forEachOrdered à la place. Chose amusante: ça ne change rien. ????

Alors peut-être que le flux n'est pas commandé en premier lieu? (Dans ce cas le comportement est correct .) Si vous créez une variable temporaire pour le flux créé à partir de strArray et vérifiez si elle est ordonnée en exécutant l'expression ((StatefulOp) stream).isOrdered(); au point d'arrêt, vous constaterez qu'il est bien commandé:

String[][] strArray = {{"Sample1", "Sample2"}, {"Sample3", "Sample4", "Sample5"}};

Stream<String> stream = Arrays.stream(strArray)
        .flatMap(indStream -> Arrays.stream(indStream))
        .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"));

// breakpoint here
System.out.println(stream);

Cela signifie que c'est très probablement une erreur d'implémentation.

Dans le code

Comme d'autres l'ont soupçonné, je pense maintenant que cela pourrait être connecté à flatMap être impatient. Plus précisément, les deux problèmes pourraient avoir la même cause fondamentale.

En regardant dans la source de WhileOps, on peut voir ces méthodes:

@Override
public void accept(T t) {
    if (take = predicate.test(t)) {
        downstream.accept(t);
    }
}

@Override
public boolean cancellationRequested() {
    return !take || downstream.cancellationRequested();
}

Ce code est utilisé par takeWhile pour vérifier pour un élément de flux donné t si le predicate est rempli:

  • Si tel est le cas, il transmet l'élément à l'opération downstream, dans ce cas System.out::println.
  • Sinon, il attribue la valeur false à take. Ainsi, lors de la prochaine demande d'annulation du pipeline (c'est-à-dire terminé), il renvoie true.

Ceci couvre l'opération takeWhile. Vous devez également savoir que forEachOrdered conduit l'opération de terminal à exécuter la méthode ReferencePipeline::forEachWithCancel:

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

Tout ça c'est:

  1. vérifier si le pipeline a été annulé
  2. sinon, avancer l'évier d'un élément
  3. arrêter si c'était le dernier élément

Cela semble prometteur, non?

Sans flatMap

Dans le "bon cas" (sans flatMap; votre deuxième exemple), forEachWithCancel agit directement sur le WhileOp comme sink et vous pouvez voir comment cela se passe :

  • ReferencePipeline::forEachWithCancel fait sa boucle:
    • WhileOps::accept reçoit chaque élément de flux
    • WhileOps::cancellationRequested est interrogé après chaque élément
  • à un moment donné "Sample4" échoue le prédicat et le flux est annulé

Yay!

Avec flatMap

Dans le "mauvais cas" (avec flatMap; votre premier exemple), forEachWithCancel fonctionne sur l'opération flatMap, qui appelle simplement forEachRemaining sur le ArraySpliterator pour {"Sample3", "Sample4", "Sample5"}, qui fait ceci:

if ((a = array).length >= (hi = fence) &&
    (i = index) >= 0 && i < (index = hi)) {
    do { action.accept((T)a[i]); } while (++i < hi);
}

En ignorant tout cela hi et fence, utilisé uniquement si le traitement du tableau est fractionné pour un flux parallèle, il s’agit d’une simple boucle for à laquelle chaque élément est passé. l'opération takeWhile, , mais ne vérifie jamais si elle est annulée . Il va donc siffler avec empressement dans tous les éléments de ce "sous-flux" avant de s’arrêter, probablement même dans le reste du flux .

54
Nicolai

Ceci est un bug, peu importe comment je le regarde - et merci Holger pour vos commentaires. Je ne voulais pas mettre cette réponse ici (sérieusement!), Mais aucune des réponses ne dit clairement qu'il s'agit d'un bug.

Les gens disent que cela doit être avec/commandé, et ce n'est pas vrai, car cela signalera true 3 fois:

Stream<String[]> s1 = Arrays.stream(strArray);
System.out.println(s1.spliterator().hasCharacteristics(Spliterator.ORDERED));

Stream<String> s2 = Arrays.stream(strArray)
            .flatMap(indStream -> Arrays.stream(indStream));
System.out.println(s2.spliterator().hasCharacteristics(Spliterator.ORDERED));

Stream<String> s3 = Arrays.stream(strArray)
            .flatMap(indStream -> Arrays.stream(indStream))
            .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"));
System.out.println(s3.spliterator().hasCharacteristics(Spliterator.ORDERED));

C'est très intéressant aussi que si vous le changez en:

String[][] strArray = { 
         { "Sample1", "Sample2" }, 
         { "Sample3", "Sample5", "Sample4" }, // Sample4 is the last one here
         { "Sample7", "Sample8" } 
};

puis Sample7 et Sample8 ne fera pas partie de la sortie, sinon ils le feront. Il semble que flatmapignore un drapeau d'annulation qui serait introduit par dropWhile.

20
Eugene

Si vous regardez la documentation pour takeWhile :

si ce flux est commandé, [retourne] un flux composé du préfixe le plus long d'éléments extraits de ce flux et correspondant au prédicat donné.

si ce flux n'est pas ordonné, [retourne] un flux constitué d'un sous-ensemble d'éléments extraits de ce flux et correspondant au prédicat donné.

Votre flux est commandé par hasard, mais takeWhile ne sait pas qu'il est. En tant que tel, il renvoie la 2ème condition - le sous-ensemble. Votre takeWhile agit simplement comme un filter .

Si vous ajoutez un appel à sorted avant takeWhile, vous verrez le résultat attendu:

Arrays.stream(strArray)
      .flatMap(indStream -> Arrays.stream(indStream))
      .sorted()
      .takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
      .forEach(ele -> System.out.println(ele));
11
Michael

La raison en est que l'opération flatMap est également une opération opérations intermédiaires avec laquelle (l'un des) stateful court-circuitant opération intermédiairetakeWhile est utilisé.

Le comportement de flatMap indiqué par Holger dans cette réponse est certainement une référence à ne pas manquer pour comprendre la sortie inattendue de telles opérations de court-circuit.

Le résultat escompté peut être obtenu en scindant ces deux opérations intermédiaires en introduisant une opération de terminal pour utiliser de manière déterministe un flux ordonné et en les effectuant pour un échantillon, de la manière suivante:

List<String> sampleList = Arrays.stream(strArray).flatMap(Arrays::stream).collect(Collectors.toList());
sampleList.stream().takeWhile(ele -> !ele.equalsIgnoreCase("Sample4"))
            .forEach(System.out::println);

En outre, il semble y avoir un bogue n ° JDK-8075939 associé pour suivre ce problème déjà enregistré.

Edit: Ceci peut être suivi plus loin à JDK-8193856 accepté comme un bug.

9
Naman