Streams

Une des motivations de l'introduction des lambdas expressions est de permettre d'utiliser des collections comme des touts, sans avoir à itérer.
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 -> Collection
On 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

Création d'un Stream

On peut créer un stream à partir de différentes sources, entre autre :

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 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);
(code dans Filter2.java)
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 cats = Arrays.asList(input);        
List namesLength = cats
    .stream()
    .map(String::length)
    .collect(Collectors.toList());
System.out.println(namesLength);
(code dans Map1.java)

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"};
List cats = 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 :
xy
02
2 + 0 = 23
3 + 2 = 55
5 + 5 = 107
10 + 7 = 1711
17 + 11 = 2813
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.