Lambda expressions

Chapitre 3. Expressions lambda

Ce chapitre couvre

  • Lambdas en quelques mots
  • Où et comment utiliser lambdas
  • Le patter « around exécution »
  • Interfaces fonctionnelles, inférence de type
  • Références de méthodes
  • Composition de lambdas

Dans le chapitre précédent, vous avez vu que le paramétrage du comportement est utile pour faire face aux changements de besoin fréquents dans votre code. Il vous permet de définir un bloc de code qui représente un comportement, puis de le transmettre. Vous pouvez décider d’exécuter ce bloc de code lorsqu’un certain événement se produit (par exemple un clic sur un bouton) ou à certains niveaux dans un algorithme (par exemple, un prédicat tel que « des pommes pesant plus de 150 g » dans un algorithme de filtrage. En général, en utilisant ce concept, vous pouvez écrire du code plus flexible et réutilisable.

Mais vous avez vu que l’utilisation de classes anonymes pour représenter différents comportements est insatisfaisante : elle est verbeuse, ce qui n’incite pas les programmeurs à utiliser la paramétrisation comportementale dans la pratique. Dans ce chapitre, nous vous présentons une nouvelle fonctionnalité de Java 8 qui aborde ce problème : les expressions lambda, qui vous permettent de représenter un comportement ou un code d’accès de manière concise. Pour l’instant, vous pouvez penser aux expressions lambda en tant que fonctions anonymes, essentiellement des méthodes sans noms déclarés, mais qui peuvent également être transmises en tant qu’arguments à une méthode de la même façon qu’avec une classe anonyme.

Je montrerai comment les construire, où les utiliser et comment rendre votre code plus concis en les utilisant. Je parlerai également de quelques nouveaux concepts tels que l’inférence de type et de nouvelles interfaces importantes disponibles dans l’API Java 8. Enfin, j’introduirai les références de méthodes, une nouvelle fonctionnalité utile qui va de pair avec les expressions lambda.

Ce chapitre est organisé de manière à vous apprendre, étape par étape, comment écrire un code plus concis et plus flexible. À la fin de ce chapitre, je rassemblerai tous les concepts enseignés dans un exemple concret : je reprendrai l’exemple de tri montré au chapitre 2 et l’améliorerai progressivement en utilisant des expressions lambda et des références de méthodes pour le rendre plus concis et lisible. Ce chapitre est important en soi et aussi parce que vous utiliserez beaucoup les lambdas tout au long du tutoriel.

3.1. Lambdas en quelques mots

Une expression lambda peut être comprise comme une représentation concise d’une fonction anonyme qui peut être transmise : elle n’a pas de nom, mais elle a une liste de paramètres, un corps, un type de retour et peut-être aussi une liste d’exceptions qui peut être jeté. C’est une grande définition que je vous ai donnée là ; Décomposons-la :

  •  Anonyme : Nous disons anonyme parce qu’il n’a pas de nom explicite comme une méthode aurait normalement. Moins de choses à écrire.
  • Fonction : Je dis fonction parce qu’un lambda n’est pas associé à une classe particulière comme l’est une méthode. Mais comme une méthode, un lambda a une liste de paramètres, un corps, un type de retour, et une liste possible d’exceptions qui peuvent être lancées.
  • Transmissible : une expression lambda peut être passée en argument à une méthode ou stockée dans une variable.
  • Concise – Vous n’avez pas besoin d’écrire beaucoup de code boilerplate comme vous le faites pour les classes anonymes.

Si vous vous demandez d’où vient le terme lambda, il provient d’un système développé dans le milieu universitaire appelé lambda calculus, qui est utilisé pour décrire les calculs.

Pourquoi devriez-vous vous soucier des expressions lambda ? Vous avez vu dans le chapitre précédent que le code de passage est actuellement fastidieux et verbeux en Java. Eh bien, bonnes nouvelles ! Lambdas corrige ce problème : ils vous permettent de passer du code de manière concise. Lambda ne vous permet techniquement pas de faire ce que vous ne pouviez pas faire avant Java 8. Mais vous n’avez plus besoin d’écrire du code fastidieux en utilisant des classes anonymes pour bénéficier de la paramétrisation du comportement ! Les expressions Lambda vous encourageront à adopter le style de programmation fonctionnel que nous avons décrit dans le chapitre précédent. Le résultat est que votre code sera plus clair et plus flexible. Par exemple, en utilisant une expression lambda, vous pouvez créer un objet Comparator personnalisé de manière plus concise.

Avant :

Après utilisation des lambdas expressions :

Vous devez admettre que le code semble plus clair ! Ne vous inquiétez pas si toutes les parties de l’expression lambda n’ont pas encore de sens pour vous ; Je les aborderai plus en détail bientôt. Pour l’instant, notez que vous ne transmettez littéralement que le code qui est vraiment nécessaire pour comparer deux pommes en utilisant leur poids. On dirait qu’on passe juste le corps de la méthode à comparer. Vous apprendrez bientôt que vous pouvez simplifier votre code encore plus. J’expliquerai dans la section suivante où et comment utiliser les expressions lambda.

Le lambda que je viens de vous montrer comporte trois parties, comme le montre la figure 3.1:

 

  • Une liste de paramètres – Dans ce cas, il reflète les paramètres de la méthode de comparaison d’un comparateur – deux pommes.
  • Une flèche – La flèche -> sépare la liste des paramètres du corps du lambda.
  • Le corps du lambda– Comparer deux pommes en utilisant leurs poids. L’expression est considérée comme la valeur de retour du lambda.

Pour aller plus loin, la liste suivante montre cinq exemples d’expressions lambda valides dans Java 8.

Cette syntaxe a été choisie par les concepteurs de langage Java car elle a été bien reçue dans d’autres langages tels que C # et Scala, qui ont une fonctionnalité similaire. La syntaxe de base d’une expression lambda est soit :

ou

Comme vous pouvez le voir, les expressions lambda suivent une syntaxe simple. Le quizz 3.1 devrait vous permettre de savoir si vous comprenez le pattern.



Quiz 3.1: Syntaxe Lambda

En fonction des règles de syntaxe que vous venez de voir, lesquelles des expressions suivantes ne sont pas des expressions lambda valides ?

  1. () -> {}
  2. () -> « Raoul »
  3. () -> {return « Mario »;}
  4. (Integer i) -> return »Alan » + i;
  5. (String s) -> {« Iron Man »;}

Réponse:

Seuls 4 et 5 sont des lambdas invalides.

  1. Cette lambda n’a pas de paramètres et renvoie vide. C’est similaire à une méthode avec un corps vide :

Public void run () {}.

  1. Cette lambda n’a pas de paramètres et renvoie une String en tant qu’expression.
  2. Cette lambda n’a pas de paramètres et renvoie une String (en utilisant une déclaration de retour explicite).
  3. return est une instruction de flux de contrôle. Pour rendre cette lambda valide, les accolades sont obligatoires comme suit : (Integer i) -> {return « Alan » + i ;}.

5. « Iron Man » est une expression, pas une déclaration. Pour que cette expression lambda soit valide, vous pouvez supprimer les accolades et le point-virgule comme suit : (String s) -> « Iron Man ». Ou si vous préférez, vous pouvez utiliser une déclaration de retour explicite comme suit : (String s) -> {return « Iron Man » ;}.

 



Le tableau 3.1 fournit une liste d’exemple lambdas.

3.2. Où et comment utiliser lambdas

Vous vous demandez peut-être où vous êtes autorisé à utiliser des expressions lambda. Dans l’exemple précédent, vous avez attribué un lambda à une variable de type Comparator<Apple>. Vous pouvez également utiliser un autre lambda avec la méthode de filtrage que vous avez implémentée dans le chapitre précédent :

Alors, où pouvez-vous utiliser les lambdas exactement ? Vous pouvez utiliser une expression lambda dans le contexte d’une interface fonctionnelle. Dans le code montré ici, vous pouvez passer un lambda comme second argument au filtre de la méthode car il attend un prédicat <T>, qui est une interface fonctionnelle. Ne vous inquiétez pas si cela semble abstrait ; j’expliquerai en détail ce que cela signifie et ce qu’est une interface fonctionnelle.

3.2.1. Interface fonctionnelle

Est-ce que vous vous rappelez de l’interface Predicate <T> que vous avez créée au chapitre 2 pour pouvoir paramétrer le comportement de la méthode de filtre ? C’est une interface fonctionnelle ! Pourquoi ? Parce que Predicate spécifie une seule méthode abstraite :

En résumé, une interface fonctionnelle est une interface qui spécifie exactement une méthode abstraite. Vous connaissez déjà plusieurs autres interfaces fonctionnelles dans l’API Java telles que Comparator et Runnable, que nous avons explorées au chapitre 2 :



Vous verrez au chapitre 9 que les interfaces peuvent maintenant avoir des méthodes par défaut (c’est-à-dire une méthode avec un corps qui fournit une implémentation par défaut pour une méthode au cas où elle n’est pas implémentée par une classe). Une interface est toujours une interface fonctionnelle même si elle comporte de nombreuses méthodes par défaut tant qu’elle ne spécifie qu’une seule méthode abstraite.



Pour vérifier votre compréhension, le Quiz 3.2 devrait vous permettre de savoir si vous saisissez le concept d’une interface fonctionnelle.



Quiz 3.2: Interface fonctionnelle

Parmi ces interfaces, lesquelles sont des interfaces fonctionnelles?

Répondre :

Seulement Adder est une interface fonctionnelle.

SmartAdder n’est pas une interface fonctionnelle car elle spécifie deux méthodes abstraites appelées add (l’une est héritée de Adder).

Nothing n’est pas une interface fonctionnelle car elle ne déclare aucune méthode abstraite.

 



Qu’est-il possible de faire avec les interfaces fonctionnelles ? Les expressions lambda permettent d’implémenter directement la méthode abstraite d’une interface fonctionnelle et de traiter l’expression entière comme une instance d’une interface fonctionnelle. Vous pouvez réaliser la même chose avec une classe interne anonyme, bien que ce soit plus maladroit : vous fournissez une implémentation et l’instanciez directement en ligne. Le code suivant est valide car Runnable est une interface fonctionnelle définissant une seule méthode abstraite, run() :

3.2.2. Descripteur de fonction

La signature de la méthode abstraite de l’interface fonctionnelle décrit essentiellement la signature de l’expression lambda. Cette méthode abstraite est un descripteur de fonction. Par exemple, l’interface Runnable peut être vue comme la signature d’une fonction qui n’accepte rien et ne renvoie rien (void) car elle n’a qu’une seule méthode abstraite appelée run, qui n’accepte rien et ne renvoie rien.

Nous utilisons une notation spéciale tout au long du chapitre pour décrire les signatures des lambdas et des interfaces fonctionnelles. La notation () -> void représente une fonction avec une liste vide de paramètres renvoyant void. C’est exactement ce que l’interface Runnable représente. Comme autre exemple, (Apple, Apple) -> int désigne une fonction prenant deux pommes comme paramètres et retournant un int. Nous fournirons plus d’informations sur les descripteurs de fonctions dans la section 3.4 et le tableau 3.2 plus loin dans le chapitre.

Vous vous demandez peut-être déjà comment les types dans les expressions lambda sont vérifiés. Je détaillerai comment le compilateur vérifie si un lambda est valide dans un contexte donné dans la section 3.5. Pour l’instant, il suffit de comprendre qu’une expression lambda peut être assignée à une variable ou transmise à une méthode en attente d’une interface fonctionnelle comme argument, à condition que l’expression lambda ait la même signature que la méthode abstraite de l’interface fonctionnelle. Par exemple, dans notre exemple précédent, vous pouvez passer un lambda directement à la méthode process comme suit :

Ce code lors de l’exécution imprimera « This is awesome ! » L’expression lambda () -> System.out.println (« This is awesome!! ») ne prend pas de paramètres et renvoie void. C’est exactement la signature de la méthode run définie dans l’interface Runnable.

Vous vous demandez peut-être « Pourquoi ne pouvons-nous passer un lambda que là où une interface fonctionnelle est attendue ?». Les concepteurs ont choisi cette façon parce que elle se comprend naturellement et n’augmente pas la complexité du langage. En outre, la plupart des programmeurs Java sont déjà familiarisés avec l’idée d’une interface avec une seule méthode abstraite (par exemple, avec la gestion des événements). Essayez le Quiz 3.3 pour tester vos connaissances sur l’utilisation des lambdas.

 



Quiz 3.3: Où pouvez-vous utiliser lambdas?

Lesquels des énoncés suivants sont des utilisations valides des expressions lambda?

Réponse :

Seuls 1 et 2 sont valides.

Le premier exemple est valide car lambda () -> {} a la signature () -> void, qui correspond à la signature de l’exécution de la méthode abstraite définie dans Runnable. Notez que l’exécution de ce code ne fera rien car le corps du lambda est vide !

Le deuxième exemple est également valide. En effet, le type de retour de la méthode fetch est Callable <String>. Callable <String> définit essentiellement une méthode avec la signature () -> String lorsque T est remplacé par String. Parce que lambda () -> « Tricky exemple  » a la signature () -> String, le lambda peut être utilisé dans ce contexte.

Le troisième exemple est invalide car l’expression lambda (Apple a) -> a.getWeight () a la signature (Apple) -> Integer, qui est différente de la signature du test de méthode défini dans Predicate <Apple>: (Apple) -> booléen.

 



Qu’en est-il de l’annotation @FunctionalInterface?

Si vous explorez la nouvelle API Java, vous remarquerez que les interfaces fonctionnelles sont annotées avec @FunctionalInterface (je montrerai une liste détaillée dans la section 3.4, où j’explorerai en profondeur les interfaces fonctionnelles). Cette annotation est utilisée pour indiquer que l’interface est destinée à être une interface fonctionnelle. Le compilateur renvoie une erreur significative si vous définissez une interface en utilisant l’annotation @FunctionalInterface et qu’elle ne soit pas une interface fonctionnelle. Par exemple, un message d’erreur pourrait être « Plusieurs méthodes abstraites non prioritaires trouvées dans l’interface Foo » pour indiquer que plus d’une méthode abstraite est disponible. Notez que l’annotation @FunctionalInterface n’est pas obligatoire, mais c’est une bonne pratique de l’utiliser lorsqu’une interface est conçue à cet effet. Vous pouvez penser à la notation @Override pour indiquer qu’une méthode est surchargée.



3.3. Mise en pratique des lambdas: le pattern around execution

Regardons un exemple de la façon dont les lambdas, ainsi que le paramétrage du comportement, peuvent être utilisés dans la pratique pour rendre votre code plus souple et plus concis. Un pattern récurrent dans le traitement des ressources (par exemple, traiter des fichiers ou des bases de données) consiste à ouvrir une ressource, à la traiter et à fermer la ressource. Les phases de configuration et de nettoyage sont toujours similaires et entourent le code important effectuant le traitement. C’est ce qu’on appelle le pattern d’exécution around, comme illustré à la figure 3.2. Par exemple, dans le code suivant, les lignes en surbrillance montrent le code de référence requis pour lire une ligne à partir d’un fichier (notez également que vous utilisez l’instruction try-with-resources de Java 7, ce qui simplifie déjà le code car vous n’avez pas besoin de fermer la ressource explicitement):

3.3.1. Étape 1: Rappelez-vous le paramétrage du comportement

Ce code actuel est limité. Vous ne pouvez lire que la première ligne du fichier. Que faire si vous souhaitez retourner les deux premières lignes à la place ou même le mot utilisé le plus fréquemment ? Idéalement, vous souhaitez réutiliser le code pour effectuer l’installation, le nettoyage et indiquer à la méthode processFile les différentes actions à effectuer sur le fichier. Cela vous semble familier ? Oui, vous devez paramétrer le comportement de processFile. Vous avez besoin d’un moyen de transmettre le comportement à processFile afin qu’il puisse exécuter différents comportements en utilisant un BufferedReader.

Le comportement transmis en paramètre est exactement ce que sont les lambdas. Alors à quoi devrait ressembler la nouvelle méthode processFile si vous vouliez lire deux lignes à la fois ? Vous avez essentiellement besoin d’un lambda qui prend un BufferedReader et renvoie une String. Par exemple, voici comment imprimer deux lignes d’un BufferedReader :

3.3.2. Étape 2: utiliser une interface fonctionnelle pour transmettre les comportements

Nous avons expliqué précédemment que les lambdas peuvent être utilisés uniquement dans le contexte d’une interface fonctionnelle. Vous devez en créer une qui correspond à la signature BufferedReader -> String et qui peut déclencher une IOException. Appelons cette interface BufferedReaderProcessor :

Vous pouvez maintenant utiliser cette interface comme argument pour votre nouvelle méthode processFile :

3.3.3. Étape 3: Exécuter un comportement!

Tous les lambdas de la forme BufferedReader -> String peuvent être passés en arguments, car ils correspondent à la signature de la méthode process définie dans l’interface Buffered-ReaderProcessor. Vous n’avez besoin maintenant que d’un moyen d’exécuter le code représenté par la lambda expression à l’intérieur du corps de processFile. Souvenez-vous que les expressions lambda vous permettent d’implémenter directement la méthode abstraite d’une interface fonctionnelle et traitent l’expression entière comme une instance d’une interface fonctionnelle. Vous pouvez donc appeler la méthode process sur l’objet BufferedReaderProcessor résultant dans le corps de la méthode processFile pour effectuer le traitement :

3.3.4. Étape 4: Passez une expression lambda

Vous pouvez maintenant réutiliser la méthode processFile et traiter les fichiers de différentes manières en passant différents lambdas.

Traitement d’une ligne :

Traitement de 2 lignes:

La figure 3.3 résume les 4 étapes qui nous ont permis de rendre la méthode processFile plus flexible.

Jusqu’à présent, nous avons montré comment vous pouvez utiliser des interfaces fonctionnelles pour passer des lambdas. Mais vous devriez définir vos propres interfaces. Dans la section suivante, nous explorons de nouvelles interfaces ajoutées à Java 8 que vous pouvez réutiliser pour passer plusieurs lambdas différents.

3.4. Utiliser des interfaces fonctionnelles

Comme vous l’avez appris dans la section 3.2.1, une interface fonctionnelle spécifie exactement une méthode abstraite. Les interfaces fonctionnelles sont utiles car la signature de la méthode abstraite peut décrire la signature d’une expression lambda. La signature de la méthode abstraite d’une interface fonctionnelle est appelée descripteur de fonction. Donc, pour utiliser différentes expressions lambda, vous avez besoin d’un ensemble d’interfaces fonctionnelles qui peuvent décrire des descripteurs de fonctions communs. Il existe plusieurs interfaces fonctionnelles déjà disponibles dans l’API Java, telles que Comparable, Runnable et Callable, que vous avez déjà vues dans la section 3.2.

Les concepteurs de bibliothèques Java pour Java 8 vous ont aidé en introduisant plusieurs nouvelles interfaces fonctionnelles dans le package java.util.function. Nous aborderons ensuite les interfaces Predicate, Consumer et Function prochainement. Et une liste plus complète est disponible dans le tableau 3.2 à la fin de cette section.

3.4.1. Prédicat

L’interface java.util.function.Predicate <T> définit une méthode abstraite nommée test qui accepte un objet de type générique T et renvoie un booléen. C’est exactement le même que vous avez créé plus tôt, mais il est directement disponible dans l’API java 8 out of the box ! Vous pouvez utiliser cette interface lorsque vous avez besoin de représenter une expression booléenne qui utilise un objet de type T. Par exemple, vous pouvez définir une expression lambda qui accepte les objets String, comme indiqué dans la figure suivante.

Si vous regarder la spécification Java doc de l’interface Predicate, vous pouvez remarquer des méthodes supplémentaires telles qu’and ou or. Ne vous en faite pas pour l’instant. Je reviendrai à ces dernière dans la section 3.8.

 

3.4.2. Consumer

L’interface java.util.function.Consumer <T> définit une méthode abstraite nommée accept qui prend un objet de type générique T et ne renvoie aucun résultat (void). Vous pouvez utiliser cette interface lorsque vous devez accéder à un objet de type T et y effectuer certaines opérations. Par exemple, vous pouvez l’utiliser pour créer une méthode forEach, qui prend une liste d’entiers et applique une opération sur chaque élément de cette liste. Dans la liste suivante, vous utilisez cette méthode forEach associée à une expression lambda pour imprimer tous les éléments de la figure.

3.4.3. Function

L’interface java.util.function.Function <T, R> définit une méthode abstraite nommée apply qui prend en entrée un objet de type générique T et renvoie un objet de type générique R. Vous pouvez utiliser cette interface lorsque vous devez définir un lambda qui mappe des informations d’un objet d’entrée vers une sortie (par exemple, extraire le poids d’une pomme ou mapper une String sur sa longueur). Dans la liste qui suit, nous montrons comment vous pouvez l’utiliser pour créer une méthode map pour transformer une liste de String en une liste d’entiers contenant la longueur de chaque String.

Nous avons décrit trois interfaces fonctionnelles génériques : Predicate <T>, Consumer <T> et Function <T, R>. Il existe également des interfaces fonctionnelles spécialisées avec certains types.

Pour vous rafraîchir un peu : chaque type Java est un type de référence (par exemple, Byte, Integer, Object, List) ou un type primitif (par exemple, int, double, octet, char). Mais les paramètres génériques (par exemple, le T dans Consumer <T>) peuvent être liés uniquement aux types de référence. Cela est dû à la façon dont les génériques sont implémentés en interne. En conséquence, il existe en Java un mécanisme pour convertir un type primitif en un type de référence correspondant. Ce mécanisme est appelé boxing. L’approche inverse (c’est-à-dire la conversion d’un type de référence en un type primitif correspondant) s’appelle unboxing. Java dispose également d’un mécanisme d’autoboxing pour faciliter la tâche des programmeurs : les opérations de boxing et unboxing sont effectuées automatiquement. Par exemple, c’est pourquoi le code suivant est valide (un int est convertit à un Integer) :

Mais cela a un coût de performance. Les valeurs en sorties ici (Integer) sont essentiellement des wrappers autour de types primitifs et sont stockées dans la Heap. Par conséquent, ces nouvelles valeurs utilisent plus de mémoire et nécessitent des recherches de mémoire supplémentaires pour extraire la valeur primitive de départ.

Java 8 apporte une version spécialisée des interfaces fonctionnelles que nous avons décrites précédemment afin d’éviter les opérations d’autoboxing lorsque les entrées ou sorties sont primitives. Par exemple, dans le code suivant, l’utilisation d’un IntPredicate évite une opération de boxing de la valeur 1000, alors que l’utilisation d’un prédicat <Integer> conditionnerait l’argument 1000 à un objet Integer :

En général, les noms des interfaces fonctionnelles qui ont une spécialisation pour un type de paramètre en entrée sont précédés du type de primitive approprié, par exemple DoublePredicate, IntConsumer, LongBinaryOperator, IntFunction, etc. L’interface Function comporte également des variantes pour le paramètre de type de sortie : ToIntFunction <T>, IntTo-DoubleFunction, etc.

Le tableau 3.2 donne un résumé des interfaces fonctionnelles les plus couramment utilisées dans l’API Java et leurs descripteurs de fonctions. Gardez à l’esprit qu’ils ne sont qu’un kit de démarrage. Vous pouvez toujours faire les vôtre si nécessaire ! Rappelez-vous, la notation (T, U) -> R montre comment penser à un descripteur de fonction. Le côté gauche de l’expression est une liste représentant les types des arguments. Dans ce cas, il représente une fonction avec deux arguments de type respectivement générique T et U et qui a un type de retour de R.

Vous avez maintenant vu beaucoup d’interfaces fonctionnelles qui peuvent être utilisées pour décrire la signature de diverses expressions lambda. Pour vérifier votre compréhension jusqu’à présent, essayez le quiz 3.4.



Quiz 3.4: Interfaces fonctionnelles

Quelles interfaces fonctionnelles utiliseriez-vous pour les descripteurs de fonctions suivants (c’est-à-dire les signatures d’une expression lambda) ? Vous trouverez la plupart des réponses dans le tableau 3.2. Dans la suite, créez des expressions lambda valides que vous pouvez utiliser avec ces interfaces fonctionnelles.

  1. T -> R
  2. (int, int) -> int
  3. T -> vide
  4. () -> T
  5. (T, U) -> R

Réponses :

  1. La fonction <T, R> est un bon candidat. Il est généralement utilisé pour convertir un objet de type T en un objet de type R (par exemple, Fonction <Apple, Integer> pour extraire le poids d’une pomme).
  2. IntBinaryOperator a une seule méthode abstraite appelée applyAsInt représentant un descripteur de fonction (int, int) -> int.
  3. Consumer <T> a une seule méthode abstraite appelée accept, représentant un descripteur de fonction T -> void.
  4. Le Supplier<T> a une seule méthode abstraite appelée get, représentant un descripteur de fonction () -> T. Alternativement, Callable <T> a également une seule méthode abstraite appelée call représentant un descripteur de fonction () -> T.
  5. BiFunction <T, U, R> a une seule méthode abstraite appelée apply représentant un descripteur de fonction (T, U) -> R.

 



Pour résumer la discussion sur les interfaces fonctionnelles et les lambdas, le tableau 3.3 fournit un résumé des cas d’utilisation, des exemples de lambdas et des interfaces fonctionnelles pouvant être utilisées.



Qu’en est-il des exceptions, des lambdas et des interfaces fonctionnelles?

Notez qu’aucune des interfaces fonctionnelles ne permet de lancer une checked exception. Vous avez deux options si vous avez besoin d’une expression lambda pour lancer une exception : définissez votre propre interface fonctionnelle qui déclare l’exception vérifiée, ou enveloppez le lambda avec un bloc try/catch.

Par exemple, dans la section 3.3, nous avons introduit une nouvelle interface fonctionnelle Buffered-ReaderProcessor qui a explicitement déclaré une exception :

 

Mais vous utilisez peut-être une API qui attend une interface fonctionnelle telle que Function<T, R> et il n’y a donc pas de possibilité de pour créer la vôtre (vous verrez dans le prochain chapitre que l’API Streams fait un usage intensif des interfaces fonctionnelles de table 3.2). Dans ce cas, vous pouvez utilisez explicitement un block try-catch pour la checked exception :

Vous avez maintenant vu comment créer lambdas et où et comment les utiliser. Maintenant nous aborderons des concepts plus avancés : comment les types lambdas sont-ils vérifiés par le compilateur et quelles sont les règles que vous devriez connaître, comme les lambdas référençant les variables locales dans leur corps et les lambdas compatibles avec les vides. Il n’est pas nécessaire de bien comprendre la section suivante tout de suite. Vous voudrez peut-être y revenir plus tard et passer à la section 3.6 sur les références de méthodes.

3.5. Vérification de type, inférence de type et restrictions

Lorsque nous avons mentionné pour la première fois des expressions lambda, nous avons dit qu’elles vous permettent de générer une instance d’une interface fonctionnelle. Néanmoins, une expression lambda elle-même ne contient pas les informations sur l’interface fonctionnelle qu’elle implémente. Afin d’avoir une compréhension plus formelle des expressions lambda, vous devriez savoir quel est le type réel d’une expression lambda.

3.5.1. Vérification de type

Le type d’une expression lambda est déduit du contexte dans lequel elle est utilisée. Le type attendu de l’expression lambda dans le contexte (par exemple, un paramètre de méthode auquel il est passé ou une variable locale à laquelle il est affecté) est appelé le type cible. Regardons un exemple pour voir ce qui se passe derrière la scène lorsque vous utilisez une expression lambda. La figure 3.4 résume le processus de vérification de type pour le code suivant :

Le processus de vérification de type est déconstruit comme suit :

  • D’abord, vous recherchez la déclaration de la méthode filter.
    • Deuxièmement, il attend comme second paramètre formel un objet de type Predicate- <Apple> (le type cible).
    • Troisièmement, Predicate <Apple> est une interface fonctionnelle définissant une seule méthode abstraite appelée test.
    • Quatrièmement, le test de méthode décrit un descripteur de fonction qui accepte un Apple et renvoie un booléen.
    • Enfin, tout argument réel de la méthode filter doit correspondre à cette exigence.

Le code est valide car l’expression lambda que nous transmettons prend aussi un paramètre Apple et renvoie un booléen. Notez que si l’expression lambda lançait une exception, la clause throws déclarée de la méthode abstraite devrait également correspondre.

3.5.2. Même lambda, différentes interfaces fonctionnelles

En raison de l’idée du typage cible, la même expression lambda peut être associée à différentes interfaces fonctionnelles si elles ont une signature de méthode abstraite compatible. Par exemple, les deux interfaces Callable et PrivilegedAction décrites précédemment présentent des fonctions qui n’acceptent rien et retournent un type générique T. Les deux affectations suivantes sont donc valides :

Dans ce cas, la première affectation a le type Callable <Integer> et la seconde affectation a le type PrivilegedAction <Integer>.

Dans le tableau 3.3, nous avons montré un exemple similaire ; la même expression lambda peut être utilisé avec plusieurs interfaces fonctionnelles différentes :



Opérateur diamant

Ceux d’entre vous qui connaissent l’évolution de Java se rappelleront que Java 7 avait déjà introduit l’idée de types déduits du contexte avec une inférence générique utilisant l’opérateur de diamants (<>). Une expression d’instance de classe donnée peut apparaître dans deux ou plusieurs contextes différents et l’argument de type approprié sera inféré comme illustré ici :



Règle spéciale de compatibilité avec void

Si une expression lambda a une instruction comme corps, il est compatible avec un descripteur de fonction qui renvoie void (à condition que la liste de paramètres soit aussi compatible). Par exemple, les deux lignes suivantes sont légales même si la méthode add d’une liste renvoie un booléen et non vide comme prévu dans le contexte du consommateur (T -> void) :



A présent, vous devriez avoir une bonne compréhension de quand et où vous êtes autorisé à utiliser des expressions lambda. Ils peuvent obtenir leur type cible à partir du contexte d’affectation, d’invocation de méthode (paramètres et retour) et de conversion. Pour vérifier vos connaissances, essayez le Quiz 3.5.



Quiz 3.5: Vérification de type: pourquoi le code suivant ne sera-t-il pas compilé?

Comment pourriez-vous résoudre le problème?

Réponse :

Le contexte de l’expression lambda est Object (le type cible). Mais Object n’est pas une interface fonctionnelle. Pour résoudre ce problème, vous pouvez changer le type de cible en Runnable, qui représente un descripteur de fonction () -> void:

 



Vous avez vu comment le type ciblé peut être utilisé pour vérifier si une expression lambda peut être utilisée dans un contexte particulier. Il peut également être utilisé pour faire quelque chose de légèrement différent : déduire les types des paramètres d’une lambda.

3.5.3. Inférence de type

Vous pouvez simplifier votre code un peu plus. Le compilateur Java déduit quelle interface fonctionnelle peut être associée à une expression lambda à partir du contexte environnant (le type cible), ce qui signifie qu’il peut également déduire une signature appropriée pour le lambda car le descripteur de fonction est disponible via le type cible. L’avantage est que le compilateur a accès aux types de paramètres d’une expression lambda, et ils peuvent être omis dans la syntaxe lambda. En d’autres termes, le compilateur Java déduit les types des paramètres d’une lambda comme montré ici :

Les avantages sur la lisibilité du code sont plus visibles avec les expressions lambda qui ont plusieurs paramètres. Par exemple, voici comment créer un objet Comparator :

Notez que parfois il est plus lisible d’inclure les types explicitement et parfois plus lisibles de les exclure. Il n’y a pas de règle pour choisir quelle est la meilleure voie ; les développeurs doivent faire leurs propres choix sur ce qui rend leur code plus lisible.

3.5.4. Utiliser des variables locales

Toutes les expressions lambda que nous vous avons montrées jusqu’ici n’ont utilisé dans leur corps que les arguments qu’ils avaient en paramètre. Mais les expressions lambda sont également autorisées à utiliser des variables libres (variables qui ne sont pas des paramètres et qui sont définies dans une portée externe) comme les classes anonymes. Ils sont appelés des « lambdas capturantes ». Par exemple, l’expression lambda suivante capture la variable portNumber :

Néanmoins, il y a une petite précaution. En effet, il y a certaines restrictions sur ce que vous pouvez faire avec ces variables. Les expressions lambdas sont autorisées à capturer (c’est-à-dire à référencer dans leur corps) des variables d’instance et des variables statiques sans restriction. Mais les variables locales doivent être explicitement déclarées finales ou effectivement finales. En d’autres termes, les expressions lambda peuvent capturer des variables locales qui leur sont attribuées une seule fois. (Remarque : la capture d’une variable d’instance peut être considérée comme la capture de la variable locale finale.) Par exemple, le code suivant ne compile pas car la variable portNumber est affectée deux fois :

Vous vous demandez peut-être pourquoi les variables locales ont ces restrictions. Tout d’abord, il y a une différence clé dans la façon dont les variables locales et d’instance sont implémentées dans les coulisses de la JVM. Les variables d’instance sont stockées sur le tas(Heap), tandis que les variables locales vivent sur la pile(Stack). Si une expression lambda pouvait accéder directement à une variable locale et que l’expression lambda était utilisée dans un thread, le thread l’utilisant pourrait essayer d’accéder à la variable après que le thread qui l’a allouée la libère. Par conséquent, Java implémente l’accès à une variable locale libre en tant qu’accès à une copie plutôt qu’à l’accès à la variable d’origine. Cela ne fait aucune différence si la variable locale est assignée une seule fois – d’où la restriction.

Deuxièmement, cette restriction décourage également les modèles de programmation impératifs typiques (qui, comme nous l’expliquerons dans les chapitres suivants, empêchent une parallélisation intuitive) qui modifie des variables externes(partagées).



Closure

Vous avez peut-être entendu parler du terme Closure et vous demandez peut-être si les lambdas répondent à la définition d’une closure (à ne pas confondre avec le langage de programmation Clojure). Pour le dire scientifiquement, une closure est une instance d’une fonction qui peut référencer des variables non-locales de cette fonction sans restriction. Par exemple, une closure pourrait être passée comme argument à une autre fonction. Il pourrait également accéder et modifier des variables définies en dehors de son champ d’application. Maintenant, les lambdas Java 8 et les classes anonymes font quelque chose de similaire aux closures : ils peuvent être passés en argument aux méthodes et peuvent accéder à des variables en dehors de leur portée. Mais ils ont une restriction : ils ne peuvent pas modifier le contenu des variables locales d’une méthode dans laquelle le lambda est défini. Ces variables doivent être implicitement final. Cela aide de savoir que les lambdas appliquent des closures sur des valeurs plutôt que des variables. Comme on l’a expliqué précédemment, cette restriction existe parce que les variables locales vivent sur la pile et sont implicitement confinées au thread dans lequel elles se trouvent. Permettre la capture de variables locales modifiables aurait ouvert de nouvelles possibilités dangereuses et pas souhaitables. Ceci du fait que les threads ne partagent pas leur pile. Par contre les variables d’instance ne posent aucun problème étant donné qu’elles vivent sur le tas, qui est lui partagé entre les threads.



Nous décrivons maintenant une autre fonctionnalité que vous verrez dans le code Java 8 : les références de méthodes. Pensez-y comme des versions abrégées de certaines expressions lambdas.

3.6. Références de méthodes

Les références de méthode vous permettent de réutiliser les définitions de méthodes existantes et de les transmettre comme des lambdas. Dans certains cas, elles semblent plus lisibles et plus naturel que d’utiliser des expressions lambda. Voici notre exemple de tri écrit avec une référence de méthode et un peu d’aide de l’API Java 8 mise à jour (nous explorons cet exemple plus en détail dans la section 3.7) :

Avant :

Après (utilisation de la méthode de référence et de la méthode java.util.Comparator.comparing)

3.6.1. En quelques mots

Pourquoi devriez-vous vous soucier des références de méthodes ? Les références de méthodes peuvent être considérées comme des raccourcis pour les lambdas appelant uniquement une méthode spécifique. L’idée de base est que si une expression lambda appelle une seule méthode directement, il est préférable de se référer à la méthode par son nom plutôt que par une description de comment l’appeler. En effet, une référence de méthode vous permet de créer une expression lambda à partir d’une implémentation de méthode existante. Mais en vous référant explicitement à un nom de méthode, votre code peut gagner en lisibilité. Comment ça marche ? Lorsque vous avez besoin d’une référence de méthode, la référence cible est placée avant le délimiteur :: et le nom de la méthode est fourni après. Par exemple, Apple::getWeight est une référence de méthode à la méthode getWeight définie dans la classe Apple. Rappelez-vous qu’aucune parenthèse n’est nécessaire parce que vous n’appelez pas la méthode. La référence de la méthode est un raccourci pour l’expression lambda (Apple a) -> a.getWeight (). Le tableau 3.4 donne quelques exemples supplémentaires de références de méthodes possibles dans Java 8.

Vous pouvez considérer les références de méthodes comme des astuces syntaxiques pour les lambdas qui ne se réfèrent qu’à une seule méthode car vous écrivez moins pour exprimer la même chose.

Il existe trois principaux types de références de méthodes :

  • La référence de méthode à une méthode statique (par exemple, la méthode parseInt de Integer, écrite Integer :: parseInt)
  • La référence de méthode à une méthode d’instance d’un type arbitraire (par exemple, la longueur d’une chaîne, écrite String :: length)
  • La référence de méthode à une méthode d’instance d’un objet existant (par exemple, supposez que vous avez une variable locale expensiveTransaction qui contient un objet de type Transaction, qui prend en charge une méthode d’instance getValue, vous pouvez écrire expensiveTransaction :: getValue)

Les deuxième et troisième type de références de méthodes peuvent être un peu déroutante au début. L’idée avec le second type de références de méthodes telles que String :: length est que vous faites référence à une méthode pour un objet qui sera fourni comme l’un des paramètres de l’expression lambda. Par exemple, l’expression lambda (String s) -> s.toUpperCase () peut être réécrite sous la forme String :: toUpperCase. Mais le troisième type de références de méthodes fait référence à une situation où vous appelez une méthode dans une expression lambda sur un objet externe qui existe déjà. Par exemple, l’expression lambda () -> expensiveTransaction.getValue() peut être réécrite en tant que expensiveTransaction::getValue.

 

Notez qu’il existe également des formes spéciales de références de méthodes pour les constructeurs, les constructeurs de tableaux et les super-calls.

Appliquons maintenant les références de méthodes dans un exemple concret. Supposons que vous souhaitez trier une liste de String, en ignorant la casse. La méthode de tri sur une liste attend un comparateur en paramètre. Vous avez vu précédemment que Comparator décrit un descripteur de fonction avec la signature (T, T) -> int. Vous pouvez définir une expression lambda qui utilise la méthode compareToIgnoreCase dans la classe String comme suit (notez que compareToIgnoreCase est prédéfini dans la classe String) :

L’expression lambda a une signature compatible avec le descripteur de fonction de Comparator. En utilisant les recettes décrites précédemment, l’exemple peut également être écrit en utilisant une référence de méthode comme ceci:

Notez que le compilateur suit un processus de vérification de type similaire à celui des expressions lambda pour déterminer si une référence de méthode est valide avec une interface fonctionnelle donnée : la signature de la référence de méthode doit correspondre au type du contexte.

Pour vérifier votre compréhension des références de méthodes, essayez le Quiz 3.6.



Quiz 3.6: Références de méthodes

Quelles sont les références de méthodes équivalentes pour les expressions lambda suivantes ?

1.Function <String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);

