Dernière modification : 23/08/2022

 Collections et Stream dans Java

Création

 

La principale nouveauté de Java 8 est l'API Stream. Elle permet de mettre en œuvre une approche de la programmation fonctionnelle en parcourant, filtrant et modifiant des données de façon séquentielle ou parallèle.

Elle se trouve dans le package java.util.stream. Plusieurs classes et interfaces fournissent une instance de cette API, comme la classe Arrays, Files ou les classes étendant l'interface Collection comme ArrayList par exemple.

Plusieurs méthodes existent pour créer des Streams, c'est ce que nous allons aborder dans cet article.

 

1. Stream vide

Il est possible de créer un Stream vide afin d'éviter des NPE ou de retourner null et complexifier le développement.

Stream myStream = Stream.empty();

Exemple de cas d'utilisation :

private Stream personsToStream(List persons){
    return persons == null ? Stream.empty() : persons.stream();
}

 

2. À partir d'une collection

Transformation d'une collection en Stream.

Collection personCollection = Arrays.asList("Alex", "Claire", "Angel");
Stream myStream = personCollection.stream();

Remarque : Plusieurs classes et interfaces utilisent l'interface Collection. Ci-dessous quelques exemples :

  • À partir d'une instante de l'interface java.util.List :
List personList = Arrays.asList("Alex", "Claire", "Angel");
Stream myStream = personList.stream();
  • À partir d'une instance de l'interface java.util.Set :
Set personSet = new HashSet<>(Arrays.asList("Alex", "Claire", "Angel"));
Stream myStream = personSet.stream();

 

3. À partir d'un tableau

Il est possible de convertir un Tableau en Stream via la class Arrays, comme suit :

String[] arrayString = new String[] {"Alex", "Claire", "Angel", null};
Stream myStream = Arrays.stream(arrayString);
myStream.forEach(System.out::println);

/**
Output:
Alex
Claire
Angel
null
*/

Attention : La méthode Arrays.stream n'est pas nullSafe, par conséquent, un NPE peut être levé si arrayString est null.

String[] arrayString = null;
Stream myStream = Arrays.stream(arrayString);
myStream.forEach(System.out::println);

/**
Output en erreur:
Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Arrays.stream(Arrays.java:5427)
	at tools.Main.main(Main.java:11)
*/

On peut éviter ce problème en utilisant la classe Optional (détaillé dans cet article), ou la méthode Stream.ofNullable, disponible à partir de java 9 (Cf. chapitre Stream.ofNullable).

// Java 8 alternative avec Optional
String[] arrayString = null;
Stream myStream = Optional.ofNullable(arrayString).map(Arrays::stream).orElseGet(Stream::empty);
myStream.forEach(System.out::println);

// Java 9
String[] arrayString =  null;
Stream myStream = Stream.ofNullable(arrayString).flatMap(Arrays::stream);
myStream.forEach(System.out::println);

// Java 9 Alternative avec Optional
String[] arrayString = null;
Stream myStream = Optional.ofNullable(arrayString).stream().flatMap(Arrays::stream);
myStream.forEach(System.out::println);

Utilisation du flatMap pour "aplatir" la liste à "streammer", passant donc de Stream> à Stream.

 

4. Les méthodes "of" et "ofNullable" (Java 9)

Fournir les éléments directement en paramètre de la méthode statique of est une alternative.

Stream myStream = Stream.of("Alex", "Claire", "Angel", null);

Attention : Même si la méthode of admet 1 ou plusieurs paramètres, si nous l'utilisons avec "un seul paramètre à null", un NPE sera levé, cependant, si plusieurs paramètres sont fournis, alors les valeurs nulles sont acceptées.

Exemple, le code suivant renvoie une erreur :

Stream myStream = Stream.of(null);

/*
Output en erreur :
Exception in thread "main" java.lang.NullPointerException
	at java.base/java.util.Arrays.stream(Arrays.java:5427)
	at java.base/java.util.stream.Stream.of(Stream.java:1188)
	at tools.Main.main(Main.java:9)
*/

Pour éviter ce type de problème, la méthode ofNullable a été introduit à partir de Java 9.

