• 40 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

course.header.alt.is_certifying

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 05/08/2019

Manipulez vos données avec les streams

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

C'est l'une des grandes nouveauté de Java 8 ! Derrière ce nom se cache en fait un tout nouveau pattern de manipulation de données (remplaçant du pattern  Iterator  entre autre), qu'elles proviennent d'un tableau, d'une collection, d'un flux de fichier (avec   java.nio  par exemple), d'un flux réseau et j'en passe.
Avec les streams nous allons pouvoir parcourir, filtrer, manipuler, transformer nos données de façon séquentiel ou parallèles.
Les streams se trouvent dans le package  java.util.stream  et ils utilisent massivement les interfaces fonctionnelles pour appliquer des traitements.
On pourrait faire une analogie avec le langage SQL ou vous pouvez spécifier des contraintes aux données que vous recherchez dans votre base, du genre « je veux tous les membres dont l'âge est inférieur à 18 ans et qui ont posté plus de 50 messages sur le forum » : vous pouvez faire ce genre de choses facilement avec les streams sans avoir à faire tout un tas de boucles, de contrôle de traitements...
Trève de bavardage, je vous sens impatient, si nous rentrions dans le vif du sujet !

Avant de commencer

Quelques informations sont toutefois nécessaires avant de jouer avec les streams. Les streams ressemblent beaucoup aux collections que nous avons pu voir dans un chapitre précédent, cependant, elles divergent en plusieurs points :

  • Un stream ne stocke aucune donnée, il se contente de les transférer vers une suite instruction a opérer ;

  • Un stream ne modifie pas les données qu'il reçoit de sa source (flux, collection, tableau, …). S'il doit modifier des données, il doit au construire un nouveau stream.

  • Un stream est a usage unique : une fois utilisé complètement, impossible de l'utiliser une seconde fois. Si nous devons réutiliser les données d'une source une seconde fois, nous devrons recréer un second stream.

  • Un stream peut être infini si on ne clos pas le traitement dessus.

  • Les traitements fait sur un stream peuvent être de deux natures :

    • Intermédiaire : ce genre d'opération conserve le stream ouvert ce qui permet d'effectuer d'autre opérations dessus. Nous pourrons voir ceci lors de l'utilisation des méthodes  map()  ou   filter()  .

    • Terminale : c'est l'opération finale du stream, c'est ce qui lance la « consommation » du stream. La méthode reduce() en est un exemple.

Bon, ça semble un peu dur à digérer mais vous allez voir, tout ira mieux avec quelques exemples...
Pour ce faire, nous allons essentiellement utiliser les collections et un objets très simple de notre cru, que voici :

public enum Couleur {
	MARRON("marron"),
	BLEU("bleu"),
	VERT("vert"),
	VERRON("verron"),
	INCONNU("non déterminé"),
	ROUGE("rouge mais j'avais piscine...");
	
	private String name = "";
	
	Couleur(String n){name = n;}
	public String toString() {return name;}
}

public class Personne {

	public Double taille = 0.0d, poids = 0.0d;
	public String nom = "", prenom = "";
	public Couleur yeux = Couleur.INCONNU;
	public Personne() {	}
	public Personne(double taille, double poids, String nom, String prenom, Couleur yeux) {
		super();
		this.taille = taille;
		this.poids = poids;
		this.nom = nom;
		this.prenom = prenom;
		this.yeux = yeux;
	}
	
	public String toString() {
		String s = "Je m'appelle " + nom + " " + prenom;
		s += ", je pèse " + poids + " Kg";
		s += ", et je mesure " + taille + " cm.";
		return s;
	}
	public Double getTaille() {return taille;}
	public void setTaille(Double taille) {this.taille = taille;}
	public Double getPoids() {return poids;}
	public void setPoids(Double poids) {this.poids = poids;}
	public String getNom() {return nom;}
	public void setNom(String nom) {this.nom = nom;}
	public String getPrenom() {return prenom;}
	public void setPrenom(String prenom) {this.prenom = prenom;}
	public Couleur getYeux() {return yeux;}
	public void setYeux(Couleur yeux) {this.yeux = yeux;}
}


//Et une classe de test : 
import java.util.Arrays;
import java.util.List;

public class TestStream {
	public static void main(String[] args) {
		List<Personne> listP = Arrays.asList(
				new Personne(1.80, 70, "A", "Nicolas", Couleur.BLEU),
				new Personne(1.56, 50, "B", "Nicole", Couleur.VERRON),
				new Personne(1.75, 65, "C", "Germain", Couleur.VERT),
				new Personne(1.68, 50, "D", "Michel", Couleur.ROUGE),
				new Personne(1.96, 65, "E", "Cyrille", Couleur.BLEU),
				new Personne(2.10, 120, "F", "Denis", Couleur.ROUGE),
				new Personne(1.90, 90, "G", "Olivier", Couleur.VERRON)
		);		
	}
}