2.BiPredicate <Liste <String>, String> contient = (liste, élément) -> list.contains(élément) ;

Réponses :

  1. Cette expression lambda renvoie son argument à la méthode statique parseInt de Integer. Cette méthode prend une String à parser et retourne un entier. En conséquence, l’expression lambda peut être réécrit en utilisant la recette de la figure 3.5 (expressions lambda appelant une méthode statique) comme suit :

Fonction <String, Integer> stringToInteger = Integer :: parseInt ;

  1. Cette lambda utilise son premier argument pour appeler la méthode contains dessus. Comme le premier argument est de type List, vous pouvez utiliser la recette de la figure 3.5 comme suit :

BiPredicate <List <String>, String> contient = List :: contains;

 



Jusqu’à présent, nous avons montré comment réutiliser les implémentations de méthodes existantes et créer des références de méthodes. Nous verrons qu’il est possible de faire quelque chose de similaire avec les constructeurs d’une classe.

3.6.2. Références de constructeurs

Vous pouvez créer une référence à un constructeur existant en utilisant son nom et le mot clé new comme suit : ClassName :: new. Il fonctionne de manière similaire à la référence à une méthode statique. Par exemple, supposez qu’il y ait un constructeur avec aucun argument. Cela correspond à la signature () -> Apple of Supplier ; vous pouvez faire ce qui ceci,