// Java9
List asList = Arrays.asList("Alex", "Claire", "Angel", null);
Stream myStream = Stream.ofNullable(asList).flatMap(List::stream);
myStream.forEach(System.out::println);

/**
Output :
Alex
Claire
Angel
null
*/

// Java9
List asList = null;
Stream myStream = Stream.ofNullable(asList).flatMap(List::stream);
myStream.forEach(System.out::println);

// Aucun output, mais aucune erreur

Remarque : Utilisation du flatMap pour "aplatir" la liste à "streammer", passant donc de Stream> à Stream.

 

5. La méthode Stream.Builder

Permet l'ajout d'élément en vue de la préparation d'un Stream (l'ordre d'ajout des éléments est conservé)

Stream myStream =  Stream.builder().add("Alex").add("Claire").add("Angel").add(null).build();

Le Stream est construit lors de l'appel à la méthode build.

 

6. La méthode Stream.generate

La méthode generate permet de créer un stream ordonné infini.

Dans l'exemple suivant, nous créons un Stream "infini" avec la méthode generate. Puis nous fixons la taille du Stream à 3 pour éviter une boucle infinie (via la méthode limit). Puis, Affichage du contenu.

Stream myStream =  Stream.generate(() -> new Random().nextLong()).limit(3);
myStream.forEach(System.out::println);

/**
Output:
-5386638481609337939
6789016581126790338
-5191696577994832266
*/

Remarque : Le Stream aurait pu être écrit de la manière suivante, qui est plus courte et plus concise en utilisant les référence des méthodes :

Stream myStream =  Stream.generate(new Random()::nextLong).limit(3);

 

7. La méthode Stream.iterate

Une autre façon de générer un stream infini est d'utiliser la méthode iterate. En premier paramètre la valeur initiale, en second une fonction qui sera appliquée à chaque tour pour obtenir la valeur de l'élément courant.

Le paramètre fourni en entrée de cette fonction est le résultat de cette dernière au précédent tour (valeur initial si premier tour).

Par conséquent, il est facile de réaliser un compteur. Ici, on initialise à 0, puis on incrément de 1 en 1. On ajoute une limite afin d'éviter une boucle infinie (via la méthode limit).

Stream myStream =  Stream.iterate(0, x -> x + 1).limit(3);
myStream.forEach(System.out::println);

/**
Output:
0
1
2
*/

 

8. Les Stream primitifs

Les Streams primitifs, sont des Streams typés (int, long, double). Pour ces types, on utilisera alors les classes IntStream, longStream et DoubleStream, qui peuvent être plus facile à manipuler que les Stream de base (Stream), car ils embarquent plusieurs outils avec eux (exemple : récupération de la valeur maximale du stream).

Ces classes facilitent également la création de Stream contenant une liste d'entier incrémentée. Cela est réalisé via les méthodes range (début, fin exclus) et rangeClosed(début, fin inclus) :

IntStream myStreamInt = IntStream.range(1, 4);
myStreamInt.forEach(System.out::println);

LongStream myStreamLong = LongStream.range(1, 4);
myStreamLong.forEach(System.out::println);

/** Output :
1
2
3
1
2
3
*/

IntStream myStreamInt = IntStream.rangeClosed(1, 4);
myStreamInt.forEach(System.out::println);

LongStream myStreamLong = LongStream.rangeClosed(1, 4);
myStreamLong.forEach(System.out::println);
/** Output :
1
2
3
4
1
2
3
4
*/

A noter que pour la classe DoubleStream, la génération d'un stream peut être réalisée via la classe Random, comme suit :

Random random = new Random();
DoubleStream myStreamDouble = random.doubles(4);
myStreamDouble.forEach(System.out::println);
/**
Output:
0.027171859265936016
0.6275420062558492
0.5934360221641584
0.9976738218178213
*/

Il est possible de passer d'un Stream à un Stream primitif à tout moment via la méthode mapToInt, mapToLong etc et inversement via mapToObj.

Rappel : Un type primitif ne peut être null. Par conséquent, si lors d'un mapping un null est transmis au Stream, un NPE sera levé.