web-dev-qa-db-fra.com

Ajouter conditionnellement une opération à un flux Java 8

Je me demande si je peux ajouter une opération à un flux, basée sur une sorte de condition définie en dehors du flux. Par exemple, je veux ajouter une opération de limitation au flux si ma variable limit n'est pas égale à -1.

Mon code ressemble actuellement à ceci, mais je n'ai pas encore vu d'autres exemples de flux utilisés de cette façon, où un objet Stream est réaffecté au résultat d'une opération intermédiaire appliquée sur lui-même:

// Do some stream stuff
stream = stream.filter(e -> e.getTimestamp() < max);

// Limit the stream
if (limit != -1) {
   stream = stream.limit(limit);
}

// Collect stream to list
stream.collect(Collectors.toList());

Comme indiqué dans ce stackoverflow post , le filtre n'est pas réellement appliqué jusqu'à ce qu'une opération de terminal soit appelée. Étant donné que je réaffecte la valeur de stream avant d'appeler une opération de terminal, le code ci-dessus est-il toujours un bon moyen d'utiliser Java 8 flux?

12
Lani

Il n'y a pas de différence sémantique entre une série chaînée d'invocations et une série d'invocations stockant les valeurs de retour intermédiaires. Ainsi, les fragments de code suivants sont équivalents:

a = object.foo();
b = a.bar();
c = b.baz();

et

c = object.foo().bar().baz();

Dans les deux cas, chaque méthode est invoquée sur le résultat de l'appel précédent. Mais dans ce dernier cas, les résultats intermédiaires ne sont pas stockés mais perdus lors de la prochaine invocation. Dans le cas de l'API de flux, les résultats intermédiaires ne doivent pas être utilisés après avoir appelé la méthode suivante dessus, donc le chaînage est le moyen naturel de l'utilisation de stream car elle garantit intrinsèquement que vous n'appelez pas plus d'une méthode sur une référence retournée.

Néanmoins, il n'est pas faux de stocker la référence dans un flux tant que vous respectez le contrat de ne pas utiliser une référence retournée plus d'une fois. En l'utilisant comme dans votre question, c'est-à-dire en écrasant la variable avec le résultat de la prochaine invocation, vous vous assurez également de ne pas invoquer plus d'une méthode sur une référence retournée, donc, c'est une utilisation correcte. Bien sûr, cela ne fonctionne qu'avec des résultats intermédiaires du même type, donc lorsque vous utilisez map ou flatMap, obtenant un flux d'un type de référence différent, vous ne pouvez pas écraser la variable locale . Ensuite, vous devez faire attention à ne pas utiliser à nouveau l'ancienne variable locale, mais, comme dit, tant que vous ne l'utilisez pas après la prochaine invocation, il n'y a rien de mal avec le stockage intermédiaire.

Parfois, vous avez pour le stocker, par ex.

try(Stream<String> stream = Files.lines(Paths.get("myFile.txt"))) {
    stream.filter(s -> !s.isEmpty()).forEach(System.out::println);
}

Notez que le code est équivalent aux alternatives suivantes:

try(Stream<String> stream = Files.lines(Paths.get("myFile.txt")).filter(s->!s.isEmpty())) {
    stream.forEach(System.out::println);
}

et

try(Stream<String> srcStream = Files.lines(Paths.get("myFile.txt"))) {
    Stream<String> tmp = srcStream.filter(s -> !s.isEmpty());
    // must not be use variable srcStream here:
    tmp.forEach(System.out::println);
}

Ils sont équivalents car forEach est toujours invoqué sur le résultat de filter qui est toujours invoqué sur le résultat de Files.lines Et peu importe sur quel résultat le final close() est appelée car la fermeture affecte l'ensemble du pipeline de flux.


Pour le mettre en une phrase, la façon dont vous l'utilisez, est correcte.


Je préfère même le faire de cette façon, car ne pas enchaîner une opération limit lorsque vous ne voulez pas appliquer de limite est le manière la plus propre d'exprimer votre intention. Il convient également de noter que les alternatives proposées peuvent fonctionner dans de nombreux cas, mais elles ne sont pas sémantiquement équivalentes:

