Conférence : Java 8, Lambdas, Streams, et Collectors s’invitent au Jug de Lyon

Java 8 est sorti maintenant depuis quelques semaines et promet d’après ses développeurs d’être une grande révolution dans le monde Java. Lundi 26 mai dernier avait lieu à Lyon une conférence dans le cadre du Lyon Java User Group. José Paumard nous proposait une rapide présentation de quelques nouveautés de Java 8 : les lambdas, les Streams et les Collectors. José Paumard est maître de conférence à l’université Paris 13, Ingénieur indépendant et développeur de programmes open-sources, je vous invite à vous rendre sur son site blog.paumard.org, sur lequel il publie ses cours sur le Java. Je vais tâcher ici de vous résumer le contenu de la conférence.

Logo Java 8

L’optimisation native

Aujourd’hui, tous les livres d’informatique ont une durée de vie d’environ quelques mois. La technologies va très vite et ces ouvrages sont très vite dépassés. José Paumard souligne qu’il existe cependant quelques exception, dont un ouvrage réalisé par le Gang of Four sur les Design Patterns, ces modèles de conception, ces bonnes pratiques de développement. Les DP n’ont pas changé et pourtant, les méthodes de développement, la technologie elle bouge. Toutes les machines ont aujourd’hui plusieurs cœurs qui leur permettent de réaliser les traitements en parallèle.

Prenons un exemple simple de code que tous les développeurs ont réalisé une fois dans leur vie.

Ce code est très simple et très souvent utilisé. Cependant, imaginons que nous ayons beaucoup d’objets Person. Ce traitement va très vite prendre du temps. Il faudrait le paralléliser pour gagner du temps peut être, donc créer des Thread, gérer les synchronismes, les ressources. Tout ça pour obtenir une moyenne. Le code doit évoluer, nous devons le réécrire pour faire ce que la machine virtuelle ne peut optimiser elle-même.
Un bon contre exemple est le langage SQL : il existe depuis 40ans, n’as pas changé ou peu, mais la manière dont il est interprété évolue en fonction du moteur sous-jacent et donc ses performances ont augmenté au cours de ses années d’existence.

C’est là qu’intervient Java 8. Son objectif est simple: le développeur rédige un code simple et le compilateur et la JVM vont se charger d’optimiser un maximum ce code pour augmenter ses performances.

Un exemple simple

Nous avons vu très brièvement un des objectifs de Java 8. Nous allons voir maintenant comment cela se concrétise au niveau du langage lui-même.
Je vais partir d’un exemple. Je souhaite faire une méthode qui va prendre une liste de personnes et me faire un tri en fonction d’une condition. Comment faire passer cette condition? Je pourrais créer une classe qui hériterait d’une méthode en implémentant l’interface Condition, et ensuite, passer l’instance de cette classe à ma méthode:

Cet exemple n’est pas très pratique car il oblige à créer une interface et les classes associées à chaque cas. Cela alourdit la structure de notre projet et demande de créer beaucoup de fichiers. Il est également possible de passer par une classe anonyme qui nous enlève le poids de la déclaration des classes et donc diminue un peu la complexité.

Java 8 va nous permettre de simplifier cette écriture, et avec cela va amener de nouveaux design patterns. C’est là qu’interviennent les lambdas.

Les Lambdas

Les lambdas sont des instances d’interfaces fonctionnelles, qui ne possèdent qu’une unique méthode.

Cette interface est fonctionnelle. Il existe une annotation @FunctionnalInterface qui permet de définir une interface fonctionnelle mais cette annotation est facultative. Pour pouvoir utiliser cette lambda, il faut que la signature de ma méthode soit la suivante :

Ma méthode va donc prendre en paramètre une instance de mon interface fonctionnelle.

Ainsi, une lambda peut être passée en paramètre de myMethod, elle définira alors le corps de l’unique méthode de l’interface.

Une lambda est composée de trois élements majeurs:

  • Les paramètres (Person p) qui peuvent bien sur varier en nombre et en type. Le type peut être omis, il sera déterminé par la JVM à la compilation.
  • la flèche (->) qui indique le début du traitement
  • Le traitement en lui même (p.getAge > 20). Cette expression peut retourner une valeur comme ici, ou ne rien retourner dans le cas d’un setteur. Si le traitement est plus complexe, on peut le mettre entre accolades avec un return explicite