Nous avons notre classe de test et nous allons pouvoir commencer à travailler.

Utilisez les streams

La première chose qu'on aurait envie de faire avec cette collection serait de la parcourir... Et bien allons y, tout en stream !

Parcourir

Stream<Personne> sp = listP.stream();
sp.forEach(System.out::println);

Et voilà ! Nous avons parcouru notre collection en deux lignes de codes. Lorsque je vous disais précédemment qu'on pouvait avoir des streams de source différentes dont des collections (celles contenant un données simple, non un couple clé/valeur). Ensuite nous utilisons la méthode  forEach  de notre objet Stream : celle-ci prend un objet de type  Consumer< ? Super T>  en paramètre. Nous en profitons pour lui passer une référence de méthode pour alléger notre code et le tour est joué !

Le code ci-dessus nous permet d'obtenir :

Je m'appelle A Nicolas, je pèse 70.0 Kg, et je mesure 1.8 cm.
Je m'appelle B Nicole, je pèse 50.0 Kg, et je mesure 1.56 cm.
Je m'appelle C Germain, je pèse 65.0 Kg, et je mesure 1.75 cm.
Je m'appelle D Michel, je pèse 50.0 Kg, et je mesure 1.68 cm.
Je m'appelle E Cyrille, je pèse 65.0 Kg, et je mesure 1.96 cm.
Je m'appelle F Denis, je pèse 120.0 Kg, et je mesure 2.1 cm.
Je m'appelle G Olivier, je pèse 90.0 Kg, et je mesure 1.9 cm.

Je profite de ce bon début pour vous donner un exemple de stream infini, fournit par une méthode statique de l'interface  Stream  elle-même :  iterate()  . Nous pouvons alors parcourir ce stream avec  forEach()  et afficher son contenu avec une référence de méthode, en une seule ligne de code :

Stream.iterate(1, (x) -> x + 1).forEach(System.out::println);

Le premier paramètre de la méthode  iterate()  est le point de départ de l'itération et le second l'opération faite sur le premier paramètre. On adosse la méthode de parcours à cela et nous avons un stream infini. Il est toutefois possible de limiter le nombre d'iteration avec la méthode  limit()  , comme ceci :

Stream.iterate(2d, (x) -> x + 1).limit(100).forEach(System.out::println);

Maintenant, il n'y aura plus que cent itération. Pratique non ?
Mine de rien, nous venons également de voir que nous pouvons générer des streams depuis l'interface  Stream   : il y a également la méthode   generate()  qui permet ceci mais celle-ci prend un Supplier<T> en paramètre.

La méthode  forEach()  est une méthode terminale sur un stream : nous ne pourrons plus invoquer de méthode après celle-ci ! Elle consomme le stream.

Après la génération et le parcours, nous allons voir les opérations intermédiaires qu'il est possible de faire sur les streams.

Opérations intermédiaires sur les streams

Vous allez voir, c'est un jeu d'enfant d'utiliser ces opérations sur un objet de type  Stream  . Je vous propose de commencer par l'opération de filtrage.

Filtrage

Celle-ci se fait grâce à la méthode  filter()  qui accepte un  Predicate< ? Super T>  en paramètre. En conservant notre exemple précédent, si nous prenions maintenant uniquement les personne de plus de 50 Kg.

Stream<Personne> sp = listP.stream();
sp.forEach(System.out::println);

System.out.println("\nAprès le filtre");
sp = listP.stream();
sp.	filter(x -> x.getPoids() > 50)
	.forEach(System.out::println);

Ce qui nous donne :

Je m'appelle A Nicolas, je pèse 70.0 Kg, et je mesure 1.8 cm.
Je m'appelle B Nicole, je pèse 50.0 Kg, et je mesure 1.56 cm.
Je m'appelle C Germain, je pèse 65.0 Kg, et je mesure 1.75 cm.
Je m'appelle D Michel, je pèse 50.0 Kg, et je mesure 1.68 cm.
Je m'appelle E Cyrille, je pèse 65.0 Kg, et je mesure 1.96 cm.
Je m'appelle F Denis, je pèse 120.0 Kg, et je mesure 2.1 cm.
Je m'appelle G Olivier, je pèse 90.0 Kg, et je mesure 1.9 cm.

 Après le filtre
Je m'appelle A Nicolas, je pèse 70.0 Kg, et je mesure 1.8 cm.
Je m'appelle C Germain, je pèse 65.0 Kg, et je mesure 1.75 cm.
Je m'appelle E Cyrille, je pèse 65.0 Kg, et je mesure 1.96 cm.
Je m'appelle F Denis, je pèse 120.0 Kg, et je mesure 2.1 cm.
Je m'appelle G Olivier, je pèse 90.0 Kg, et je mesure 1.9 cm.