Si vous avez un constructeur avec la signature Apple (Integer weight), il correspond à la signature de l’interface fonctionnelle :

ce qui est équivalent à:

Dans le code suivant, chaque élément d’une List d’Integer est transmis au constructeur d’Apple en utilisant là méthode map similaire que nous avons définie précédemment, ce qui donne une liste de pommes avec des poids différents :

Si vous avez un constructeur à deux arguments, Apple (String color, Integer weight), il correspond à la signature de l’interface BiFunction, donc vous pouvez :

ce qui est équivalent à:

La possibilité de se référer à un constructeur sans l’instancier permet des applications intéressantes. Par exemple, vous pouvez utiliser une Map pour associer des constructeurs à une String. Vous pouvez ensuite créer une méthode giveMeFruit qui, étant donné une String et un Integer, peut créer différents types de fruits avec des poids différents :

Pour vérifier votre compréhension des références de méthode et constructeur, essayez le Quiz 3.7.



Quiz 3.7: Références du constructeur

Vous avez vu comment transformer les constructeurs à zéro, à un et à deux arguments en références constructeur. Que devez-vous faire pour utiliser une référence de constructeur pour un constructeur à trois arguments tel que Color (int, int, int)?

Répondre:

Vous avez vu que la syntaxe d’une référence de constructeur est ClassName :: new, donc dans ce cas, il s’agit de Color :: new. Mais vous avez besoin d’une interface fonctionnelle qui correspondra à la signature de cette référence de constructeur. Parce qu’il n’y en a pas un par défaut dans l’API java 8.

