Nouveau tuto dev.java : https://dev.java/learn/api/streams
Ancien tuto : https://docs.oracle.com/javase/tutorial/collections/streams/index.html
- Map-Filter-Reduce algorithm
- Declarative and functional
- Itération interne, parallélisable
- Chainer les opérations sur les streams au lieu de stocker le résultat dans une variable intermédiaire car ils ne sont "consommables" qu'une seule fois (cf. l'erreur
stream has already been operated upon or closed) - 4 classes de streams :
Streampour les objets etIntStream,LongStreametDoubleStreampour les types primitifs (plus performant car pas d'autoboxing)
Fonctions courantes du package java.util.function à maîtriser pour apréhender l'API des streams :
- Consumer : utilise un objet mais ne renvoit rien
- Function<T,R> : transforme un objet en un autre
- IntFunction : spécialisation de Function, très utile pour instancier un tableau (eg.
stream.toArray(String[]::new)) - Predicate : teste un objet (renvoie un booléen)
- Bi : même chose que
Consumer,FunctionetPredicatemais qui prend deux objets en entrée au lieu d'un seul - Supplier : fournit un objet
Comparator.comparing(Person::getLastName, String.CASE_INSENSITIVE_ORDER)Map.Entry.comparingByValue()
map()flatMap(),mapMulti()filter()dropWhile(),takeWhile()skip(),limit()distinct(),sorted()
reduce(): bas niveau, à utiliser en dernier ressort car l'opération doit être associativecollect():min(),max(),count(), etc...
Opération finale avec trois formes proposées par l'API :
Forme simplifiée avec l'élément identité
Integer sum = integers.reduce(0, (a, b) -> a+b);
Integer sum = integers.reduce(0, Integer::sum);Forme simplifiée sans l'élément identité
Optional<Integer> min = integers.reduce(Integer::min);Forme générale
Stream<String> strings = Stream.of("one", "two", "three", "four");
BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;
Function<String, Integer> mapper = String::length;
BiFunction<Integer, String, Integer> accumulator = (partialReduction, element) -> partialReduction + mapper.apply(element);
int result = strings.reduce(0, accumulator, combiner);
System.out.println("sum = " + result);Méthodes :
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)<R,A> R collect(Collector<? super T,A,R> collector)toArray(String[]::new)
Plusieurs Collector "prêt à l'emploi" via la classe utilitaire Collectors :
Collectors.toList(): collecte dans uneArrayListCollectors.toUnmodifiableList(): collecte dans une liste non mutable, éq. àstream.toList()à partir de Java 16Collectors.toSet()etCollectors.toUnmodifiableSet()Collectors.toCollection(LinkedList::new)Collectors.counting(): éq. àstream.count()Collectors.joining(): seulement supporté pour les streams de stringsCollectors.partitioningBy(): collecte dans uneMap<Boolean, T>Collectors.groupingBy(): collecte dans uneMap<R, T>(plusieurs valeurs autorisées par clé)Collectors.toMap(),Collectors.toConcurrentMap(): collecte dans uneMap(une seule valeur autorisée par clé)Collectors.mapping(),Collectors.filtering(),Collectors.flatMapping(): opération intermédiaire avant de déléguer à un deuxième downstream collector, utile en combinaison avecCollectors.groupingBy()Collectors.maxBy()/minBy(),Collectors.summingInt()/summingLong()/summingDouble(),Collectors.averagingInt()/averagingLong()/averagingDouble()Collectors.teeing()(Java 12) : pour utiliser deux downstream collectors et merger le resultatCollectors.collectingAndThen(): applique un finisher à un downstream collector
Fonctionnalité permettant de créer des histogrammes en combinant les valeurs d'une map issu d'un premier Collector avec un deuxième Collector.
Exemple 1 :
Collection<String> strings =
List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
"ten", "eleven", "twelve");
Map<Integer, Long> map =
strings.stream()
.collect(
Collectors.groupingBy(
String::length,
Collectors.counting()));Exemple 2 :
Collection<String> strings =
List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
"ten", "eleven", "twelve");
Map<Integer, String> map =
strings.stream()
.collect(
Collectors.groupingBy(
String::length,
Collectors.joining(", ")));Pour les streams d'objets, il faut fournir un Comparator pour définir l'algorithme de tri.
Exemple 1 :
Stream<String> strings = Stream.of("one", "two", "three", "four");
String longest =
strings.max(Comparator.comparing(String::length))
.orElseThrow();
System.out.println("longest = " + longest);Exemple 2 :
Stream<Person> persons = ...
Person first = persons.min(Comparator.comparing(Person::getLastName, String.CASE_INSENSITIVE_ORDER))
orElseThrow();
System.out.println("first = " + first);Les Optional sont stream fluently.
Exemple d'utilisation de Stream.flatMap() + Optional.map() pour filtrer les optionals vides :
https://dev.java/learn/api/streams/optionals/#processing
Utiliser mapMulti(BiConsumer<? super T,? super Consumer<R>> mapper) au lieu de flatMap() pour valider et filtrer (Java 16)
https://dev.java/learn/api/streams/intermediate-operation/#flatmap-and-mapmulti
Avant Java 16, avec flatMap() :
Function<String, Stream<Integer>> flatParser = s -> {
try {
return Stream.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
}
return Stream.empty();
};
List<String> strings = List.of("1", " ", "2", "3 ", "", "3");
List<Integer> ints =
strings.stream()
.flatMap(flatParser)
.collect(Collectors.toList());
System.out.println("ints = " + ints);Depuis Java 16, avec mapMulti() :
List<Integer> ints =
strings.stream()
.<Integer>mapMulti((string, consumer) -> {
try {
consumer.accept(Integer.parseInt(string));
} catch (NumberFormatException ignored) {
}
})
.collect(Collectors.toList());
System.out.println("ints = " + ints);Avec mapMulti(), il y a un gain de performance car on évite de créer un Stream pour chaque élément.
Depuis Java 10, une meilleure alternative est orElseThrow() au lieu de get() pour rappeler qu'une exception est lancée s'il n'y a pas de valeur dans l'optional.
Alternative plus courte.
Note
Il y a une subtilité : toList() retourne une collection immuable tandis que collect(Collectors.toList()) utilise une ArrayList.
Utilisation de la méthode peek pour regarder au lieu de consommer.
List<String> strings = List.of("one", "two", "three", "four");
List<String> result =
strings.stream()
.peek(s -> System.out.println("Starting with = " + s))
.filter(s -> s.startsWith("t"))
.peek(s -> System.out.println("Filtered = " + s))
.map(String::toUpperCase)
.peek(s -> System.out.println("Mapped = " + s))
.collect(Collectors.toList());
System.out.println("result = " + result);Output :
Starting with = one
Starting with = two
Filtered = two
Mapped = TWO
Starting with = three
Filtered = three
Mapped = THREE
Starting with = four
result = [TWO, THREE]
- Equivalent à deux boucles imbriquées en impératif
- Map chaque élément à un stream et applanit le tout