Les Stream en JAVA 8

Depuis son introduction dans la version 1.8 de Java, les « Stream » font « Streamer » beaucoup d’encre… mais beaucoup moins de lignes de code !

Dans cet article nous verrons comment les Stream nous permettent d’effectuer des traitements sur des collections d’une manière simple et performante.

Les Stream sont souvent utilisés avec les Lambda. Les « Lambda » feront l’objet d’un autre article, nous n’expliquerons ici que le minimum afin de voir le potentiel des Stream.

Qu’est-ce qu’un Stream?14

Avant tout, un Stream n’est pas une collection ou une structure de donnée de manière générale. C’est une séquence d’éléments sur laquelle on peut effectuer un groupe d’opérations de manière séquentielle ou parallèle.

D’ailleurs l’implémentation Java définit bien une interface spécifique :

Il existe deux types d’opérations sur les Stream :

  • Les opérations intermédiaires : elles transforment un Stream en autre Stream
  • Les opérations finales : elles produisent un résultat ou un « side-effect » (on verra plus bas)

Vous pouvez consulter la définition complète de l’interface dans sa documentation

Comment les utiliser ?

À la manière du SQL sur une table , les éléments d’un Stream vont passer à travers un pipeline de prédicats, de comparateurs, de fonctions…

Prenons un exemple concrêt dans lequel nous allons manipuler des ordres d’achats et de ventes de produits. Un ordre sera soit d’achat ou de vente d’un produit quelconque (mobile, souris, vélo,…) avec un prix donné. Nous avons donc un objet Order qui définit quatre propriétés.

L’exercice consiste à récupérer dans la liste ci-dessous les produits vendus et les trier par prix croissant.

Orders

Stream JAVA 8 / Tableau de 5 colonnes (id, type, price, product) et valeurs fictives
Tableau 1 : La liste Order

Implémentation de la classe Order

Création de la liste dans notre main !

Récupération de la liste triée sans l’utilisation des stream

Voici comment nous devons procéder pour récupérer la liste sans les stream :

Récupération de la liste triée avec l’utilisation de Stream

Voyons maintenant la même récupération mais en utilisant les stream.

Alors ?

Le code parle de lui-même, vous ne trouvez pas ? 🙂

  • l’ ArrayList temporaire disparaît
  • l’application d’opérations successives sur la liste est plus claire
  • le code se réduit à une ligne
  • on verra que le parallélisme se fait très facilement avec les stream

Expliquons tout de même un peu ce code.

  • orderList.stream() renvoie un stream d’objets Order soit Stream
  • filter() permet de sélectionner dans le stream tous les éléments respectant un prédicat, en retour on obtient toujours un Stream. C’est une fonction intermédiaire.

Notons l’utilisation de la lambda o-> o.getType()==OrderType.SELL. Appliquée à chaque élément du Stream, elle vérifiera le type de l’ordre. Ici par exemple nous souhaitons ne sélectionner que les ordres de vente.

On peut imaginer ici la définition d’une fonction anonyme: Order -> boolean. Elle prend un paramètre formel (ici o) et lui associe la valeur de retour de la comparaison o.getType()==OrderType.SELL soit un boolean.

  • .sorted() va trier les éléments du Stream en utilisant un comparator passé en argument, ici encore j’ai fait le choix d’utiliser une lambda:

(o1, o2) -> o1.getPrice().compareTo(o2.getPrice())

  • .map() est bien connue en programmation fonctionnelle (voir le dossier sur la programmation fonctionnelle pour un cours de rattrapage sur ce paradigme de développement), elle va appliquer une transformation à chaque élément, ici on va récupérer le produit sur lequel porte un ordre et ainsi récupérer en retour un stream de produits, Stream.

 

JAVA 8 STREAM / Schéma descendant d'un Stream avec les différentes fonctions
Schéma 1 : Schéma du Stream 

Utilisation du parallélisme

La grande force des Stream est surement l’utilisation du parallélisme pour l’exécution des opérations.

