Java Language
Ruisseaux
Recherche…
Introduction
Un Stream
représente une séquence d'éléments et prend en charge différents types d'opérations pour effectuer des calculs sur ces éléments. Avec Java 8, l'interface de la Collection
dispose de deux méthodes pour générer un Stream
: stream()
et parallelStream()
. Stream
opérations de Stream
sont soit intermédiaires, soit terminales. Les opérations intermédiaires renvoient un Stream
afin que plusieurs opérations intermédiaires puissent être chaînées avant la fermeture du Stream
. Les opérations du terminal sont soit vides, soit renvoient un résultat non-flux.
Syntaxe
- collection.stream ()
- Arrays.stream (array)
- Stream.iterate (firstValue, currentValue -> nextValue)
- Stream.generate (() -> valeur)
- Stream.of (elementOfT [, elementOfT, ...])
- Stream.empty ()
- StreamSupport.stream (iterable.spliterator (), false)
Utiliser des flux
Un Stream
est une séquence d'éléments sur laquelle des opérations d'agrégation séquentielles et parallèles peuvent être effectuées. Un Stream
donné peut potentiellement contenir une quantité illimitée de données. En conséquence, les données reçues d'un Stream
sont traitées individuellement à son arrivée, au lieu d'effectuer un traitement par lot sur les données. Lorsqu'elles sont combinées avec des expressions lambda, elles fournissent un moyen concis d'effectuer des opérations sur des séquences de données en utilisant une approche fonctionnelle.
Exemple: ( voir ça marche sur Ideone )
Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");
fruitStream.filter(s -> s.contains("a"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
Sortie:
POMME
BANANE
ORANGE
POIRE
Les opérations effectuées par le code ci-dessus peuvent être résumées comme suit:
Créez un
Stream<String>
contenant unStream
ordonné d’éléments deString
de caractères fruit à l’aide de la méthode de fabrique statiqueStream.of(values)
.L'opération
filter()
conserve uniquement les éléments correspondant à un prédicat donné (les éléments testés par le prédicat retournent true). Dans ce cas, il conserve les éléments contenant un"a"
. Le prédicat est donné sous la forme d'une expression lambda .L'opération
map()
transforme chaque élément en utilisant une fonction donnée, appelée mappeur. Dans ce cas, chaqueString
Fruit est mappée sur sa versionString
majuscule à l'aide de la référence de méthodeString::toUppercase
.Notez que l'opération
map()
renverra un flux avec un type générique différent si la fonction de mappage renvoie un type différent de son paramètre d'entrée. Par exemple, sur unStream<String>
appel de.map(String::isEmpty)
renvoie unStream<Boolean>
L'opération sort
sorted()
trie les éléments duStream
fonction de leur ordre naturel (lexicographiquement, dans le cas de laString
).Enfin, l'opération
forEach(action)
exécute une action qui agit sur chaque élément duStream
, en le transmettant à un consommateur . Dans l'exemple, chaque élément est simplement imprimé sur la console. Cette opération est une opération de terminal, ce qui rend son fonctionnement impossible.Notez que les opérations définies sur le
Stream
sont effectuées en raison du fonctionnement du terminal. Sans opération de terminal, le flux n'est pas traité. Les flux ne peuvent pas être réutilisés. Une fois qu'une opération de terminal est appelée, l'objetStream
devient inutilisable.
Les opérations (comme vu ci-dessus) sont enchaînées pour former ce qui peut être vu comme une requête sur les données.
Fermeture des cours d'eau
Notez qu'un
Stream
n'a généralement pas besoin d'être fermé. Il est seulement nécessaire de fermer les flux qui fonctionnent sur les canaux IO. La plupart des types deStream
ne fonctionnent pas sur les ressources et ne nécessitent donc pas de fermeture.
L'interface Stream
étend AutoCloseable
. Les flux peuvent être fermés en appelant la méthode close
ou en utilisant des instructions try-with-resource.
Un exemple de cas d'utilisation où un Stream
doit être fermé est lorsque vous créez un Stream
de lignes à partir d'un fichier:
try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
lines.forEach(System.out::println);
}
L'interface Stream
déclare également la méthode Stream.onClose()
qui vous permet d'enregistrer les gestionnaires Runnable
qui seront appelés lorsque le flux sera fermé. Un exemple de cas d'utilisation est celui où le code qui produit un flux doit savoir quand il est utilisé pour effectuer un nettoyage.
public Stream<String>streamAndDelete(Path path) throws IOException {
return Files.lines(path).onClose(() -> someClass.deletePath(path));
}
Le gestionnaire d'exécution ne s'exécutera que si la méthode close()
est appelée, explicitement ou implicitement, par une instruction try-with-resources.
Commande en traitement
Le traitement d'un objet Stream
peut être séquentiel ou parallèle .
En mode séquentiel , les éléments sont traités dans l'ordre de la source du Stream
. Si le Stream
est commandé (par exemple, une implémentation SortedMap
ou une List
), le traitement est garanti pour correspondre à l'ordre de la source. Dans d'autres cas, toutefois, il convient de ne pas dépendre de la commande (voir: l'ordre d'itération keySet()
Java HashMap
keySet()
cohérent? ).
Exemple:
List<Integer> integerList = Arrays.asList(0, 1, 2, 3, 42);
// sequential
long howManyOddNumbers = integerList.stream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbers); // Output: 2
Le mode parallèle permet l'utilisation de plusieurs threads sur plusieurs cœurs, mais il n'y a aucune garantie sur l'ordre dans lequel les éléments sont traités.
Si plusieurs méthodes sont appelées sur un Stream
séquentiel, toutes les méthodes ne doivent pas être appelées. Par exemple, si un Stream
est filtré et que le nombre d'éléments est réduit à un, aucun appel ultérieur à une méthode telle que le sort
ne se produira. Cela peut augmenter les performances d'un Stream
séquentiel - une optimisation impossible avec un Stream
parallèle.
Exemple:
// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbersParallel); // Output: 2
Différences par rapport aux conteneurs (ou aux collections )
Bien que certaines actions puissent être effectuées à la fois sur les conteneurs et les flux, elles servent en fin de compte à des objectifs différents et prennent en charge différentes opérations. Les conteneurs se concentrent davantage sur la manière dont les éléments sont stockés et sur la manière dont ces éléments peuvent être utilisés efficacement. Un Stream
, d'autre part, ne fournit pas un accès et une manipulation directs à ses éléments; Il est plus dédié au groupe d'objets en tant qu'entité collective et effectue des opérations sur cette entité dans son ensemble. Stream
et Collection
sont des abstractions de haut niveau distinctes pour ces différents objectifs.
Collecte des éléments d'un flux dans une collection
Recueillir avec toList()
et toSet()
Les éléments d'un Stream
peuvent être facilement collectés dans un conteneur à l'aide de l'opération Stream.collect
:
System.out.println(Arrays
.asList("apple", "banana", "pear", "kiwi", "orange")
.stream()
.filter(s -> s.contains("a"))
.collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]
D'autres instances de collection, telles qu'un Set
, peuvent être créées à l'aide d'autres méthodes intégrées de Collectors
. Par exemple, Collectors.toSet()
collecte les éléments d'un Stream
dans un Set
.
Contrôle explicite de l'implémentation de List
ou Set
Selon la documentation de Collectors#toList()
et Collectors#toSet()
, il n’ya aucune garantie sur le type, la mutabilité, la sérialisabilité ou la sécurité des threads de la List
ou de l’ Set
renvoyés.
Pour que le contrôle explicite de l'implémentation soit renvoyé, Collectors#toCollection(Supplier)
peut être utilisé à la place, où le fournisseur donné retourne une nouvelle collection vide.
// syntax with method reference
System.out.println(strings
.stream()
.filter(s -> s != null && s.length() <= 3)
.collect(Collectors.toCollection(ArrayList::new))
);
// syntax with lambda
System.out.println(strings
.stream()
.filter(s -> s != null && s.length() <= 3)
.collect(Collectors.toCollection(() -> new LinkedHashSet<>()))
);
Collecter des éléments en utilisant toMap
Le collecteur accumule des éléments dans une carte, où la clé est l'ID de l'étudiant et la valeur est la valeur de l'étudiant.
List<Student> students = new ArrayList<Student>();
students.add(new Student(1,"test1"));
students.add(new Student(2,"test2"));
students.add(new Student(3,"test3"));
Map<Integer, String> IdToName = students.stream()
.collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(IdToName);
Sortie:
{1=test1, 2=test2, 3=test3}
Le Collectors.toMap a une autre implémentation Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
.La fonction mergeFunction est principalement utilisée pour sélectionner une nouvelle valeur ou conserver l'ancienne valeur si la clé est répétée lors de l'ajout d'un nouveau membre dans la carte à partir d'une liste.
La fonction de fusion ressemble souvent à: (s1, s2) -> s1
pour conserver la valeur correspondant à la clé répétée ou (s1, s2) -> s2
pour mettre une nouvelle valeur pour la clé répétée.
Collecte des éléments à la carte des collections
Exemple: de ArrayList à mapper <String, List <>>
Souvent, il faut créer une carte de liste à partir d'une liste primaire. Exemple: À partir d'un élève de la liste, nous devons faire une carte de la liste des matières pour chaque élève.
List<Student> list = new ArrayList<>();
list.add(new Student("Davis", SUBJECT.MATH, 35.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 12.9));
list.add(new Student("Davis", SUBJECT.GEOGRAPHY, 37.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 85.0));
list.add(new Student("Sascha", SUBJECT.MATH, 80.0));
list.add(new Student("Sascha", SUBJECT.SCIENCE, 12.0));
list.add(new Student("Sascha", SUBJECT.LITERATURE, 50.0));
list.add(new Student("Robert", SUBJECT.LITERATURE, 12.0));
Map<String, List<SUBJECT>> map = new HashMap<>();
list.stream().forEach(s -> {
map.computeIfAbsent(s.getName(), x -> new ArrayList<>()).add(s.getSubject());
});
System.out.println(map);
Sortie:
{ Robert=[LITERATURE],
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE],
Davis=[MATH, SCIENCE, GEOGRAPHY] }
Exemple: de ArrayList à Map <String, Map <>>
List<Student> list = new ArrayList<>();
list.add(new Student("Davis", SUBJECT.MATH, 1, 35.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 2, 12.9));
list.add(new Student("Davis", SUBJECT.MATH, 3, 37.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 4, 37.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 5, 85.0));
list.add(new Student("Sascha", SUBJECT.MATH, 1, 80.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 6, 12.0));
list.add(new Student("Sascha", SUBJECT.MATH, 3, 50.0));
list.add(new Student("Robert", SUBJECT.ENGLISH, 5, 12.0));
Map<String, Map<SUBJECT, List<Double>>> map = new HashMap<>();
list.stream().forEach(student -> {
map.computeIfAbsent(student.getName(), s -> new HashMap<>())
.computeIfAbsent(student.getSubject(), s -> new ArrayList<>())
.add(student.getMarks());
});
System.out.println(map);
Sortie:
{ Robert={ENGLISH=[12.0]},
Sascha={MATH=[80.0, 50.0], ENGLISH=[85.0, 12.0]},
Davis={MATH=[35.0, 37.0], SCIENCE=[12.9, 37.0]} }
Cheat-Sheet
Objectif | Code |
---|---|
Recueillir dans une List | Collectors.toList() |
Recueillir dans une ArrayList avec une taille pré-allouée | Collectors.toCollection(() -> new ArrayList<>(size)) |
Recueillir à un Set | Collectors.toSet() |
Recueillir dans un Set avec une meilleure performance d'itération | Collectors.toCollection(() -> new LinkedHashSet<>()) |
Recueillir dans un Set<String> insensible à la casse Set<String> | Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)) |
Recueillir dans un EnumSet<AnEnum> (meilleures performances pour les énumérations) | Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class)) |
Recueillir sur une Map<K,V> avec des clés uniques | Collectors.toMap(keyFunc,valFunc) |
Mappez MyObject.getter () sur un objet unique MyObject | Collectors.toMap(MyObject::getter, Function.identity()) |
Mappez MyObject.getter () sur plusieurs MyObjects | Collectors.groupingBy(MyObject::getter) |
Streams infinis
Il est possible de générer un Stream
qui ne se termine pas. L'appel d'une méthode de terminal sur un Stream
infini entraîne l'entrée du Stream
dans une boucle infinie. La méthode de limit
d'un Stream
peut être utilisée pour limiter le nombre de termes du Stream
traité par Java.
Cet exemple génère un Stream
de tous les nombres naturels, en commençant par le nombre 1. Chaque terme successif du Stream
est supérieur à celui précédent. En appelant la méthode des limites de ce Stream
, seuls les cinq premiers termes du Stream
sont pris en compte et imprimés.
// Generate infinite stream - 1, 2, 3, 4, 5, 6, 7, ...
IntStream naturalNumbers = IntStream.iterate(1, x -> x + 1);
// Print out only the first 5 terms
naturalNumbers.limit(5).forEach(System.out::println);
Sortie:
1
2
3
4
5
Une autre façon de générer un flux infini consiste à utiliser la méthode Stream.generate . Cette méthode prend un lambda de type fournisseur .
// Generate an infinite stream of random numbers
Stream<Double> infiniteRandomNumbers = Stream.generate(Math::random);
// Print out only the first 10 random numbers
infiniteRandomNumbers.limit(10).forEach(System.out::println);
Consommer des flux
Un Stream
ne sera parcouru que s'il y a une opération de terminal , comme count()
, collect()
ou forEach()
. Sinon, aucune opération sur le Stream
ne sera effectuée.
Dans l'exemple suivant, aucune opération de terminal n'est ajoutée au Stream
. Par conséquent, l'opération filter()
ne sera pas appelée et aucune sortie ne sera produite car peek()
N'EST PAS une opération de terminal .
IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);
Ceci est une séquence de Stream
avec une opération de terminal valide, donc une sortie est produite.
Vous pouvez également utiliser forEach
au lieu de peek
:
IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println);
Sortie:
2
4
6
8
Une fois l'opération du terminal effectuée, le Stream
est consommé et ne peut pas être réutilisé.
Bien qu'un objet flux donné ne peut pas être réutilisé, il est facile de créer un réutilisable Iterable
que les délégués à un pipeline de flux. Cela peut être utile pour renvoyer une vue modifiée d'un ensemble de données en direct sans avoir à collecter les résultats dans une structure temporaire.
List<String> list = Arrays.asList("FOO", "BAR");
Iterable<String> iterable = () -> list.stream().map(String::toLowerCase).iterator();
for (String str : iterable) {
System.out.println(str);
}
for (String str : iterable) {
System.out.println(str);
}
Sortie:
foo
bar
foo
bar
Cela fonctionne car Iterable
déclare une seule méthode abstraite Iterator<T> iterator()
. Cela en fait une interface fonctionnelle, implémentée par un lambda qui crée un nouveau flux à chaque appel.
En général, un Stream
fonctionne comme indiqué dans l'image suivante:
REMARQUE : Les vérifications d'argument sont toujours effectuées, même sans opération de terminal :
try {
IntStream.range(1, 10).filter(null);
} catch (NullPointerException e) {
System.out.println("We got a NullPointerException as null was passed as an argument to filter()");
}
Sortie:
Nous avons une exception NullPointerException car null a été passé en argument à filter ()
Créer une carte de fréquence
Le collecteur groupingBy(classifier, downstream)
permet la collecte d'éléments Stream
dans une Map
en classant chaque élément dans un groupe et en effectuant une opération en aval sur les éléments classés dans le même groupe.
Un exemple classique de ce principe consiste à utiliser une Map
pour compter les occurrences d'éléments dans un Stream
. Dans cet exemple, le classificateur est simplement la fonction d'identité, qui renvoie l'élément tel quel. L'opération en aval compte le nombre d'éléments égaux, en utilisant le counting()
.
Stream.of("apple", "orange", "banana", "apple")
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.forEach(System.out::println);
L'opération en aval est elle-même un collecteur ( Collectors.counting()
) qui opère sur des éléments de type String et produit un résultat de type Long
. Le résultat de l'appel de la méthode de collect
est un Map<String, Long>
.
Cela produirait la sortie suivante:
banane = 1
orange = 1
apple = 2
Flux parallèle
Remarque: Avant de décider quel Stream
utiliser, consultez le comportement ParallelStream vs Sequential Stream .
Lorsque vous souhaitez effectuer des opérations de Stream
simultanément, vous pouvez utiliser l'une de ces méthodes.
List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();
Ou:
Stream<String> aParallelStream = data.parallelStream();
Pour exécuter les opérations définies pour le flux parallèle, appelez un opérateur de terminal:
aParallelStream.forEach(System.out::println);
(A possible) sortie du Stream
parallèle:
Trois
Quatre
Un
Deux
Cinq
L'ordre peut changer car tous les éléments sont traités en parallèle (ce qui peut le rendre plus rapide). Utilisez parallelStream
lorsque la commande n'a pas d'importance.
Impact sur la performance
Dans le cas où la mise en réseau est impliquée, les Stream
parallèles peuvent dégrader les performances globales d'une application car tous les Stream
parallèles utilisent un pool de threads de jointure commune pour le réseau.
D'autre part, les Stream
parallèles peuvent améliorer considérablement les performances dans de nombreux autres cas, en fonction du nombre de cœurs disponibles dans le processeur en cours d'exécution.
Conversion d'un flux de données facultatif en un flux de valeurs
Vous devrez peut-être convertir un Stream
émetteur Optional
en un Stream
de valeurs, en n'émettant que des valeurs de Optional
existant. (c.-à-d. sans valeur null
et ne pas traiter avec Optional.empty()
).
Optional<String> op1 = Optional.empty();
Optional<String> op2 = Optional.of("Hello World");
List<String> result = Stream.of(op1, op2)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
System.out.println(result); //[Hello World]
Créer un flux
Tous les java Collection<E>
ont des méthodes stream()
et parallelStream()
partir desquelles un Stream<E>
peut être construit:
Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();
Un Stream<E>
peut être créé à partir d'un tableau en utilisant l'une des deux méthodes suivantes:
String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);
La différence entre Arrays.stream()
et Stream.of()
est que Stream.of()
a un paramètre varargs, donc il peut être utilisé comme:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Il existe également des Stream
primitifs que vous pouvez utiliser. Par exemple:
IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
Ces flux primitifs peuvent également être construits à l'aide de la méthode Arrays.stream()
:
IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });
Il est possible de créer un Stream
partir d'un tableau avec une plage spécifiée.
int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);
Notez que tout flux primitif peut être converti en flux de type boîte en utilisant la méthode des boxed
:
Stream<Integer> integerStream = intStream.boxed();
Cela peut être utile dans certains cas si vous souhaitez collecter les données, car le flux primitif ne possède aucune méthode de collect
prenant un Collector
comme argument.
Réutilisation des opérations intermédiaires d'une chaîne de flux
Le flux est fermé lorsque le terminal est appelé. Réutiliser le flux d'opérations intermédiaires, lorsque seul le fonctionnement du terminal ne fait que varier. nous pourrions créer un fournisseur de flux pour construire un nouveau flux avec toutes les opérations intermédiaires déjà configurées.
Supplier<Stream<String>> streamSupplier = () -> Stream.of("apple", "banana","orange", "grapes", "melon","blueberry","blackberry")
.map(String::toUpperCase).sorted();
streamSupplier.get().filter(s -> s.startsWith("A")).forEach(System.out::println);
// APPLE
streamSupplier.get().filter(s -> s.startsWith("B")).forEach(System.out::println);
// BANANA
// BLACKBERRY
// BLUEBERRY
int[]
tableaux int[]
peuvent être convertis en List<Integer>
aide de flux
int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());
Recherche de statistiques sur les flux numériques
Java 8 fournit des classes appelées IntSummaryStatistics
, DoubleSummaryStatistics
et LongSummaryStatistics
qui fournissent un objet d'état pour collecter des statistiques telles que count
, min
, max
, sum
et average
.
List<Integer> naturalNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = naturalNumbers.stream()
.mapToInt((x) -> x)
.summaryStatistics();
System.out.println(stats);
Ce qui entraînera:
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}
Obtenir une tranche d'un flux
Exemple: Obtenez un Stream
de 30 éléments, contenant du 21ème au 50ème élément (inclus) d'une collection.
final long n = 20L; // the number of elements to skip
final long maxSize = 30L; // the number of elements the stream should be limited to
final Stream<T> slice = collection.stream().skip(n).limit(maxSize);
Remarques:
-
IllegalArgumentException
estIllegalArgumentException
sin
est négatif oumaxSize
négatif -
skip(long)
etlimit(long)
sont des opérations intermédiaires - si un flux contient moins de
n
éléments, alorsskip(n)
renvoie un flux vide -
skip(long)
etlimit(long)
sont des opérations peu coûteuses sur des pipelines de flux séquentiels, mais peuvent coûter très cher sur des pipelines parallèles ordonnés
Concatenate Streams
Déclaration de variable pour des exemples:
Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");
Exemple 1 - Concaténer deux Stream
s
final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());
concat1.forEach(System.out::print);
// prints: abc123
Exemple 2 - Concaténer plus de deux Stream
s
final Stream<String> concat2 = Stream.concat(
Stream.concat(abc.stream(), digits.stream()),
greekAbc.stream());
System.out.println(concat2.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma
Alternativement, pour simplifier la concat()
imbriquée de concat()
les Stream
peuvent également être concaténés avec flatMap()
:
final Stream<String> concat3 = Stream.of(
abc.stream(), digits.stream(), greekAbc.stream())
.flatMap(s -> s);
// or `.flatMap(Function.identity());` (java.util.function.Function)
System.out.println(concat3.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma
Soyez prudent lors de la construction de Stream
s à partir de concaténations répétées, car l'accès à un élément d'un Stream
profondément concaténé peut entraîner des chaînes d'appel profondes ou même une StackOverflowException
.
IntStream en chaîne
Java ne possède pas de flux de caractères , donc lorsque vous travaillez avec des String
et que vous construisez un Stream
de Character
, une option consiste à obtenir un IntStream
de points de code en utilisant la méthode String.codePoints()
. Donc, IntStream
peut être obtenu comme ci-dessous:
public IntStream stringToIntStream(String in) {
return in.codePoints();
}
Il est un peu plus compliqué de faire la conversion autrement que IntStreamToString. Cela peut se faire comme suit:
public String intStreamToString(IntStream intStream) {
return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}
Trier avec Stream
List<String> data = new ArrayList<>();
data.add("Sydney");
data.add("London");
data.add("New York");
data.add("Amsterdam");
data.add("Mumbai");
data.add("California");
System.out.println(data);
List<String> sortedData = data.stream().sorted().collect(Collectors.toList());
System.out.println(sortedData);
Sortie:
[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]
Il est également possible d'utiliser un mécanisme de comparaison différent car il existe une version sorted
surchargée qui prend un comparateur comme argument.
En outre, vous pouvez utiliser une expression lambda pour le tri:
List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());
Cela produirait [Sydney, New York, Mumbai, London, California, Amsterdam]
Vous pouvez utiliser Comparator.reverseOrder()
pour avoir un comparateur qui impose l' reverse
de l'ordre naturel.
List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
Flux de primitifs
Java fournit des Stream
spécialisés pour trois types de primitives IntStream
(for int
s), LongStream
(for s long
) et DoubleStream
(for double
s). En plus d'être des implémentations optimisées pour leurs primitives respectives, elles fournissent également plusieurs méthodes de terminal spécifiques, généralement pour des opérations mathématiques. Par exemple:
IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0
Recueillir les résultats d'un flux dans un tableau
Analogue pour obtenir une collection pour un Stream
par collect()
un tableau peut être obtenu par la méthode Stream.toArray()
:
List<String> fruits = Arrays.asList("apple", "banana", "pear", "kiwi", "orange");
String[] filteredFruits = fruits.stream()
.filter(s -> s.contains("a"))
.toArray(String[]::new);
// prints: [apple, banana, pear, orange]
System.out.println(Arrays.toString(filteredFruits));
String[]::new
est un type spécial de référence de méthode: une référence de constructeur.
Trouver le premier élément qui correspond à un prédicat
Il est possible de trouver le premier élément d'un Stream
correspondant à une condition.
Pour cet exemple, nous trouverons le premier Integer
dont le carré est supérieur à 50000
.
IntStream.iterate(1, i -> i + 1) // Generate an infinite stream 1,2,3,4...
.filter(i -> (i*i) > 50000) // Filter to find elements where the square is >50000
.findFirst(); // Find the first filtered element
Cette expression retournera un OptionalInt
avec le résultat.
Notez qu'avec un Stream
infini, Java continuera à vérifier chaque élément jusqu'à ce qu'il trouve un résultat. Avec un Stream
fini, si Java est à court d'éléments mais ne peut toujours pas trouver de résultat, il retourne un OptionalInt
vide.
Utiliser IntStream pour itérer sur les index
Stream
d'éléments ne permettent généralement pas d'accéder à la valeur d'index de l'élément en cours. Pour parcourir un tableau ou ArrayList
tout en ayant accès aux index, utilisez IntStream.range(start, endExclusive)
.
String[] names = { "Jon", "Darin", "Bauke", "Hans", "Marc" };
IntStream.range(0, names.length)
.mapToObj(i -> String.format("#%d %s", i + 1, names[i]))
.forEach(System.out::println);
La méthode range(start, endExclusive)
renvoie un autre ÌntStream
et mapToObj(mapper)
renvoie un flux de String
.
Sortie:
# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc
Ceci est très similaire à l' aide d' une normale for
la boucle avec un compteur, mais avec l'avantage de pipelining et parallélisation:
for (int i = 0; i < names.length; i++) {
String newName = String.format("#%d %s", i + 1, names[i]);
System.out.println(newName);
}
Aplatir les Streams avec flatMap ()
Un Stream
d'éléments pouvant être à leur tour diffusés peut être mis à plat en un seul Stream
continu:
Un tableau de liste d'éléments peut être converti en une seule liste.
List<String> list1 = Arrays.asList("one", "two");
List<String> list2 = Arrays.asList("three","four","five");
List<String> list3 = Arrays.asList("six");
List<String> finalList = Stream.of(list1, list2, list3).flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(finalList);
// [one, two, three, four, five, six]
Une carte contenant une liste d'éléments en tant que valeurs peut être aplatie en une liste combinée
Map<String, List<Integer>> map = new LinkedHashMap<>();
map.put("a", Arrays.asList(1, 2, 3));
map.put("b", Arrays.asList(4, 5, 6));
List<Integer> allValues = map.values() // Collection<List<Integer>>
.stream() // Stream<List<Integer>>
.flatMap(List::stream) // Stream<Integer>
.collect(Collectors.toList());
System.out.println(allValues);
// [1, 2, 3, 4, 5, 6]
List
de Map
peut être mise à plat en un seul Stream
continu
List<Map<String, String>> list = new ArrayList<>();
Map<String,String> map1 = new HashMap();
map1.put("1", "one");
map1.put("2", "two");
Map<String,String> map2 = new HashMap();
map2.put("3", "three");
map2.put("4", "four");
list.add(map1);
list.add(map2);
Set<String> output= list.stream() // Stream<Map<String, String>>
.map(Map::values) // Stream<List<String>>
.flatMap(Collection::stream) // Stream<String>
.collect(Collectors.toSet()); //Set<String>
// [one, two, three,four]
Créer une carte basée sur un flux
Cas simple sans clés en double
Stream<String> characters = Stream.of("A", "B", "C");
Map<Integer, String> map = characters
.collect(Collectors.toMap(element -> element.hashCode(), element -> element));
// map = {65=A, 66=B, 67=C}
Pour rendre les choses plus déclaratives, nous pouvons utiliser la méthode statique dans Function
interface de Function.identity()
- Function.identity()
. Nous pouvons remplacer cet element -> element
lambda element -> element
par Function.identity()
.
Cas où il pourrait y avoir des clés en double
Le javadoc pour Collectors.toMap
indique:
Si les clés mis en correspondance contient des doublons (selon
Object.equals(Object)
), uneIllegalStateException
est levée lorsque l'opération de collecte est effectuée. Si les clés mappées peuvent avoir des doublons, utiliseztoMap(Function, Function, BinaryOperator)
.
Stream<String> characters = Stream.of("A", "B", "B", "C");
Map<Integer, String> map = characters
.collect(Collectors.toMap(
element -> element.hashCode(),
element -> element,
(existingVal, newVal) -> (existingVal + newVal)));
// map = {65=A, 66=BB, 67=C}
Le BinaryOperator
transmis à Collectors.toMap(...)
génère la valeur à stocker en cas de collision. Ça peut:
- renvoyer l'ancienne valeur, de sorte que la première valeur du flux prenne le pas,
- renvoyer la nouvelle valeur, de sorte que la dernière valeur du flux soit prioritaire, ou
- combiner les anciennes et les nouvelles valeurs
Regroupement par valeur
Vous pouvez utiliser Collectors.groupingBy
lorsque vous devez effectuer l'équivalent d'une opération "group by" en cascade dans une base de données. Pour illustrer cela, ce qui suit crée une carte dans laquelle les noms des personnes sont mappés aux noms de famille:
List<Person> people = Arrays.asList(
new Person("Sam", "Rossi"),
new Person("Sam", "Verdi"),
new Person("John", "Bianchi"),
new Person("John", "Rossi"),
new Person("John", "Verdi")
);
Map<String, List<String>> map = people.stream()
.collect(
// function mapping input elements to keys
Collectors.groupingBy(Person::getName,
// function mapping input elements to values,
// how to store values
Collectors.mapping(Person::getSurname, Collectors.toList()))
);
// map = {John=[Bianchi, Rossi, Verdi], Sam=[Rossi, Verdi]}
Génération de chaînes aléatoires à l'aide de flux
Il est parfois utile de créer des Strings
aléatoires, peut-être comme ID de session pour un service Web ou un mot de passe initial après l'inscription à une application. Cela peut être facilement réalisé en utilisant les Stream
.
Nous devons d'abord initialiser un générateur de nombres aléatoires. Pour améliorer la sécurité des String
générées, il est SecureRandom
utiliser SecureRandom
.
Remarque : la création d'un SecureRandom
est assez onéreuse, il est donc SecureRandom
de ne le faire qu'une seule fois et d'appeler de temps en temps l'une de ses méthodes setSeed()
pour le réamorcer.
private static final SecureRandom rng = new SecureRandom(SecureRandom.generateSeed(20));
//20 Bytes as a seed is rather arbitrary, it is the number used in the JavaDoc example
Lors de la création de String
aléatoires, nous souhaitons généralement qu'elles utilisent uniquement certains caractères (par exemple, uniquement des lettres et des chiffres). Nous pouvons donc créer une méthode renvoyant un boolean
qui pourra ensuite être utilisé pour filtrer le Stream
.
//returns true for all chars in 0-9, a-z and A-Z
boolean useThisCharacter(char c){
//check for range to avoid using all unicode Letter (e.g. some chinese symbols)
return c >= '0' && c <= 'z' && Character.isLetterOrDigit(c);
}
Ensuite, nous pouvons utiliser le RNG pour générer une chaîne aléatoire de longueur spécifique contenant le jeu de caractères qui passe notre vérification useThisCharacter
.
public String generateRandomString(long length){
//Since there is no native CharStream, we use an IntStream instead
//and convert it to a Stream<Character> using mapToObj.
//We need to specify the boundaries for the int values to ensure they can safely be cast to char
Stream<Character> randomCharStream = rng.ints(Character.MIN_CODE_POINT, Character.MAX_CODE_POINT).mapToObj(i -> (char)i).filter(c -> this::useThisCharacter).limit(length);
//now we can use this Stream to build a String utilizing the collect method.
String randomString = randomCharStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
return randomString;
}
Utilisation de flux pour implémenter des fonctions mathématiques
Stream
, et en particulier IntStream
, constituent un moyen élégant d'implémenter des termes de sommation (∑). Les plages du Stream
peuvent être utilisées comme limites de la somme.
Par exemple, l'approximation de Pi par Madhava est donnée par la formule (Source: wikipedia ):
Cela peut être calculé avec une précision arbitraire. Par exemple, pour 101 termes:
double pi = Math.sqrt(12) *
IntStream.rangeClosed(0, 100)
.mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
.sum();
Note: Avec la précision du double
, sélectionner une limite supérieure de 29 est suffisant pour obtenir un résultat indiscernable de Math.Pi
Utilisation de références de flux et de méthode pour écrire des processus auto-documentés
Les références de méthode constituent un excellent code auto-documenté, et l'utilisation de références de méthode avec Stream
facilite la lisibilité et la compréhension des processus complexes. Considérez le code suivant:
public interface Ordered {
default int getOrder(){
return 0;
}
}
public interface Valued<V extends Ordered> {
boolean hasPropertyTwo();
V getValue();
}
public interface Thing<V extends Ordered> {
boolean hasPropertyOne();
Valued<V> getValuedProperty();
}
public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
List<V> results = new ArrayList<V>();
for (Thing<V> thing : things) {
if (thing.hasPropertyOne()) {
Valued<V> valued = thing.getValuedProperty();
if (valued != null && valued.hasPropertyTwo()){
V value = valued.getValue();
if (value != null){
results.add(value);
}
}
}
}
results.sort((a, b)->{
return Integer.compare(a.getOrder(), b.getOrder());
});
return results;
}
Cette dernière méthode réécrite à l'aide de Stream
et de références de méthode est beaucoup plus lisible et chaque étape du processus est rapidement et facilement comprise - elle n'est pas seulement plus courte, elle montre également les interfaces et les classes responsables du code dans chaque étape:
public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
return things.stream()
.filter(Thing::hasPropertyOne)
.map(Thing::getValuedProperty)
.filter(Objects::nonNull)
.filter(Valued::hasPropertyTwo)
.map(Valued::getValue)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(Ordered::getOrder))
.collect(Collectors.toList());
}
Utilisation de flux de Map.Entry pour conserver les valeurs initiales après le mappage
Lorsque vous devez mapper un Stream
mais que vous souhaitez également conserver les valeurs initiales, vous pouvez mapper le Stream
vers un Map.Entry<K,V>
utilisant une méthode d’utilitaire comme celle-ci:
public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}
Vous pouvez ensuite utiliser votre convertisseur pour traiter les Stream
ayant accès aux valeurs d'origine et mappées:
Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
.map(entryMapper(transformer));
Vous pouvez ensuite continuer à traiter ce Stream
comme d'habitude. Cela évite de créer une collection intermédiaire.
Catégories d'opérations de flux
Les opérations de flux se répartissent en deux catégories principales, les opérations intermédiaires et terminales, et deux sous-catégories, sans état et avec état.
Opérations intermédiaires:
Une opération intermédiaire est toujours paresseuse , telle qu'une simple Stream.map
. Il n'est pas appelé tant que le flux n'est pas réellement consommé. Cela peut être vérifié facilement:
Arrays.asList(1, 2 ,3).stream().map(i -> {
throw new RuntimeException("not gonna happen");
return i;
});
Les opérations intermédiaires sont les blocs de construction communs d'un flux, chaînés après la source et sont généralement suivis d'une opération de terminal déclenchant la chaîne de flux.
Opérations Terminal
Les opérations terminales sont ce qui déclenche la consommation d'un flux. Les plus courants sont Stream.forEach
ou Stream.collect
. Ils sont généralement placés après une chaîne d'opérations intermédiaires et sont presque toujours impatients .
Opérations apatrides
L'apatridie signifie que chaque élément est traité sans le contexte des autres éléments. Les opérations sans état permettent un traitement efficace des flux dans la mémoire. Les opérations telles que Stream.map
et Stream.filter
ne nécessitant pas d'informations sur d'autres éléments du flux sont considérées comme étant sans état.
Opérations avec état
Statefulness signifie que l'opération sur chaque élément dépend de (certains) autres éléments du flux. Cela nécessite un état à préserver. Les opérations d'état peuvent se rompre avec des flux longs ou infinis. Les opérations telles que Stream.sorted
requièrent que l'intégralité du flux soit traitée avant que tout élément ne soit émis, ce qui entraînera un flux d'éléments suffisamment long. Cela peut être démontré par un long flux ( exécuté à vos risques et périls ):
// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);
Cela provoquera un manque de mémoire dû à l'état de Stream.sorted
:
// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);
Conversion d'un itérateur en flux
Utilisez Spliterators.spliterator()
ou Spliterators.spliteratorUnknownSize()
pour convertir un itérateur en flux:
Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);
Réduction avec des flux
La réduction est le processus consistant à appliquer un opérateur binaire à chaque élément d'un flux pour obtenir une valeur.
La méthode sum()
d'un IntStream
est un exemple de réduction; il applique une addition à chaque terme du Stream, résultant en une valeur finale:
Ceci est équivalent à (((1+2)+3)+4)
La méthode de reduce
d'un flux permet de créer une réduction personnalisée. Il est possible d'utiliser la méthode reduce
pour implémenter la méthode sum()
:
IntStream istr;
//Initialize istr
OptionalInt istr.reduce((a,b)->a+b);
La version Optional
est renvoyée afin que les flux vides puissent être traités de manière appropriée.
Un autre exemple de réduction consiste à combiner un Stream<LinkedList<T>>
en un seul LinkedList<T>
:
Stream<LinkedList<T>> listStream;
//Create a Stream<LinkedList<T>>
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
LinkedList<T> retList = new LinkedList<T>();
retList.addAll(list1);
retList.addAll(list2);
return retList;
});
Vous pouvez également fournir un élément d'identité . Par exemple, l'élément d'identité pour l'addition est 0, comme x+0==x
. Pour la multiplication, l'élément d'identité est 1, comme x*1==x
. Dans le cas ci-dessus, l'élément identity est une LinkedList<T>
vide LinkedList<T>
, car si vous ajoutez une liste vide à une autre liste, la liste à laquelle vous "ajoutez" ne change pas:
Stream<LinkedList<T>> listStream;
//Create a Stream<LinkedList<T>>
LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
LinkedList<T> retList = new LinkedList<T>();
retList.addAll(list1);
retList.addAll(list2);
return retList;
});
Notez que lorsqu'un élément d'identité est fourni, la valeur de retour n'est pas encapsulée dans un élément Optional
-si appelé sur un flux vide, reduce()
renvoie l'élément d'identité.
L'opérateur binaire doit également être associatif , ce qui signifie que (a+b)+c==a+(b+c)
. C'est parce que les éléments peuvent être réduits dans n'importe quel ordre. Par exemple, la réduction d'addition ci-dessus peut être effectuée comme suit:
Cette réduction équivaut à écrire ((1+2)+(3+4))
. La propriété d'associativité permet également à Java de réduire le Stream
en parallèle. Une partie du flux peut être réduite par chaque processeur, avec une réduction combinant le résultat de chaque processeur à la fin.
Joindre un flux à une seule chaîne
Un cas d'utilisation fréquemment rencontré consiste à créer une String
partir d'un flux, où les éléments de flux sont séparés par un certain caractère. La méthode Collectors.joining()
peut être utilisée pour cela, comme dans l'exemple suivant:
Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");
String result = fruitStream.filter(s -> s.contains("a"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", "));
System.out.println(result);
Sortie:
POMME, BANANE, ORANGE, POIRE
La méthode Collectors.joining()
peut également prendre en charge les pré-et postfixes:
String result = fruitStream.filter(s -> s.contains("e"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", ", "Fruits: ", "."));
System.out.println(result);
Sortie:
Fruits: POMME, ORANGE, POIRE.