Quand devrais-je utiliser les flux?

99

Je viens de tomber sur une question lors de l'utilisation de a Listet sa stream()méthode. Bien que je sache comment les utiliser, je ne sais pas trop quand les utiliser.

Par exemple, j'ai une liste, contenant divers chemins vers différents endroits. Maintenant, j'aimerais vérifier si un seul chemin donné contient l'un des chemins spécifiés dans la liste. Je voudrais retourner un booleanbasé sur le fait que la condition a été remplie ou non.

Ce n'est bien sûr pas une tâche difficile en soi. Mais je me demande si je devrais utiliser des flux ou une boucle for (-each).

La liste

private static final List<String> EXCLUDE_PATHS = Arrays.asList(new String[]{
    "my/path/one",
    "my/path/two"
});

Exemple - Stream

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream()
                        .map(String::toLowerCase)
                        .filter(path::contains)
                        .collect(Collectors.toList())
                        .size() > 0;
}

Exemple - Pour chaque boucle

private boolean isExcluded(String path){
    for (String excludePath : EXCLUDE_PATHS) {
        if(path.contains(excludePath.toLowerCase())){
            return true;
        }
    }
    return false;
}

Notez que le pathparamètre est toujours en minuscules .

Ma première hypothèse est que l'approche pour chaque est plus rapide, car la boucle reviendrait immédiatement, si la condition est remplie. Alors que le flux bouclerait toujours sur toutes les entrées de la liste afin de terminer le filtrage.

Mon hypothèse est-elle correcte? Si oui, pourquoi (ou plutôt quand ) utiliserais-je stream()alors?

mcuenez
la source
11
Les flux sont plus expressifs et lisibles que les boucles for traditionnelles. Dans ce dernier cas, vous devez faire attention aux intrinsèques de if-then et des conditions, etc. L'expression du flux est très claire: convertir les noms de fichiers en minuscules, puis filtrer par quelque chose, puis compter, collecter, etc. expression du flux des calculs.
Jean-Baptiste Yunès
12
Il n'y a pas besoin new String[]{…}ici. Juste utiliserArrays.asList("my/path/one", "my/path/two")
Holger
4
Si votre source est a String[], il n'est pas nécessaire d'appeler Arrays.asList. Vous pouvez simplement diffuser sur le tableau en utilisant Arrays.stream(array). En passant, j'ai du mal à comprendre le but du isExcludedtest dans son ensemble. Est-il vraiment intéressant de savoir si un élément de EXCLUDE_PATHSest littéralement contenu quelque part dans le chemin? Ie isExcluded("my/path/one/foo/bar/baz")reviendra true, ainsi que isExcluded("foo/bar/baz/my/path/one/")
Holger
3
Génial, je n'étais pas au courant de la Arrays.streamméthode, merci de l'avoir signalé. En effet, l'exemple que j'ai posté semble tout à fait inutile pour quelqu'un d'autre que moi. Je suis conscient du comportement de la isExcludedméthode, mais c'est vraiment juste quelque chose dont j'ai besoin pour moi, donc, pour répondre à votre question: oui , c'est intéressant pour des raisons que je ne voudrais pas mentionner, car cela ne rentrerait pas dans le champ d'application de la question initiale.
mcuenez
1
Pourquoi est-il toLowerCaseappliqué à la constante qui est déjà en minuscules? Ne devrait-il pas s'appliquer à l' pathargument?
Sebastian Redl

Réponses:

78

Votre hypothèse est correcte. Votre implémentation de flux est plus lente que la boucle for.

Cette utilisation du flux devrait être aussi rapide que la boucle for:

EXCLUDE_PATHS.stream()  
                               .map(String::toLowerCase)
                               .anyMatch(path::contains);

Cela parcourt les éléments, appliquant String::toLowerCaseet le filtre aux éléments un par un et se terminant au premier élément qui correspond.

Les deux collect()& anyMatch()sont des opérations de terminal. anyMatch()se termine au premier élément trouvé, cependant, alors collect()que tous les éléments doivent être traités.

Stefan Pries
la source
2
Génial, je ne savais pas findFirst()en combinaison avec filter(). Apparemment, je ne sais pas comment utiliser les flux aussi bien que je le pensais.
mcuenez
4
Il y a des articles de blog et des présentations vraiment intéressants sur le Web sur les performances des API de flux, que j'ai trouvé très utiles pour comprendre comment cela fonctionne sous le capot. Je peux certainement recommander un peu de recherche, si cela vous intéresse.
Stefan Pries
Après votre modification, j'ai l'impression que votre réponse est celle qui devrait être acceptée, car vous avez également répondu à ma question dans les commentaires de l'autre réponse. Cependant, j'aimerais remercier @ rvit34 d'avoir posté le code :-)
mcuenez
34

La décision d'utiliser ou non Streams ne doit pas être motivée par des considérations de performances, mais plutôt par la lisibilité. Quand il s'agit vraiment de performances, il y a d'autres considérations.

Avec votre .filter(path::contains).collect(Collectors.toList()).size() > 0approche, vous traitez tous les éléments et les collectez dans unList , avant de comparer la taille, cela n'a presque jamais d'importance pour un Stream composé de deux éléments.