Vous pouvez utiliser le constructeur référence comme ceci:



Nous avons ingurgité beaucoup de nouvelles informations : lambdas, interfaces fonctionnelles et références de méthodes. Nous mettrons tout en pratique dans la section suivante.

3.7. Mise en pratique des lambdas et des références de méthodes!

Pour conclure ce chapitre et tout ce que nous avons vu sur les lambdas, nous continuons avec notre problème initial de tri d’une liste de pommes avec des stratégies de commande différentes et montrer comment vous pouvez progressivement transformer une solution naïve en une solution concise, en utilisant tous les concepts et caractéristiques expliquées jusqu’ici dans le tutoriel : paramétrage du comportement, classes anonymes, expressions lambda et références de méthodes. La solution finale est la suivante (notez que tout le code source est disponible sur la page web github) :

3.7.1. Étape 1: Passer des fonctions en paramètre

Heuresement, l’API Java 8 vous fournit déjà une méthode de tri disponible sur List afin de ne pas l’implémenter. Donc, la partie difficile est faite ! Mais comment pouvez-vous passer une stratégie de filtre à la méthode sort ? Eh bien, la méthode sort a la signature suivante :

Il attend un objet Comparator comme argument pour comparer deux pommes. C’est ainsi que vous pouvez passer différentes stratégies en Java : elles doivent être enveloppées dans un objet. Nous disons que le comportement de type est paramétré : son comportement sera différent en fonction des différentes stratégies d’ordonnancement passées.