.limit(condition? aLimit: Long.MAX_VALUE)

suppose que le nombre maximum d'éléments que vous pouvez rencontrer est Long.MAX_VALUE mais les flux peuvent avoir plus d'éléments que cela, ils peuvent même être infinis.

.limit(condition? aLimit: list.size())

lorsque la source du flux est list, rompt l'évaluation paresseuse d'un flux. En principe, une source de flux mutable peut légalement être modifiée arbitrairement jusqu'au moment où l'action terminale est lancée. Le résultat reflétera toutes les modifications apportées jusqu'à ce point. Lorsque vous ajoutez une opération intermédiaire incorporant list.size(), c'est-à-dire la taille réelle de la liste à ce stade, les modifications ultérieures appliquées à la collection entre ce point et l'opération de terminal peuvent transformer cette valeur pour avoir une signification différente de celle prévue Sémantique "en fait sans limite".

Comparer avec section "Non Interference" de la documentation de l'API :

Pour les sources de flux bien comportées, la source peut être modifiée avant le début de l'opération du terminal et ces modifications seront reflétées dans les éléments couverts. Par exemple, considérez le code suivant:

List<String> l = new ArrayList(Arrays.asList("one", "two"));
Stream<String> sl = l.stream();
l.add("three");
String s = sl.collect(joining(" "));

Tout d'abord, une liste est créée, composée de deux chaînes: "une"; et deux". Ensuite, un flux est créé à partir de cette liste. Ensuite, la liste est modifiée en ajoutant une troisième chaîne: "trois". Enfin, les éléments du flux sont collectés et réunis. Étant donné que la liste a été modifiée avant le début de l'opération de collecte du terminal, le résultat sera une chaîne "un deux trois".

Bien sûr, il s'agit d'un cas rare, car normalement, un programmeur formulera un pipeline de flux entier sans modifier la collection source entre les deux. Pourtant, les différentes sémantiques subsistent et cela peut devenir un bug très difficile à trouver lorsque vous entrez une fois dans un tel cas de coin.

De plus, comme elles ne sont pas équivalentes, l'API de flux ne reconnaîtra jamais ces valeurs comme "en fait aucune limite". Même la spécification de Long.MAX_VALUE Implique que l'implémentation du flux doit suivre le nombre d'éléments traités pour garantir que la limite a été respectée. Ainsi, ne pas ajouter d'opération limit peut avoir un avantage significatif en termes de performances par rapport à l'ajout d'une limite avec un nombre que le programmeur s'attend à ne jamais dépasser.

14
Holger

Il y a deux façons de procéder

// Do some stream stuff
List<E> results = list.stream()
                  .filter(e -> e.getTimestamp() < max);
                  .limit(limit > 0 ? limit : list.size())
                  .collect(Collectors.toList());

OR

// Do some stream stuff
stream = stream.filter(e -> e.getTimestamp() < max);

// Limit the stream
if (limit != -1) {
   stream = stream.limit(limit);
}

// Collect stream to list
List<E> results = stream.collect(Collectors.toList());

Comme il s'agit d'une programmation fonctionnelle, vous devez toujours travailler sur le résultat de chaque fonction. Vous devez spécifiquement éviter de modifier quoi que ce soit dans ce style de programmation et tout traiter comme si c'était immuable si possible.

Étant donné que je réaffecte la valeur de stream avant d'appeler une opération de terminal, le code ci-dessus est-il toujours un bon moyen d'utiliser Java 8 flux?

Cela devrait fonctionner, mais il se lit comme un mélange de codage impératif et fonctionnel. Je suggère de l'écrire comme un flux fixe selon ma première réponse.

5
Peter Lawrey

Je pense que votre première ligne doit être:

stream = stream.filter(e -> e.getTimestamp() < max);

afin que vous utilisiez le flux renvoyé par le filtre dans les opérations suivantes plutôt que le flux d'origine.

5
WillShackleford