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);
}
}
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 pourflatMap
.
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.
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:
downstream
, dans ce cas System.out::println
.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:
Cela semble prometteur, non?
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 fluxWhileOps::cancellationRequested
est interrogé après chaque élément"Sample4"
échoue le prédicat et le flux est annuléYay!
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 .
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 flatmap
ignore un drapeau d'annulation qui serait introduit par dropWhile
.
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));
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.