Votre première solution ressemble à ceci :

3.7.2. Étape 2: utilisez une classe anonyme

Plutôt que d’implémenter Comparator dans le but de l’instancier une seule fois, vous avez vu que vous pouviez utiliser une classe anonyme pour améliorer votre solution :

3.7.3. Étape 3: utiliser des expressions lambda

Mais votre solution précédente est toujours verbeuse. Java 8 introduit les expressions lambda, qui fournissent une syntaxe légère pour atteindre le même objectif : passer du code. Vous avez vu qu’une expression lambda peut être utilisée lorsqu’une interface fonctionnelle est attendue. Pour rappel, une interface fonctionnelle est une interface définissant une seule méthode abstraite. La signature de la méthode abstraite (appelée descripteur de fonction) peut décrire la signature d’une expression lambda. Dans ce cas, le comparateur représente un descripteur de fonction (T, T) -> int. Parce que vous utilisez des pommes, cela représente plus spécifiquement (Apple, Apple) -> int. Votre nouvelle solution améliorée se présente donc comme ceci :

Ensuite nous avons expliqué que le compilateur Java pouvait déduire les types des paramètres d’une expression lambda en utilisant le contexte dans lequel le lambda apparaît. Vous pouvez donc réécrire votre solution comme ceci :

Pouvez-vous rendre votre code encore plus lisible ? Le Comparator a une méthode statique appelée comparing qui prend une fonction extrayant une clé comparable et produit un Comparator (nous expliquons pourquoi les interfaces peuvent avoir des méthodes statiques dans le chapitre 9). Il peut être utilisé comme suit (notez que vous passez maintenant une expression lambda avec un seul argument : l’expression spécifie comment extraire la clé à comparer avec une pomme) :