N'oubliez surtout pas de recréer le stream car vous le premier est entièrement consommé, sinon, vous allez recevoir ce genre de message :

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
 at java.base/java.util.stream.AbstractPipeline.<init>(Unknown Source)
 at java.base/java.util.stream.ReferencePipeline.<init>(Unknown Source)
 at java.base/java.util.stream.ReferencePipeline$StatelessOp.<init>(Unknown Source)
 at java.base/java.util.stream.ReferencePipeline$2.<init>(Unknown Source)
 at java.base/java.util.stream.ReferencePipeline.filter(Unknown Source)
 at TestStream.main(TestStream.java:25)

Bon vous avez vu, c'est simple comme un « hello world ! ». Alors continuons.

L'opération map

Ici, nous allons appliquer une opération sur chaque élément afin de ne récupérer que ce qui nous intéresse. Par exemple, en ne conservant que le poids de personnes que nous avons filtré.

Stream<Personne> sp = listP.stream();
sp.forEach(System.out::println);

System.out.println("\nAprès le filtre et le map");
sp = listP.stream();
sp.	filter(x -> x.getPoids() > 50)
	.map(x -> x.getPoids())
	.forEach(System.out::println);

Ce qui donne le résultat suivant :

Je m'appelle A Nicolas, je pèse 70.0 Kg, et je mesure 1.8 cm.
Je m'appelle B Nicole, je pèse 50.0 Kg, et je mesure 1.56 cm.
Je m'appelle C Germain, je pèse 65.0 Kg, et je mesure 1.75 cm.
Je m'appelle D Michel, je pèse 50.0 Kg, et je mesure 1.68 cm.
Je m'appelle E Cyrille, je pèse 65.0 Kg, et je mesure 1.96 cm.
Je m'appelle F Denis, je pèse 120.0 Kg, et je mesure 2.1 cm.
Je m'appelle G Olivier, je pèse 90.0 Kg, et je mesure 1.9 cm.

Après le filtre
70.0
65.0
65.0
120.0
90.0

Opérations terminales sur les streams

Nous allons rester dans la même façon de fonctionner que précédemment, vous verrez que les opérations terminales sont justes des opérations mise en fin d'instruction mais elles ressemblent trait pour trait à tout ce qu'on a déjà vu jusqu'ici. Par contre, elle consomme votre stream et donc le rende par la suite inutilisable.

Reduce

Cette opération a pour but d'agréger le contenu de votre stream pour fournir un résultat unique. Que diriez-vous d'avoir la somme des poids des personnes que nous avons filtré précédemment.

Stream<Personne> sp = listP.stream();
sp.forEach(System.out::println);

System.out.println("\nAprès le filtre et le map et reduce");
sp = listP.stream();

Double sum = sp	.filter(x -> x.getPoids() > 50)
				.map(x -> x.getPoids())
				.reduce(0.0d, (x,y) -> x+y);
System.out.println(sum);

Ce qui nous donne :

Je m'appelle A Nicolas, je pèse 70.0 Kg, et je mesure 1.8 cm.
Je m'appelle B Nicole, je pèse 50.0 Kg, et je mesure 1.56 cm.
Je m'appelle C Germain, je pèse 65.0 Kg, et je mesure 1.75 cm.
Je m'appelle D Michel, je pèse 50.0 Kg, et je mesure 1.68 cm.
Je m'appelle E Cyrille, je pèse 65.0 Kg, et je mesure 1.96 cm.
Je m'appelle F Denis, je pèse 120.0 Kg, et je mesure 2.1 cm.
Je m'appelle G Olivier, je pèse 90.0 Kg, et je mesure 1.9 cm.

Après le filtre et le map
410.0

Parfait mais, avant de poursuivre, je dois vous informer d'une chose : les opérations terminale peuvent retourner un objet apparu également avec Java 8, un objet  Optional  . C'est objet est un conteneur pour une valeur pouvant être  null  . Il possède deux états : contenant une valeur ou ne contenant rien. Voyons comment ça se passe en reprenant notre exemple un peu modifié. Pour vous montrer l'utilisation de ce type d'objet, nous utiliserons une autre méthode  reduce()  qui n'a pas la même signature que précédemment mais surtout nous allons filtrer pour être sûr de ne pas avoir de résultat et ainsi ne pas pouvoir faire la somme des poids :

System.out.println("\nAprès le filtre et le map et reduce");
sp = listP.stream();

Optional<Double> sum = sp	.filter(x -> x.getPoids() > 250)
							.map(x -> x.getPoids())
							.reduce((x,y) -> x+y);
