Dans cette approche, on se concentre sur quoi faire plutôt que comment le faire.
Cette nouvelle API permet d'appliquer à des collections entières des méthodes comme
filter()
, map()
, reduce()
etc.
Collection stream API
Pour arriver à ça, les designers de java ont dû résoudre un problème : de nombreuses libs implémentant les interfaces définies par l'API collections ont été développées hors de leur contrôle, notamment des libs permettant de simuler cette nouvelle manière de travailler.Le risque de collision de nom était fort donc une nouvelle classe a été introduite :
java.util.stream.Stream<T>
D'après
Stream
javadoc :
A sequence of elements supporting sequential and parallel aggregate operations.
L'idée est d'appliquer une suite d'opérations (un pipeline), appliquées au Stream
.
Chaque opération est habituellement exprimée par une lambda expression.
A la fin du pipeline, les résultats doivent être rassemblés dans un nouveau stream.
Cela est effectué soit en utilisant un
Collector
, soit en terminant le pipeline par une "opération terminale" (comme reduce()
), qui renvoie une valeur plutôt qu'un stream.
stream() filter() map() collect() Collection -> Stream -> Stream -> Stream -> CollectionOn distingue deux types d'opérations :
Intermédiaires, comme
filter()
ou map()
, qui renvoient un nouveau stream.
Terminales, comme
sum()
.
Un pipeline est constitué d'une
source
, de 0 ou plus opérations intermédiaires, et d'une opération terminale
Caractéristiques d'un Stream
-
Pas de stockage. Contrairement à une
List
, unStream
ne stocke pas ses éléments, mais prend une source de données et applique des transformations à ses éléments.
Les sources de données peuvent être une collection, un tableau, une fonction génératrice, un channel I/O. -
Nature fonctionnelle : ne modifie pas sa source. Par exemple, filtrer un
Stream
obtenu à partir d'une collection produit un nouveauStream
sans modifier la collection. - Lazy Les opérations intermédiaires ne sont pas effectuées tant que l'opération terminale n'a pas été déclenchée.
-
Possiblement infini : certaines opérations (par ex
findFirst()
) peuvent être effectuées sur des sources de tailles infinies. -
Jetables : Les éléments d'un
Stream
ne sont visités qu'une fois. Pour les revister, il faut créer un autrestream
.
Création d'un Stream
On peut créer un stream à partir de différentes sources, entre autre :
-
Collection.stream()
-
Arrays.stream(Object[])
-
Static factory methods dans la classe
Stream
:Stream.of(Object[])
IntStream.range(int, int)
Stream.iterate(Object, UnaryOperator)
. -
BufferedReader.lines()
-
Plusieurs méthodes dans
java.nio.file.Files
filter()
Idiome appliquant du code (qui renvoie un booléen) à chaque élément de la collection, et fabrique une nouvelle collection à partir des éléments passant le test.String[] input = {"tiger", "cat", "TIGER", "Tiger", "leopard"}; String search = "tiger"; String tigers = Arrays.stream(input) .filter(s -> s.equalsIgnoreCase(search)) .collect(Collectors.joining(", ")); System.out.println(tigers);(code dans Filter1.java)
Remarquer que dans cet exemple, on a créé le stream à partir d'un tableau.
filter()
prend en paramètre une instance de l'interface java.util.function.Predicate
, interface fonctionnelle dont la méthode fonctionnelle est : boolean test(T t)
Predicate
contient d'autres méthodes permettant de combiner des prédicats.
Exemple : si on veut aussi accepter des léopards :
String[] input = {"tiger", "cat", "TIGER", "Tiger", "leopard"}; List(code dans Filter2.java)cats = Arrays.asList(input); String search = "tiger"; Predicate<String> p = s -> s.equalsIgnoreCase(search); Predicate<String> combined = p.or(s -> s.equals("leopard")); String pride = cats .stream() .filter(combined) .collect(Collectors.joining(", ")); System.out.println(pride);
Remarquer que dans cet exemple, on a converti le tableau en
List
, puis créé le stream à partir de la List
.
map()
Idiome permettant de transformer une collection en une collection d'un type potentiellement différent.map()
prend en paramètre une java.util.function.Function<T, R>
, interface fonctionnelle qui représente une fonction, dont la méthode fonctionnelle est :
R apply(T t)
T
représente le type en entrée, R
représente le type renvoyé.
Exemple :
String[] input = {"tiger", "cat", "TIGER", "Tiger", "leopard"}; List(code dans Map1.java)cats = Arrays.asList(input); List namesLength = cats .stream() .map(String::length) .collect(Collectors.toList()); System.out.println(namesLength);
Rappel :
String::length
est équivalent à s -> s.length();
("bound method reference").
forEach()
Idiome permettant de modifier une collection.forEach()
prend en paramètre un java.util.function.Consumer<T>
, interface fonctionnelle dont la méthode fonctionnelle est :
void accept(T t)
String[] input = {"tiger", "cat", "TIGER", "Tiger", "leopard"}; Listcats = Arrays.asList(input); cats.forEach(System.out::println);
forEach()
permet de modifier la collection sous-jacente (action par effet de bord, ce qui est considéré comme "mal" dans les langages purement fonctionnels).
Exercice : MapTest
Exercice : Intersection et comparaison de la rapidité d'exécution : taleaux vs
java.util.List
vs API streams.
reduce()
Permet d'effectuer des opérations d'aggrégations.T reduce(T identity, BinaryOperator<T> accumulator)-
identity
est la valeur initiale du stream
-
accumulator
est une lambda à 2 paramètres = interface fonctionnelle dont la méthode fonctionnelle est :
R apply(T t, U u)
accumulator
fabrique un total en parcourant le stream :
Il part de la valeur initiale (
identity
), la combine avec la première valeur du stream, fabrique un résultat, combine ce résultat avec la 2e valeur du stream etc.
T result = identity; for (T element : this stream) result = accumulator.apply(result, element) return result;Exemple :
int sommePremiers = Stream.of(2, 3, 5, 7, 11, 13, 17, 19, 23) .reduce(0, (x, y) -> {return x + y;}); System.out.println("Somme : " + sommePremiers);(code dans Reduce1.java)
Pour les itérations successives, on a :
x | y |
---|---|
0 | 2 |
2 + 0 = 2 | 3 |
3 + 2 = 5 | 5 |
5 + 5 = 10 | 7 |
10 + 7 = 17 | 11 |
17 + 11 = 28 | 13 |
Exercice : Créez un tableau de chaînes de caractères.
En utilisant l'API stream,
- Affichez le nombre de lettres contenues dans toutes les chaînes du tableau.
- Affichez le nombre de voyelles contenues dans toutes les chaînes du tableau.
Generators, lazy evaluation, infinite streams
cf "Java in a nutshell", 6th edition, chap 7, p 222.D'autres langages fournissent aussi ces possibilités : PHP, Python.