Vous pouvez maintenant réécrire votre solution sous une forme légèrement plus compacte:

3.7.4. Étape 4: utiliser des références de méthode

Nous avons expliqué que les références de méthodes sont des astuces syntaxiques pour les expressions lambda qui transmettent leurs arguments. Vous pouvez utiliser une référence de méthode pour rendre votre code légèrement moins détaillé (en supposant une importation statique de java.util.Comparator.comparing) :

Félicitations, ceci est la solution finale ! Pourquoi est-ce mieux que le code avant Java 8 ? Ce n’est pas seulement parce que c’est plus court ; le code se lit comme l’énoncé du problème « trier l’inventaire en comparant le poids des pommes ».

3.8. Méthodes utiles pour composer des expressions lambda

Plusieurs interfaces fonctionnelles de l’API Java 8 contiennent des méthodes pratiques. Plus précisément, de nombreuses interfaces fonctionnelles telles que Comparator, Function et Predicate, utilisées pour transmettre des expressions lambda, fournissent des méthodes permettant la composition. Qu’est-ce que ça veut dire ? En pratique, cela signifie que vous pouvez combiner plusieurs expressions lambda simples pour construire des expressions plus complexes. Par exemple, vous pouvez combiner deux prédicats dans un prédicat plus grand qui effectue une opération ou une opération entre les deux prédicats. De plus, vous pouvez également composer des fonctions telles que le résultat de l’une devient l’entrée d’une autre fonction. Vous pouvez vous demander comment est-il possible qu’il y ait des méthodes supplémentaires dans une interface fonctionnelle. (Après tout, cela va à l’encontre de la définition d’une interface fonctionnelle !) L’astuce est que les méthodes que nous allons introduire sont appelées méthodes par défaut (c’est-à-dire qu’elles ne sont pas des méthodes abstraites). Nous les expliquons en détail dans le chapitre 9. Pour l’instant, il suffit de me faire confiance et de lire le chapitre 9 plus tard quand vous voudrez en savoir plus sur les méthodes par défaut et sur ce que vous pouvez en faire.