Pour cela il suffit de récupérer un parallelStream au lieu d’un Stream. L’exemple ci-dessous devient simplement :

Les pièges à éviter

Attention toutefois à l’utilisation des parallelStream, il faut éviter certains pièges.

Tout d’abord l’exécution n’est pas déterministe au sens qu’aucune supposition ne doit être faite sur l’ordre de parcours du stream.

Les parallelStream utilisent en interne le framework Fork/Join introduit en Java 1.7.

On remarque donc de suite que le ForkJoinPool.commonPool() (Thread pool commune à toute l’application) est utilisé par défaut. Attention car tous les coeurs sont donc utilisés ! Une application web pourrait donc rester “bloquée” faute de ressources disponibles pour traiter les requêtes.

L’article suivant donne plus de détails sur le sujet.

Une solution consiste alors à utiliser un custom Thread pool :

Dans cet exemple, on appelle le constructeur en précisant le niveau de parallélisme. La valeur 4 est choisie ici de manière arbitraire. En pratique, elle dépend de l’environnement. Une bonne règle consiste à choisir cette valeur en fonction du nombre de core disponibles sur le CPU.

Les Side effects

Un Side effect est une action effectuée par une opération d’un stream modifiant quelque chose d’externe. Par exemple, modifier une variable de l’application, envoyer un message JMS, envoyer un email, modifier l’interface graphique dans une application, etc.

Toutes les opérations ne sont pas autorisées à utiliser des side effects.

Par exemple :

  • ForEach(), ForEachOrdered() et peek() renvoient void et sont donc destinées à produire un side effect.
  • Les opérations intermédiaires avec des paramètres comportementaux de type map, filter, etc.. qui ne retournent pas de valeur void ne devraient pas utiliser des side effects.

Il s’agit en tout cas de suivre les règles suivantes de la doc Java :

“Side-effects in behavioral parameters to stream operations are, in general, discouraged, as they can often lead to unwitting violations of the statelessness requirement, as well as other thread-safety hazards.”

“If the behavioral parameters do have side-effects, unless explicitly stated, there are no guarantees as to the visibility of those side-effects to other threads, nor are there any guarantees that different operations on the “same” element within the same stream pipeline are executed in the same thread”

Dans un parallelStream, les opérations peek() et forEach() ne respectent pas l’ordre et ne sont pas déterministes. On pourra utiliser les side effects si l’ordre n’est pas important. Le cas échéant, il faudra utiliser forEachOrdered().

Conclusion

L’API Stream est bien utile : elle permet de travailler de manière concise sur les collections avec une syntaxe de haut niveau (similaire aux requêtes d’un SGBD) et laisse le choix de la meilleure implémentation bas niveau à la charge de la librairie Stream. L’utilisation des stream prend toute son ampleur dans l’utilisation du parallélisme car elle nous permet de nous abstraire des Thread executor et de la gestion des Fork/Join, évitant ainsi les difficultés de synchronisation. L’utilisation des Lambdas augmente d’autant plus la concision du code. Il faut tout de même suivre les bonnes pratiques concernant les side effects et les exceptions avec les Stream si on ne veut pas tomber dans certains pièges. Nous aborderons le sujet des exceptions dans un prochain article.

Laisser un commentaire

MERITIS ICI. ET LÀ.

Carte Meritis

Meritis Finance

5 – 7, rue d’Athènes
75009 Paris

+33 (0) 1 86 95 55 00

contact@meritis.fr

Meritis PACA

Les Algorithmes – Aristote B
2000 Route des Lucioles
06901  Sophia Antipolis Cedex

+33 (0) 4 22 46 31 00

contact@meritis-paca.fr

Europarc de Pichaury – Bâtiment B5
13290 Aix-en-Provence

+33 (0) 4 22 46 31 00

contact@meritis-paca.fr

Meritis Technologies

5 – 7, rue d’Athènes
75009 Paris

contact@meritis-technologies.fr

+33 (0) 1 86 95 55 00


Contactez-nous