L'utilisation .map(String::toLowerCase).anyMatch(path::contains)peut économiser des cycles CPU et de la mémoire, si vous avez un nombre d'éléments sensiblement plus grand. Pourtant, cela convertit chacun Stringdans sa représentation en minuscules, jusqu'à ce qu'une correspondance soit trouvée. De toute évidence, il y a un intérêt à utiliser

private static final List<String> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .collect(Collectors.toList());

private boolean isExcluded(String path) {
    return EXCLUDE_PATHS.stream().anyMatch(path::contains);
}

au lieu. Vous n'avez donc pas besoin de répéter la conversion en lowcase à chaque invocation de isExcluded. Si le nombre d'éléments EXCLUDE_PATHSou la longueur des chaînes devient vraiment important, vous pouvez envisager d'utiliser

private static final List<Predicate<String>> EXCLUDE_PATHS =
    Stream.of("my/path/one", "my/path/two").map(String::toLowerCase)
          .map(s -> Pattern.compile(s, Pattern.LITERAL).asPredicate())
          .collect(Collectors.toList());

private boolean isExcluded(String path){
    return EXCLUDE_PATHS.stream().anyMatch(p -> p.test(path));
}

Compiler une chaîne en tant que modèle regex avec le LITERALdrapeau, la fait se comporter comme des opérations de chaîne ordinaires, mais permet au moteur de passer un certain temps à se préparer, par exemple en utilisant l'algorithme de Boyer Moore, pour être plus efficace lorsqu'il s'agit de la comparaison réelle.

Bien sûr, cela ne paie que s'il y a suffisamment de tests ultérieurs pour compenser le temps passé à la préparation. Déterminer si ce sera le cas est l'une des considérations de performances réelles, outre la première question de savoir si cette opération sera un jour critique en termes de performances. Pas la question de savoir s'il faut utiliser Streams oufor boucles.

En passant, les exemples de code ci-dessus conservent la logique de votre code d'origine, ce qui me semble discutable. Votre isExcludedméthode renvoie true, si le chemin spécifié contient l'un des éléments de la liste, elle renvoie donc truepour /some/prefix/to/my/path/one, ainsi que my/path/one/and/some/suffixou même /some/prefix/to/my/path/one/and/some/suffix.

Même dummy/path/onerousest considéré comme remplissant les critères car c'est containsla chaîne my/path/one

Holger
la source
Belles informations sur l'optimisation possible des performances, merci. En ce qui concerne la dernière partie de votre réponse: si ma réponse à votre commentaire n'était pas satisfaisante, considérez mon exemple de code comme une simple aide pour que les autres comprennent ce que je demande - plutôt que comme du code réel. En outre, vous pouvez toujours modifier la question si vous avez un meilleur exemple en tête.
mcuenez
3
Je comprends votre commentaire que cette opération est ce que vous voulez vraiment, il n'est donc pas nécessaire de la changer. Je vais juste garder la dernière section pour les futurs lecteurs, afin qu'ils soient conscients que ce n'est pas une opération typique, mais aussi, qu'il a déjà été discuté et n'a pas besoin de commentaires supplémentaires ...
Holger
En fait, les flux sont parfaits à utiliser pour l'optimisation de la mémoire lorsque la quantité de mémoire de travail
dépasse la
21

Ouais. Vous avez raison. Votre approche de flux aura des frais généraux. Mais vous pouvez utiliser une telle construction:

private boolean isExcluded(String path) {
    return  EXCLUDE_PATHS.stream().map(String::toLowerCase).anyMatch(path::contains);
}

La principale raison d'utiliser les flux est qu'ils rendent votre code plus simple et facile à lire.

rvit34
la source
3
Est anyMatchun raccourci pour filter(...).findFirst().isPresent()?
mcuenez
6
Oui, ça l'est! C'est encore mieux que ma première suggestion.
Stefan Pries
8

L'objectif des flux en Java est de simplifier la complexité de l'écriture de code parallèle. Il est inspiré de la programmation fonctionnelle. Le flux série est juste pour rendre le code plus propre.

Si nous voulons des performances, nous devons utiliser parallelStream, qui a été conçu pour. La série, en général, est plus lente.

Il y a un bon article à lire , et la performance ForLoopStreamParallelStream .

Dans votre code, nous pouvons utiliser des méthodes de terminaison pour arrêter la recherche sur la première correspondance. (anyMatch ...)

Paulo Ricardo Almeida
la source
5
Notez que pour les petits flux et dans certains autres cas, un flux parallèle peut être plus lent en raison du coût de démarrage. Et si vous avez une opération de terminal ordonnée, plutôt qu'une opération parallélisable non ordonnée, resynchronisation à la fin.
CAD97
0

Comme d'autres l'ont mentionné de nombreux points positifs, mais je veux juste mentionner l' évaluation paresseuse dans l' évaluation de flux. Lorsque nous map()créons un flux de chemins en minuscules, nous ne créons pas tout le flux immédiatement, au lieu de cela, le flux est construit paresseusement , c'est pourquoi les performances devraient être équivalentes à la boucle for traditionnelle. Il ne fait pas une analyse complète map()et anyMatch()sont exécutés en même temps. Une fois anyMatch()renvoyé vrai, il sera court-circuité.

Kaicheng Hu
la source