3.8.1. Composer des Comparateurs

Vous avez vu que vous pouvez utiliser la méthode statique Comparator.comparing pour renvoyer un Comparator basé sur une fonction qui extrait une clé de comparaison comme suit :

Reversed order : Et si on voulait trier les pommes selon un poids décroissant ? Il n’est pas nécessaire de créer une instance différente d’un comparateur. L’interface inclut une méthode Reversed par défaut qui impose l’ordre inverse d’un comparateur donné. Vous pouvez donc simplement modifier l’exemple en réutilisant le comparateur initial :

Chaining comparator : Tout cela est bien, mais que faire si vous trouvez deux pommes qui ont le même poids ? Quelle pomme devrait avoir la priorité dans la liste triée ? Vous pouvez fournir un second comparator pour affiner la comparaison. Par exemple, après avoir comparé deux pommes en fonction de leur poids, vous pouvez les trier par pays d’origine. La méthode thenComparing vous permet de faire exactement cela. Il prend une fonction en tant que paramètre (tout comme la méthode comparing) et fournit un second comparateur si deux objets sont considérés égaux en utilisant le comparateur initial. Vous pouvez résoudre le problème avec élégance à nouveau :

3.8.2. Composer des prédicats

L’interface Predicate comprend trois méthodes qui vous permettent de réutiliser un prédicat existant pour en créer des plus complexes : negate, and, et or. Par exemple, vous pouvez utiliser la méthode negate pour renvoyer la négation d’un prédicat, par exemple une pomme qui n’est pas rouge :

