Je viens de tomber sur une question lorsque j'utilise une méthode List
et sa méthode stream()
. Bien que je sache comment les utiliser, je ne suis pas tout à fait sûr de quand les utiliser.
Par exemple, j'ai une liste contenant divers chemins d'accès à 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 boolean
basé sur si la condition a été remplie ou non.
Bien entendu, ce n’est 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;
}
Note que le paramètre path
est toujours en minuscule .
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. Considérant que le flux serait toujours en boucle sur toutes les entrées de la liste afin de terminer le filtrage.
Est-ce que mon hypothèse est correcte? Si oui, pourquoi (ou plutôt quand ) utiliserais-je stream()
alors?
Votre hypothèse est correcte. L'implémentation de votre flux est plus lente que la boucle for.
Cette utilisation de flux devrait être aussi rapide que la boucle for cependant:
EXCLUDE_PATHS.stream()
.map(String::toLowerCase)
.anyMatch(path::contains);
Cela itère à travers les éléments, en appliquant String::toLowerCase
et le filtre aux éléments un à un et se terminant au premier élément qui correspond.
Les deux collect()
& anyMatch()
sont des opérations terminales. anyMatch()
quitte au premier élément trouvé, alors que collect()
nécessite le traitement de tous les éléments.
La décision d'utiliser ou non les Streams ne devrait pas être motivée par des considérations de performances, mais plutôt par la lisibilité. En matière de performance, il y a d'autres considérations.
Avec votre approche .filter(path::contains).collect(Collectors.toList()).size() > 0
, vous traitez tous les éléments et vous les collectez dans un List
temporaire, avant de comparer la taille, cela n'a pourtant guère d'importance pour un Stream composé de deux éléments.
L'utilisation de .map(String::toLowerCase).anyMatch(path::contains)
peut économiser des cycles de calcul et de la mémoire si vous avez un nombre d'éléments beaucoup plus important. Néanmoins, ceci convertit chaque String
en sa représentation en minuscule, jusqu'à ce qu'une correspondance soit trouvée. De toute évidence, il y a un point en utilisant
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. Il n’est donc pas nécessaire de répéter la conversion en lowcase à chaque invocation de isExcluded
. Si le nombre d'éléments dans EXCLUDE_PATHS
ou les longueurs des chaînes deviennent vraiment grands, 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));
}
La compilation d'une chaîne en tant que motif regex avec l'indicateur LITERAL
lui permet de se comporter comme des opérations de chaîne ordinaires, tout en permettant au moteur de passer un certain temps en préparation, par exemple. en utilisant l'algorithme de Boyer Moore, pour être plus efficace quand il s'agit de la comparaison réelle.
Bien entendu, cela ne porte ses fruits que s’il ya suffisamment de tests ultérieurs pour compenser le temps consacré à la préparation. Déterminer si tel sera le cas, est l’un des aspects à prendre en compte en matière de performances, outre la première question de savoir si cette opération sera un jour critique en termes de performances. Pas la question d'utiliser des flux ou des boucles for
.
En passant, les exemples de code ci-dessus conservent la logique de votre code d'origine, ce qui me semble douteux. Votre méthode isExcluded
retourne true
, si le chemin spécifié contient l'un des éléments de la liste, elle renvoie donc true
pour /some/prefix/to/my/path/one
, ainsi que my/path/one/and/some/suffix
ou même /some/prefix/to/my/path/one/and/some/suffix
.
Même dummy/path/onerous
est considéré comme remplissant les critères car il contains
la chaîne my/path/one
…
Ouais. Tu as 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 des flux est qu'ils rendent votre code plus simple et facile à lire.
Le but des flux dans Java est de simplifier la complexité d'écriture de code parallèle. C'est inspiré par la programmation fonctionnelle. Le flux série sert uniquement à rendre le code plus propre.
Si nous voulons des performances, nous devrions utiliser parallelStream, qui a été conçu pour. Le numéro de série, en général, est plus lent.
Il existe un bon article à lire sur ForLoop
, Stream
et ParallelStream
Performance .
Dans votre code, nous pouvons utiliser des méthodes de terminaison pour arrêter la recherche lors de la première correspondance. (anyMatch ...)