Le compilateur associe la lambda à la signature de la méthode de l’interface. Il y a plusieurs exigences à ce type de liens entre interface et lambda, l’interface doit obligatoirement être fonctionnelle, les paramètres de la méthode et le type de retour doivent être compatibles et les exceptions levées doivent également être compatibles.

Plusieurs simplifications peuvent être faites au niveau du code pour l’alléger et le rendre plus rapide à rédiger. Il est possible d’utiliser les Générics, d’enlever les types dans la lambda et dans le cas plus précis d’une méthode sans paramètre d’utiliser une autre forme d’écriture. Ainsi les écriture suivantes sont identiques.

Une lambda étant une instance d’interface, il est possible de la stocker sous la forme d’une variable et réutilisable dans le code. L’utilisation des Générics va permettre de rendre plus facile l’emploi de l’interface dans plusieurs cas. La gestion des types simplifiée et gérée au niveau du compilateur ne fait pas disparaître les typages forts de Java, mais lui donne une ouverture supplémentaire comme l’ont fait les Générics avec Java 5.

On voit que la lambda permet de simplifier légèrement l’écriture de nos appels. En plus de cela, elle va permettre d’augmenter les performances générales de notre application. Lorsque l’on instancie un objet afin de pouvoir utiliser une méthode, on va le créer avec le mot clé new. Cela va appelé toutes les méthodes d’initialisation, charger les méthodes et attributs statiques, appeler le constructeur de toutes les classes mère jusqu’à la classe Objet. Cette construction d’objet est un traitement lourd pour la JVM. Pour la lambda, il n’y a pas de création explicite d’une instance avec le mot new. La lambda est une zone où est stocké le code qui sera copié aux endroits voulus lors de la compilation. L’absence d’instanciation explicite permet un gain de performance notable. C’est un objet sans réel identité propre. De plus, les lambdas permettent l’utilisation importante de la parallélisation des tâches, le code étant en grande partie gérer par le compilateur et la JVM.

Afin de ne pas avoir a recréer des interfaces très similaires sur chacune de nos applications, Java 8 fournit un ensemble d’interfaces fonctionnelles couvrant une grande partie des cas principaux notamment grâce à l’utilisation des Générics. Le nouveau package java.util.fonctionnel contient 43 nouvelles interfaces fonctionnelles utilisables très facilement avec les lambdas.

Revisitons les interfaces

On le remarque, les interfaces ont été bouleversées dans cette nouvelle version de Java. Il est maintenant possible d’avoir des instances d’interfaces avec les lambdas dans des cas précis. De plus, un nouveau mot clé permet de donner une définition aux méthodes d’une interface : default. Cette définition sera alors utilisable sans avoir à faire de surcharge dans les classes l’implémentant.
Les raisons de cet ajout sont très simples. Les lambdas ont apporté énormément de modification aux classes existantes de Java, dont les interfaces majeures du langages. Or, si une nouvelle méthode était ajoutée dans l’interface Collection du langage, toutes les classes l’implémentant auraient du être modifiées, y compris les classes personnalisées créées par les développeurs. Ce qui détruit tous les programmes existants. Avec les méthodes default, les sous-classes n’ont plus l’obligation d’implémenter l’intégralité des méthodes de l’interface. Cet ajout permet la compatibilité avec tous les programmes des autres versions de Java.

Dans le cas d’une implémentation multiple qui est possible avec des interfaces, il est possible d’avoir des erreurs.

Ceci va générer une erreur car il y a deux définitions de sayToto(). Pour corriger cela, il est possible de surcharger la méthode et faire explicitement appel à l’une ou l’autre des interfaces.

Ce conflit peut également être réglé si l’une des interfaces hérite de l’autre, alors la classe utilisera la définition de l’interface la plus « basse », la plus spécifique.
On peut voir des similitudes entre ces nouvelles interfaces et les méthodes abstraites. Mais il y a encore des différences notamment le manque d’attributs et de constructeurs qui constituent un partie importante des classes abstraites. De plus, les classes abstraites ne peuvent pas être utilisées pour les lambdas qui servent de corps aux méthodes des interfaces fonctionnelles tout comme une méthode default.