System.out.println(sum.get());

Ceci nous retourne une belle exception :

Exception in thread "main" java.util.NoSuchElementException: No value present
 at java.base/java.util.Optional.get(Unknown Source)
 at TestStream.main(TestStream.java:27)

C'est normal puisqu'il n'y a personne qui pèse plus de 250 Kg, impossible de faire la somme. Dans ce cas, nous sommes dans l'obligation de gérer la possibilité d'absence de résultat, ce qui est une bonne chose car trop souvent omise par les développeurs. Voici comment s'y prendre :

System.out.println("\nAprès le filtre et le map et reduce");
sp = listP.stream();

Optional<Double> sum = sp	.filter(x -> x.getPoids() > 250)
							.map(x -> x.getPoids())
							.reduce((x,y) -> x+y);
if(sum.isPresent())
	System.out.println(sum.get());
else
	System.out.println("Aucun aggrégat de poids...");

La méthode   isPresent()  permet de savoir si l'objet  Optional  contient une valeur ou non, donc si notre traitement a fonctionné. Il est également possible de contrôler le résultat en utilisant une valeur par défaut en cas de problème. Regardez plutôt :

sp = listP.stream();

Optional<Double> sum = sp	.filter(x -> x.getPoids() > 250)
							.map(x -> x.getPoids())
							.reduce((x,y) -> x+y);
//Permet de gérer le cas d'erreur en renvoyant 0.0 si isPresent() == false
System.out.println(sum.orElse(0.0));

Bon, continuons avec nos opération terminales.

Count

Vous l'aurez compris, celle-ci compte le nombre d'éléments restant après les opérations précédentes.

sp = listP.stream();

long count = sp	.filter(x -> x.getPoids() > 50)
				.map(x -> x.getPoids())
				.count();

System.out.println("Nombre d'éléments : " + count);
Collect

Permet de récupérer le résultat des opérations successives sous une certaines forme. Cette forme est définie par un objet  Collectors  (implémentant l'interface  Collector  ). C'est avec cet objet que nous pourrons dire que nous souhaitons avoir notre résultat sous forme de  Set  , de  Map  , de  List  et plus encore.

Voici un code qui va nous donner une liste contenant tous les poids qui satisfont les opérations précédentes réalisées :

sp = listP.stream();

List<Double> ld = sp.filter(x -> x.getPoids() > 50)
					.map(x -> x.getPoids())
					.collect(Collectors.toList());
System.out.println(ld);

Ce qui nous retourne :

[70.0, 65.0, 65.0, 120.0, 90.0]

Il existe beaucoup d'autre opérations dont je vous laisse le soin de faire la découverte :

  • peek  : opération intermédiaire perrmettant de « déboguer » entre chaque opération.

  • limit  ou  skip  : opération également intermédiaire permettant de limiter ou bloquer des éléments d'un stream.

  • findFirst  ,  findAny  : opérations terminales qui permet de trouver des éléments particuliers d'un stream.

  • ...

Je vous avait parlé en début de chapitre que les streams peuvent provenir de source diverses, je ne me voyait pas clore ce chapitre sans vous donner un exemple d'utilisation de stream avec les fichiers.

Utiliser les stream avec NIO2

Vous l'aurez maintenant compris, il suffira d'utiliser les nouvelles méthodes retournant un objet de type   Stream<T>  et d'utiliser les méthodes que vous connaissez maintenant sur celui-ci. Bon, comme je suis sympa, voici un code d'exemple pour lire un fichier sur votre disque (pensez à changer le nom du fichier chez vous...).

String fileName = "D://Documents/IPTABLES.sh";
try(Stream<String> sf = Files.lines(Paths.get(fileName))){
	sf.forEach(System.out::println);
}catch(IOException e) {
	e.printStackTrace();
}

Vous êtes libre d'ajouter des opérations pour tester, d'ailleurs, je nous y encourage grandement.
Avant d'en terminer avec ce chapitre, vous devez savoir qu'il est également de récupérer un stream qui sera traité parallèlement sur les processeur de votre machine. Pour ce faire, au lieu d'appeler la méthode stream(), vous pouvez utiliser la méthode   parallelStream()  sur une collection ou encore   parallel()  sur les objets de type  Stream  (comme celui retourner dans notre boucle ci-dessus).

En résumé

  • Les streams permettent de faire des opérations sur des flux de données, un peu comme une requête SQL.

  • Vous pouvez afficher, trier, filtrer, calculer, ... Avec très peu de code.

  • Il existe des opération intermédiaires et des opérations terminales sur les streams.

  • Un stream ne modifie pas la données d'origine.

  • un stream peut être infini.

Exemple de certificat de réussite
Exemple de certificat de réussite