jeudi 25 septembre 2014

Introduction Java 8 : Stream, Expression lambda, Interface fonctionnelle

Parmi les nouveaux concepts introduits dans Java 8, nous pouvons particulièrement souligner les expressions lambda qui ont connu un développement considérable pour ses rapports étroits avec les langages de programmation fonctionnelle. Son intérêt principal provient de la simplicité de sa syntaxe.
L’introduction des expressions lambda n’aurait pas été utile si l’API Collection n’avait été revue. Il faut se rappeler que L’API Collection est la plus utilisée du JDK. Conçue il y a un peu plus de 15 ans, elle est encore aujourd’hui au cœur de toutes les applications Java. Avec l’introduction de la genericité en 2004, elle a subi sa toute première mise à jour. Cette mise à jour, bien qu’importante, n’a cependant pas modifié ses patterns d’utilisation basés essentiellement sur les Iterator. Avec l’introduction des expressions lambda, l’API Collection a été entièrement révisée en introduisant les Stream, qui, ajoutée à la notion de Collector, rend obsolète le pattern Iterator.
Les expressions lambda : Quel est le besoin ?
Nous allons implémenter une classe permettant d’effectuer des opérations arithmétiques sur des nombres entiers (addition, soustraction, multiplication). Pour de raison de clarté et simplicité, les opérations se portent sur deux  nombres.

Commençons par créer une interface Operation contenant une méthode calculer prenant 2 nombres entiers en paramètre :

 Ensuite nous implémentons les classes pour chaque opération (addition, soustraction, multiplication) :

Imaginons maintenant une méthode statique prenant en paramètre : deux entiers et une opération à calculer :

Notre classe pour effectuer les calculs :


Que peut-on remarquer ? Nous avons défini une interface et une classe pour chaque opération. L’ajout d’une nouvelle opération (par exemple la division) nécessite la création d’une nouvelle classe pour l’implémenter.
Grace aux expressions lambda, nous allons simplifier le code de notre calculatrice. L’utilisation des expressions lambda est une technique pour écrire des fonctions anonyme au sens où son prototype n’a pas de nom et directement définies là où elles sont appelées. Cela permet une écriture plus simple pour les fonctions créées à la volée, pour un besoin spécifique.

Expressions lambda et interface fonctionnelle de Java

Definition de la JSR 335

A functional interface is an interface that has just one abstract method, and thus represents a single function contract. (In some cases, this "single" method may take the form of multiple abstract methods with override-equivalent signatures (8.4.2) inherited from superinterfaces; in this case, the inherited methods logically represent a single method.)

Pour implémenter les expressions lambda, le langage s’appuie sur les interfaces fonctionnelles également connues sous le nom de Single Abstract Method interfaces (SAM Interfaces). C’est toute interface qui possède une seule méthode d’instance abstraite. Il est aussi possible d’annoter une interface par @FunctionalInterface afin de demander au  compilateur de vérifier que l'interface possède  bien une seule méthode abstraite. C’est un peu le même principe qu’avec l’annotation @Override. Notre interface Operation est un bel exemple. Quelques exemples supplémentaires d’interfaces fonctionnelles du jdk : ActionListener, Runnable, Callable, Comparator
Une expression lambda peut être affectée dans une variable d’une interface fonctionnelle à condition que les signatures de la méthode abstraite (de l’interface fonctionnelle) correspondent à celle de l’expression lambda. Vous pouvez imaginer l’expression lambda comme une implémentation de la méthode abstraite de l’interface.
Une expression Lambda se compose de paramètres, d’un symbole flèche et d’un corps (exécution). La syntaxe utilisée est la suivante :

S’il y a une seule instruction, les accolades peuvent être omises ou même les types des paramètres qui seront déterminés par l’inférence de type du compilateur :

Que peut-on remarquer : La création d’une lambda expression ne nécessite pas l’instanciation d’un objet (utilisation du mot clé new). En effet, nous n'avons pas besoin de demander à la JVM de créer un objet qui sera ensuite nettoyé par le garbage ce qui permet de faire un gain de performance. En utilisant les expressions lambda, seule l’interface fonctionnelle (Operation) est nécessaire :



Vous pouvez même remarquer la définition à la volée d’une nouvelle opération (la division)

Collections et lambda expression :