Les Streams

José Paumard nous invite ensuite à nous tourner vers une importante nouveauté de Java 8: les Streams.
Les Streams sont des interfaces paramétrées, utilisant les Généric et qui apportent un concept nouveau au Java et aux traitements d’ensembles de données. Les capacités de traitement de ces objets sont très importantes. Ils comportent nativement un support pour le parallélisme. Les méthodes des objets Stream sont optimisées afin de profiter un maximum de la puissance de la JVM. Ils permettent la réalisation de multiples opérations les unes à la suite des autres sans stockage intermédiaire des résultats.

Tout d’abord, un stream ne contient pas d’objet, il se base sur une source de données (collection, socket, fichier…). Il ne peut pas modifier sa source et celle-ci peut être infinie (sur une socket par exemple on ne sait pas quelle quantité de données peut arriver). C’est un objet sur lequel on va déclarer des opérations qu’il va réaliser de manière lazy, c’est à dire sans stockage intermédiaire.

Un stream peut être obtenu à partir d’une collection, d’un tableau, d’un fichier, de la classe random avec la méthode ints() et d’autres classes du JDK comportant des méthodes de création de streams.

Les opérations des objets Stream prennent en paramètre des instances d’interfaces fonctionnelles, nous pouvons donc facilement utiliser les lambdas.

Les opérations peuvent être de deux types: intermédiaire, qui ne déclenche pas le début des opérations mais est stocké dans le stream et terminale qui va lancer toutes les opérations présentes dans le stream elle comprise (une seule opération terminale est autorisée dans le stream). Ici, forEach est une méthode terminale, sans elle, la ligne ne faisait aucun traitement, elle ne représentait qu’une déclaration.

Les streams sont composés de plusieurs états qui le caractérisent, des méthodes permettent de changer ces états (ordered, sorted, nonnull, immutable, sized,…). Par exemple la méthode distinct() va permettre d’enlever les doublons du stream, ordered() va le trier par ordre alphabétique (elle peut prendre un objet Comparator en paramètre).

La méthode parallel() permet d’indiquer au stream de réaliser les opérations parallèlement, sans avoir soi même à le faire. C’est donc le compilateur et la machine virtuelle qui vont se charger de cette parallélisation en l’optimisant un maximum notamment avec l’utilisateur des cœurs multiples de la machine. Attention cependant certaines opérations peuvent être mal parallélisée si il y a un attribut commun à tous les traitements par exemple, le fait d’y accéder de manière synchronisée va ralentir l’ensemble du mécanisme, cela s’appelle des points de contention.

Les Collectors

Je n’ai utilisé pour le moment que des méthodes simples permettant d’afficher le résultat sur la console, mais il est très facile de récupérer le résultat dans une Collection. Pour cela on utilise la méthode collect() et les Collectors. Cette classe propose 37 méthodes permettant de récupérer les résultats sous différente forme.

Ces méthode permettent de faire des traitements vraiment rapides
Exemple: Map contenant l’âge en clé et la les noms des personnes de cet âge

A la fin de la conférence, nous avons eu une session très rapide de code de la part de José Paumard qui nous démontre sur quelques tests précis la puissance de ces nouveaux outils.
Ainsi l’apparition des lambdas dans le langage va permettre à celui-ci d’évoluer. De nouveau design patterns vont apparaître et nos habitudes vont être changées notamment autour des traitements sur les collections. Les Streams vont permettre des traitements plus rapides en développement et en exécution.

Conclusion

Ici se termine mon résumé de la conférence. Comme vous pouvez le voir c’est un résumé assez long mais je souhaitais vraiment vous amenez sur le même cheminement de pensée sur lequel José Paumard nous a emmené. On voit à travers son discours qu’il a vraiment la passion du langage et qu’il aurait sûrement pu resté quelques heures à nous partager cette passion et à nous décrire ces nouveaux concepts. Ces derniers sont d’ailleurs tellement importants qu’ils ne sauraient tenir sur cet article. De mon côté, j’ai été particulièrement intéressé par le coté performance de cette nouvelle version de Java qui nous promet de beaux jours devant nous.

spacer

Laisser un commentaire