Vous pouvez combiner deux lambdas pour dire qu’une pomme est à la fois rouge et lourde avec la méthode and :

Vous pouvez combiner le prédicat résultant plus loin pour exprimer les pommes rouges et lourdes (plus de 150 g) ou simplement les pommes vertes :

Pourquoi est-ce génial ? A partir d’expressions lambda plus simples, vous pouvez représenter des expressions lambda plus complexes qui continue à traduire rapidement l’énoncé du problème ! Notez que la priorité des méthodes and et or est gérée de gauche à droite en utilisant leurs positions dans la chaîne. Ainsi a.or (b).and(c) peut être vu comme (a || b) && c.

3.8.3. Fonctions de composition

Enfin, vous pouvez également composer des expressions lambda représentées par l’interface Function. L’interface Function est fournie avec deux méthodes par défaut, andThen et compose, qui renvoient chacune une instance de Function.

La méthode andThen renvoie une fonction qui applique d’abord une fonction donnée à une entrée, puis applique une autre fonction au résultat de cette application. Par exemple, étant donné une fonction f qui incrémente un nombre (x -> x + 1) et une autre fonction g qui multiplie un nombre par 2, vous pouvez les combiner pour créer une fonction h qui incrémente d’abord un nombre, puis multiplie le résultat par 2 :

Vous pouvez également utiliser la méthode compose de la même manière pour appliquer d’abord la fonction donnée comme argument à composer puis appliquer la fonction au résultat. Par exemple, dans l’exemple précédent utilisant compose, cela signifierait f (g (x)) au lieu de g (f (x)) en utilisant andThen :

La figure 3.6 illustre la différence entre and Then et compose.

Tout cela semble un peu trop abstrait. Comment pouvez-vous les utiliser dans la pratique ? Supposons que vous ayez différentes méthodes utilitaires qui effectuent la transformation de texte sur une lettre représentée par une String :

Vous pouvez maintenant créer divers pipelines de transformation en composant les méthodes utilitaires, par exemple en créant un pipeline qui ajoute d’abord un en-tête, puis vérifie l’orthographe et ajoute un pied de page, comme illustré à la figure 3.7:

Un deuxième pipeline pourrait être d’ajouter un en-tête et un pied de page sans vérifier l’orthographe :

3.9. Idées similaires des mathématiques

Si vous vous sentez à l’aise avec les mathématiques à l’école, cette section donne un autre point de vue sur l’idée des expressions lambda et des fonctions de passage. Sentez-vous libre de simplement la sauter ; rien d’autre dans le tutoriel n’en dépend, mais vous pouvez apprécier une autre perspective.

3.9.1. L’intégration

Supposons que vous ayez une fonction (mathématique, pas Java) f, peut-être définie par

f (x) = x + 10

Ensuite, une question souvent posée (à l’école, en ingénierie) consiste à trouver l’aire sous la fonction lorsqu’elle est dessinée sur papier (en comptant l’axe des abscisses sur la ligne zéro). Par exemple, vous écrivez

pour l’aire montrée dans la figure 3.8

Dans cet exemple, la fonction f est une ligne droite, et vous pouvez donc facilement quantifier cette zone par la méthode du trapèze (essentiellement tracer des triangles) pour trouver la solution :

1/2 × ((3 + 10) + (7 + 10)) × (7-3) = 60

Maintenant, comment pourriez-vous l’exprimer en Java ? Votre premier problème est de représenter la notation étrange comme le symbole d’intégration ou dy / dx en langage de programmation.

En effet, en partant des premiers principes, il faut une méthode, peut-être appelée integrate, qui prend trois arguments : l’un est f et les autres sont les limites (3.0 et 7.0 ici). Ainsi, vous voulez écrire en Java quelque chose qui ressemble à ceci, où la fonction f est juste passée en paramètre :

Notez que vous ne pouvez pas écrire quelque chose d’aussi simple que ceci:

Pour deux raisons. Tout d’abord, la portée de x n’est pas claire, et deuxièmement, cela passerait une valeur de x + 10 à intégrer au lieu de passer la fonction f.

3.9.2. Connexion à Java 8 lambdas

Maintenant, comme nous l’avons mentionné précédemment, Java 8 utilise exactement la notation (double x) -> x + 10 (une expression lambda) ; donc vous pouvez écrire :

ou

ou alors en utilisant une référence de méthode:

Si C est une classe contenant f comme une méthode statique. L’idée est que vous passez le code pour f à la méthode integrate.

Vous pouvez maintenant vous demander comment vous allez écrire la méthode integrate. Continuez à supposer que f est une fonction linéaire (ligne droite). Vous aimeriez probablement écrire sous une forme similaire aux mathématiques :

Mais comme les expressions lambda ne peuvent être utilisées que dans un contexte en attente d’une interface fonctionnelle (dans ce cas, Function), vous devez l’écrire comme suit :

En fait, il est un peu dommage de devoir écrire f.apply (a) au lieu de simplement f (a) comme en mathématiques, mais Java ne peut tout simplement pas s’empêcher de considérer que tout est un objet – au lieu de l’idée d’une fonction vraiment indépendante.

3.10. Résumé

Voici les principaux concepts que vous devriez retirer de ce chapitre :

  • Une expression lambda peut être comprise comme une sorte de fonction anonyme : elle n’a pas de nom, mais elle comporte une liste de paramètres, un corps, un type de retour et peut-être même une liste d’exceptions pouvant être lancées.
  • Les expressions lambda vous permettent de passer du code de manière concise.
  • Une interface fonctionnelle est une interface qui déclare exactement une méthode abstraite.
  • Les expressions lambda ne peuvent être utilisées que lorsqu’une interface fonctionnelle est attendue.
  • Les expressions lambda permettent de fournir directement la mise en œuvre de la méthode abstraite d’une interface fonctionnelle et de traiter l’expression entière comme une instance d’une interface fonctionnelle.
  • Java 8 est fourni avec une liste d’interfaces fonctionnelles communes dans le package java.util .function, qui comprend Predicate <T>, Function <T, R>, Supplier <T>, Consumer <T> et BinaryOperator <T> décrit dans le tableau 3.2.
  • Il existe des spécialisations de primitives d’interfaces fonctionnelles génériques communes telles que Predicate <T> et Function <T, R> qui peuvent être utilisées pour éviter les opérations de autoboxing : IntPredicate, IntToLongFunction, etc.
  • Le pattern d’exécution around (c’est-à-dire, exécuter une fonctionnalité au milieu du code qui est toujours requis dans une méthode, par exemple l’allocation des ressources et le nettoyage) peut être utilisé avec lambdas pour gagner en flexibilité et réutilisation.
  • Le type attendu pour une expression lambda est appelé le type cible.
  • Les références de méthode vous permettent de réutiliser une implémentation de méthode existante et de la transmettre directement.
  • Les interfaces fonctionnelles telles que Comparator, Predicate et Function ont plusieurs méthodes par défaut qui peuvent être utilisées pour combiner des expressions lambda.

 

Précédent