Une collection de données est un conteneur d’éléments de même type (ou d’un type dérivé) qui possède un protocole particulier pour l’ajout, le retrait et la recherche d’éléments. Depuis le JDK 5.0, les collections sont manipulées par le biais de classes génériques implémentant l’interface Collection<E>, E représentant le type des éléments de la collection. Cette interface fournit tout ce qui est nécessaire au type de rangement des données (listes, tables associatives ou map en anglais, piles, files, ordonnées, etc…) et à leur manipulation simple (ajout, suppression, parcours, …).  Cependant le traitement plus complexe (filtrage, transformation, …), nécessite une API externes notamment Google Guava, Commons Collection, etc.

L’objectif ici est de pouvoir appliquer une lambda expression aux éléments d’une collection par exemple pour filtrer les éléments en ne sélectionnant que ceux qui remplissent une condition particulière (cela revient à appliquer une lambda expression à chaque élément ((E e) -> true/false, si true, l’élément est sélectionné) ou transformer un élément E en un autre élément X ((E e) -> X). Ce type de traitement renvoie une autre collection (donc duplication de collection en mémoire) contenant les éléments sélectionnés ou les nouveaux éléments transformés. Pour éviter ce type de duplication, Oracle a introduit un nouveau concept : Stream. L'utilisation d'un Stream permet d'éviter la duplication de collections en mémoire lorsque le traitement que l'on veut effectuer nécessite l'utilisation de collections intermédiaires (la nouvelle collection contenant les éléments filtrés ou transformés).

Un Stream est composé d’une source (List, tableau, etc.) et d’un ensemble d’opérations qu’on peut appliquer sur les éléments de la source.



On a deux types d’opérations : opération intermédiaire qui transforme le Stream  en un autre (après avoir appliqué une expression lambda sur les éléments de la source) et opération terminale qui produit un résultat. Un exemple pour illustrer nos propos : à partir d’un ensemble de personnes (une Collection), nous voulons le nom de personnes de plus de 18 ans. Comment faire ?

  1.        Il faut tout d’abord créer un Stream à partir d’une collection de personnes (la source du Stream est une collection de personnes)
  2.        Ensuite appliquer une opération intermédiaire qui filtre en appliquant une expression lambda : (Personne p) -> p.getAge() > 18. Il faudra imaginer ici une méthode (nommons Stream.filter (lambdaExpression)) fournie par la classe Stream, prenant en paramètre une expression lambda dont le type de retour est boolean (le filtre appliqué à une personne retourne true si la personne a plus de 18 ans sinon false) et qui (l’opération filter de la classe Stream) retourne un autre Stream dont la source ne contient que les personnes de plus 18 ans. 
  3.        Il faudra ensuite appliquer sur le Stream résultant, une  opération (méthode) de transformation. Il faudra aussi imaginer ici une méthode (nommons Stream.map ((Personne p) -> p.getName()) fournie par la classe Stream, prenant en paramètre une expression lambda dont le type de retour est String (l’expression lambda appliquée à une personne retourne le nom de la personne) et qui retourne un autre Stream dont la source ne contient que les noms de personnes.  
  4.        Une opération terminale pour finir qui permet de collecter les éléments de la source du Stream dans une liste. Ce type d’opération est aussi fourni par la classe Stream (Stream.collect). Ici pas besoin d’une expression lambda, il suffit juste de passer le paramètre Stream.collect(Collectors.toList()) fourni par la classe Collectors pour transformer les éléments de la source du Stream en une liste de nom de personnes (classe List<String>)
Nous pouvons remarquer que la stratégie d’itération est encapsulée dans la classe Stream. En effet un Stream dispose de sa propre stratégie de parcours. Il n'est donc PAS nécessaire d’indiquer explicitement COMMENT faire ce parcours en utilisant par exemple une boucle comme pour les collections (for, while, etc.).


Un autre exemple sur une liste de nombres. Nous voulons le carré de tous les nombres impairs de la liste :

Comme vous le constatez, la classe collection dispose d’une nouvelle méthode : stream. Cette évolution survenue en Java 8 aurait engendré la problématique de modification de toutes les classes qui implémentent la classe Collection :



Pour pallier à ce problème les méthodes par défaut sont introduites. L’objectif est de fournir une solution au manque d’évolution des interfaces. Les méthodes par défaut permettent de déclarer une méthode dans une interface et proposer une implémentation par défaut qui sera exécutée si elle n'est pas surchargée.


Avec Java 8 vous pouvez désormais créer de méthodes comportant du code (sans oublier d’ajouter le mot clé default) ainsi que la déclaration de méthodes statiques. Une méthode par défaut n'exige pas d'être implémentée puisqu'elle l'est déjà mais il reste tout à fait possible de le faire.


 A suivre ...

Aucun commentaire:

Enregistrer un commentaire