Chapitre 1 Bonjour, expression lambda !
Section 1
Le style de codage de Java est confronté à d'énormes changements.
Notre travail quotidien deviendra plus simple, plus pratique et plus expressif. Java, une nouvelle méthode de programmation, est apparue dans d'autres langages de programmation il y a plusieurs décennies. Une fois ces nouvelles fonctionnalités introduites dans Java, nous pouvons écrire du code plus concis, élégant, plus expressif et contenant moins d’erreurs. Nous pouvons mettre en œuvre diverses stratégies et modèles de conception avec moins de code.
Dans ce livre, nous explorerons la programmation de style fonctionnel à travers des exemples tirés de la programmation quotidienne. Avant d'utiliser cette nouvelle et élégante façon de concevoir et de coder, examinons d'abord ce qu'elle a de si bon.
tu as changé ta façon de penser
Style impératif - C'est l'approche proposée par le langage Java depuis sa création. En utilisant ce style, nous devons dire à Java quoi faire à chaque étape, puis le regarder l'exécuter étape par étape. C'est bien sûr bien, mais cela semble un peu rudimentaire. Le code semble un peu verbeux et nous souhaitons que le langage devienne un peu plus intelligent ; nous devrions simplement lui dire ce que nous voulons au lieu de lui dire comment le faire. Heureusement, Java peut enfin nous aider à réaliser ce souhait. Regardons quelques exemples pour comprendre les avantages et les différences de ce style.
manière normale
Commençons par deux exemples familiers. Il s'agit d'une méthode de commande permettant de vérifier si Chicago fait partie de la collection de villes spécifiée. N'oubliez pas que le code répertorié dans ce livre n'est qu'un fragment partiel.
Copiez le code comme suit :
booléen trouvé = faux ;
pour (String city : villes) {
if(city.equals("Chicago")) {
trouvé = vrai ;
casser;
}
}
System.out.println("Chicago trouvé ?:" + trouvé);
Cette version impérative paraît un peu verbeuse et rudimentaire ; elle est divisée en plusieurs parties d'exécution. Tout d'abord, initialisez une balise booléenne appelée found, puis parcourez chaque élément de la collection ; si la ville que nous recherchons est trouvée, définissez cette balise, puis sortez de la boucle et enfin imprimez les résultats de la recherche ;
une meilleure façon
Après avoir lu ce code, les programmeurs Java attentifs penseront rapidement à une manière plus concise et plus claire, comme ceci :
Copiez le code comme suit :
System.out.println("Chicago trouvé ?:" + villes.contains("Chicago"));
C’est aussi un style d’écriture impératif – la méthode contain le fait directement pour nous.
améliorations réelles
Écrire du code comme celui-ci présente plusieurs avantages :
1. Plus besoin de jouer avec cette variable mutable
2. Encapsuler l'itération dans la couche inférieure
3. Le code est plus simple
4. Le code est plus clair et plus ciblé
5. Faites moins de détours et intégrez plus étroitement le code et les besoins métier
6. Moins sujet aux erreurs
7. Facile à comprendre et à entretenir
Prenons un exemple plus compliqué.
Cet exemple est trop simple. La requête impérative pour savoir si un élément existe dans une collection peut être vue partout en Java. Supposons maintenant que nous souhaitions utiliser la programmation impérative pour effectuer des opérations plus avancées, telles que l'analyse de fichiers, l'interaction avec des bases de données, l'appel de services WEB, la programmation simultanée, etc. Nous pouvons désormais utiliser Java pour écrire du code plus concis, élégant et sans erreur, et pas seulement dans ce scénario simple.
à l'ancienne
Regardons un autre exemple. Nous définissons une fourchette de prix et calculons le prix total réduit de différentes manières.
Copiez le code comme suit :
prix de la liste finale <BigDecimal> = Arrays.asList (
nouveau BigDecimal("10"), nouveau BigDecimal("30"), nouveau BigDecimal("17"),
nouveau BigDecimal("20"), nouveau BigDecimal("15"), nouveau BigDecimal("18"),
nouveau BigDecimal("45"), nouveau BigDecimal("12"));
En supposant qu'il y ait une remise de 10 % si elle dépasse 20 yuans, mettons-la d'abord en œuvre de la manière habituelle.
Copiez le code comme suit :
BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO ;
pour (prix BigDecimal : prix) {
si (prix.compareTo (BigDecimal.valueOf (20)) > 0)
totalOfDiscountedPrices =
totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9)));
}
System.out.println("Total des prix réduits : " + totalOfDiscountedPrices);
Ce code doit être très familier : utilisez d'abord une variable pour stocker le prix total, puis parcourez tous les prix, trouvez ceux qui sont supérieurs à 20 yuans, calculez leurs prix réduits et ajoutez-les au prix total ; prix après remise.
Voici le résultat du programme :
Copiez le code comme suit :
Total des prix réduits : 67,5
Le résultat est tout à fait correct, mais le code est un peu brouillon. Ce n'est pas de notre faute si nous ne pouvons écrire que comme nous l'avons fait. Cependant, un tel code est un peu rudimentaire. Il souffre non seulement d’une paranoïa de type basique, mais viole également le principe de responsabilité unique. Si vous travaillez à domicile et que vous avez des enfants qui veulent devenir codeurs, vous devez cacher votre code, au cas où ils le verraient et soupireraient de déception et diraient : « Vous gagnez votre vie en faisant ça ?
Il existe une meilleure façon
Nous pouvons faire mieux – et bien mieux. Notre code est un peu comme une spécification d’exigences. Cela peut réduire l'écart entre les exigences métier et le code mis en œuvre, réduisant ainsi le risque d'interprétation erronée des exigences.
Nous ne laissons plus Java créer une variable et l'attribuer à l'infini. Nous devons communiquer avec elle à partir d'un niveau d'abstraction supérieur, comme le code suivant.
Copiez le code comme suit :
final BigDecimal totalOfDiscountedPrices =
prix.stream()
.filter(prix -> price.compareTo(BigDecimal.valueOf(20)) > 0)
.map(prix -> price.multiply(BigDecimal.valueOf(0.9)))
.reduce(BigDecimal.ZERO, BigDecimal::add);
System.out.println("Total des prix réduits : " + totalOfDiscountedPrices);
Lisez-le à voix haute : filtrez les prix supérieurs à 20 yuans, convertissez-les en prix réduits, puis additionnez-les. Ce code est exactement le même que le processus que nous avons utilisé pour décrire nos exigences. En Java, il est également très pratique de plier une longue ligne de code et de l'aligner ligne par ligne en fonction du point devant le nom de la méthode, comme ci-dessus.
Le code est très simple, mais nous utilisons beaucoup de nouveautés en Java8. Tout d’abord, nous appelons une méthode de flux de la liste de prix. Cela ouvre la porte à d’innombrables itérateurs pratiques, dont nous parlerons plus tard.
Nous utilisons certaines méthodes spéciales, telles que le filtre et la carte, au lieu de parcourir directement toute la liste. Ces méthodes ne sont pas comme celles du JDK que nous avons utilisé auparavant, elles acceptent une fonction anonyme - expression lambda - comme paramètre. (Nous en discuterons en profondeur plus tard). Nous appelons la méthode réduire() pour calculer la somme des prix renvoyés par la méthode map().
Tout comme la méthode contain, le corps de la boucle est masqué. Cependant, la méthode map (et la méthode filter) est beaucoup plus compliquée. Il appelle l'expression lambda transmise pour calculer chaque prix dans la liste de prix et place le résultat dans une nouvelle collection. Enfin nous appelons la méthode réduire sur cette nouvelle collection pour obtenir le résultat final.
Voici le résultat du code ci-dessus :
Copiez le code comme suit :
Total des prix réduits : 67,5
axes d'amélioration
Il s’agit d’une amélioration significative par rapport à la mise en œuvre précédente :
1. Bien structuré mais pas encombré
2. Aucune opération de bas niveau
3. Logique facile à améliorer ou à modifier
4. Itération par bibliothèque de méthodes
5. Efficace ; évaluation paresseuse du corps de la boucle
6. Facilement parallélisé
Ci-dessous, nous parlerons de la façon dont Java implémente cela.
Les expressions lambda sont là pour sauver le monde
Les expressions lambda sont un raccourci qui nous évite les problèmes de programmation impérative. Cette nouvelle fonctionnalité fournie par Java a modifié notre méthode de programmation originale, rendant le code que nous écrivons non seulement concis et élégant, moins sujet aux erreurs, mais également plus efficace, facile à optimiser, améliorer et paralléliser.
Section 2 : Le plus grand gain de la programmation fonctionnelle
Le code de style fonctionnel a un rapport signal/bruit plus élevé ; moins de code est écrit, mais plus est fait par ligne ou expression. Par rapport à la programmation impérative, la programmation fonctionnelle nous a beaucoup profité :
On évite la modification ou l'affectation explicite de variables, qui sont souvent sources de bugs et rendent le code difficile à paralléliser. Dans la programmation en ligne de commande, nous attribuons continuellement des valeurs à la variable totalOfDiscountedPrices dans le corps de la boucle. Dans le style fonctionnel, le code ne subit plus d'opérations de modification explicites. Moins les variables sont modifiées, moins le code comporte de bugs.
Le code de style fonctionnel peut être facilement parallélisé. Si le calcul prend du temps, nous pouvons facilement exécuter les éléments de la liste simultanément. Si nous voulons paralléliser le code impératif, nous devons également nous soucier des problèmes causés par la modification simultanée de la variable totalOfDiscountedPrices. En programmation fonctionnelle, nous n'accédons à cette variable qu'après son traitement complet, éliminant ainsi les problèmes de sécurité des threads.
Le code est plus expressif. La programmation impérative est divisée en plusieurs étapes pour expliquer ce qu'il faut faire - créer une valeur d'initialisation, parcourir les prix, ajouter des prix réduits aux variables, etc. - tandis que la programmation fonctionnelle n'a besoin que que la méthode map de la liste renvoie une valeur incluant la remise. . Créez simplement une nouvelle liste de prix, puis cumulez-les.
La programmation fonctionnelle est plus simple ; moins de code est nécessaire pour obtenir le même résultat que la programmation impérative. Un code plus propre signifie moins de code à écrire, moins à lire et moins à maintenir - voir « Est-ce que moins succinct est suffisant pour être succinct ? »
Le code fonctionnel est plus intuitif – lire le code revient à décrire le problème – et est facile à comprendre une fois que l’on est familiarisé avec la syntaxe. La méthode map exécute la fonction donnée (calcule le prix réduit) pour chaque élément de la collection, puis renvoie l'ensemble de résultats, comme le montre la figure ci-dessous.
Figure 1 - la carte remplit la fonction donnée sur chaque élément de la collection
Avec les expressions lambda, nous pouvons exploiter pleinement la puissance de la programmation fonctionnelle en Java. En utilisant un style fonctionnel, vous pouvez écrire du code plus expressif, plus concis, comportant moins d'affectations et comportant moins d'erreurs.
La prise en charge de la programmation orientée objet constitue un avantage majeur de Java. La programmation fonctionnelle et la programmation orientée objet ne s'excluent pas mutuellement. Le véritable changement de style vient de la programmation en ligne de commande vers la programmation déclarative. Dans Java 8, les fonctions fonctionnelles et orientées objet peuvent être efficacement intégrées. Nous pouvons continuer à utiliser le style POO pour modéliser les entités de domaine ainsi que leurs états et relations. De plus, nous pouvons également utiliser des fonctions pour modéliser des transitions de comportement ou d'état, des flux de travail et du traitement des données, et créer des fonctions composites.
Section 3 : Pourquoi utiliser le style fonctionnel ?
Nous avons vu les avantages de la programmation fonctionnelle, mais vaut-il la peine d'utiliser ce nouveau style ? Est-ce juste une petite amélioration ou un changement complet ? Il reste encore de nombreuses questions pratiques auxquelles il faudra répondre avant de vraiment y consacrer du temps.
Copiez le code comme suit :
Xiao Ming a demandé :
Moins de code signifie-t-il simplicité ?
La simplicité signifie moins mais pas l'encombrement. En dernière analyse, cela signifie être capable d'exprimer l'intention de manière efficace. Les avantages sont considérables.
Écrire du code, c'est comme empiler des ingrédients. La simplicité signifie être capable de mélanger des ingrédients pour créer un assaisonnement. Écrire du code concis nécessite un travail acharné. Il y a moins de code à lire et le code vraiment utile est transparent pour vous. Un shortcode difficile à comprendre ou qui cache des détails est court plutôt que concis.
Un code simple signifie en fait une conception agile. Code simple sans formalités administratives. Cela signifie que nous pouvons rapidement tester des idées, passer à autre chose si elles fonctionnent bien et sauter rapidement si elles ne fonctionnent pas bien.
Écrire du code en Java n'est pas difficile et la syntaxe est simple. Et nous connaissons déjà très bien les bibliothèques et API existantes. Ce qui est vraiment difficile, c'est de l'utiliser pour développer et maintenir des applications au niveau de l'entreprise.
Nous devons nous assurer que nos collègues ferment la connexion à la base de données au bon moment, qu'ils ne continuent pas à occuper des transactions, que les exceptions sont traitées correctement au niveau de la couche appropriée, que les verrous sont acquis et libérés correctement, etc.
Pris individuellement, chacun de ces problèmes n’est pas grave. Cependant, combiné à la complexité du domaine, le problème devient très difficile, les ressources de développement sont limitées et la maintenance est difficile.
Que se passerait-il si nous encapsulions ces stratégies dans de nombreux petits morceaux de code et les laissions gérer les contraintes de manière indépendante ? Nous n’avons alors plus besoin de dépenser constamment de l’énergie pour mettre en œuvre des stratégies. C'est une énorme amélioration, regardons comment la programmation fonctionnelle le fait.
Itérations folles
Nous avons écrit diverses itérations pour traiter des listes, des ensembles et des cartes. L’utilisation d’itérateurs en Java est très courante, mais c’est trop compliqué. Non seulement ils occupent plusieurs lignes de code, mais ils sont également difficiles à encapsuler.
Comment parcourir la collection et les imprimer ? Vous pouvez utiliser une boucle for. Comment filtrer certains éléments de la collection ? Utilisez toujours une boucle for, mais vous devez ajouter des variables modifiables supplémentaires. Après avoir sélectionné ces valeurs, comment les utiliser pour trouver la valeur finale, telle que la valeur minimale, la valeur maximale, la valeur moyenne, etc. ? Ensuite, vous devez recycler et modifier les variables.
Ce genre d'itération est comme une panacée, elle peut tout faire, mais tout est clairsemé. Java fournit désormais des itérateurs intégrés pour de nombreuses opérations : par exemple, ceux qui effectuent uniquement des boucles, ceux qui effectuent des opérations de mappage, ceux qui filtrent les valeurs, ceux qui réduisent les opérations, et il existe de nombreuses fonctions pratiques telles que maximum, minimum et moyenne, etc. De plus, ces opérations peuvent être bien combinées, nous pouvons donc les rassembler pour implémenter une logique métier, qui est simple et nécessite moins de code. De plus, le code écrit est très lisible car il est logiquement cohérent avec l'ordre de description du problème. Nous verrons plusieurs exemples de ce type dans le chapitre 2, Utiliser les collections, à la page 19, et ce livre en regorge.
Appliquer la stratégie
Les stratégies sont mises en œuvre dans les applications à l’échelle de l’entreprise. Par exemple, nous devons confirmer qu'une opération a été correctement authentifiée pour des raisons de sécurité, nous devons nous assurer que la transaction peut être exécutée rapidement et que le journal des modifications est correctement mis à jour. Ces tâches finissent généralement par être un morceau de code ordinaire côté serveur, similaire au pseudocode suivant :
Copiez le code comme suit :
Transaction transaction = getFromTransactionFactory();
//... opération à exécuter dans la transaction...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
Il y a deux problèmes avec cette approche. Premièrement, cela entraîne souvent une duplication des efforts et augmente également les coûts de maintenance. Deuxièmement, il est facile d'oublier les exceptions qui peuvent être générées dans le code métier, ce qui peut affecter le cycle de vie des transactions et la mise à jour du journal des modifications. Cela devrait être implémenté à l'aide de blocs try et enfin, mais chaque fois que quelqu'un touche ce code, nous devons reconfirmer que cette stratégie n'a pas été détruite.
Il existe un autre moyen, nous pouvons supprimer l'usine et mettre ce code devant elle. Au lieu d'obtenir l'objet de transaction, transmettez le code exécuté à une fonction bien entretenue, comme ceci :
Copiez le code comme suit :
runWithinTransaction((Transaction) -> {
//... opération à exécuter dans la transaction...
});
C'est un petit pas pour vous, mais cela vous évite bien des ennuis. La stratégie consistant à vérifier l'état et à mettre à jour le journal en même temps est résumée et encapsulée dans la méthode runWithinTransaction. Nous envoyons à cette méthode un morceau de code qui doit être exécuté dans le contexte d'une transaction. Nous n'avons plus à craindre que quelqu'un oublie d'effectuer cette étape ou ne gère pas correctement l'exception. La fonction qui met en œuvre la politique s’en charge déjà.
Nous verrons comment utiliser les expressions lambda pour appliquer cette stratégie au chapitre 5.
Stratégie d'expansion
Les stratégies semblent être partout. En plus de les appliquer, les applications d’entreprise doivent également les étendre. Nous espérons ajouter ou supprimer certaines opérations via certaines informations de configuration. En d'autres termes, nous pouvons les traiter avant que la logique de base du module ne soit exécutée. Ceci est très courant en Java, mais doit être réfléchi et conçu à l'avance.
Les composants qui doivent être étendus ont généralement une ou plusieurs interfaces. Nous devons concevoir soigneusement l'interface et la structure hiérarchique des classes d'implémentation. Cela peut bien fonctionner, mais cela vous laissera avec un tas d'interfaces et de classes qui doivent être maintenues. Une telle conception peut facilement devenir lourde et difficile à entretenir, ce qui va finalement à l’encontre de l’objectif initial de mise à l’échelle.
Il existe une autre solution : les interfaces fonctionnelles et les expressions lambda, que nous pouvons utiliser pour concevoir des stratégies évolutives. Nous n'avons pas besoin de créer une nouvelle interface ou de suivre le même nom de méthode. Nous pouvons nous concentrer davantage sur la logique métier à implémenter, que nous mentionnerons dans l'utilisation des expressions lambda pour la décoration à la page 73.
La concurrence simplifiée
Une application volumineuse approche d'une étape de publication lorsqu'un grave problème de performances surgit soudainement. L’équipe a rapidement déterminé que le goulot d’étranglement en termes de performances résidait dans un énorme module qui traite d’énormes quantités de données. Un membre de l'équipe a suggéré que les performances du système pourraient être améliorées si les avantages du multicœur pouvaient être pleinement exploités. Cependant, si cet énorme module est écrit dans l'ancien style Java, la joie apportée par cette suggestion sera bientôt brisée.
L'équipe s'est rapidement rendu compte que faire passer ce géant de l'exécution en série à l'exécution en parallèle nécessiterait beaucoup d'efforts, ajouterait une complexité supplémentaire et provoquerait facilement des BUG liés au multithreading. N'y a-t-il pas une meilleure façon d'améliorer les performances ?
Est-il possible que les codes série et parallèle soient identiques, que vous choisissiez l'exécution en série ou en parallèle, tout comme appuyer sur un interrupteur et exprimer votre idée ?
Il semble que cela ne soit possible qu'à Narnia, mais si nous nous développons complètement en termes fonctionnels, tout cela deviendra une réalité. Les itérateurs intégrés et le style fonctionnel supprimeront le dernier obstacle à la parallélisation. La conception du JDK permet de basculer entre l'exécution série et parallèle avec seulement quelques changements de code discrets, que nous mentionnerons dans « Terminer le saut vers la parallélisation » à la page 145.
raconter des histoires
Beaucoup de choses sont perdues dans le processus de transformation des exigences métier en mise en œuvre du code. Plus les pertes sont importantes, plus le risque d’erreur et le coût de gestion sont élevés. Si le code semble décrire les exigences, il sera plus facile à lire, il sera plus facile de discuter avec les responsables des exigences et il sera plus facile de répondre à leurs besoins.
Par exemple, vous entendez le chef de produit dire : « Obtenez les prix de toutes les actions, trouvez celles dont les prix sont supérieurs à 500 yuans et calculez le total des actifs pouvant rapporter des dividendes. Grâce aux nouvelles fonctionnalités fournies par Java, vous pouvez écrire :
Copiez le code comme suit :
tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum()
Ce processus de conversion s’effectue presque sans perte car il n’y a pratiquement rien à convertir. Il s'agit d'un style fonctionnel en action, et vous en verrez de nombreux autres exemples tout au long du livre, en particulier le chapitre 8, Création de programmes avec des expressions Lambda, page 137.
Focus sur la quarantaine
Dans le développement de systèmes, l’activité principale et la logique fine qu’elle nécessite doivent généralement être isolées. Par exemple, un système de traitement des commandes peut vouloir utiliser différentes stratégies de taxation pour différentes sources de transactions. Isoler les calculs de taxes du reste de la logique de traitement rend le code plus réutilisable et évolutif.
En programmation orientée objet, nous appelons cela l'isolement des préoccupations, et le modèle de stratégie est généralement utilisé pour résoudre ce problème. La solution consiste généralement à créer des interfaces et des classes d’implémentation.
Nous pouvons obtenir le même effet avec moins de code. Nous pouvons également tester rapidement nos propres idées de produits sans avoir à créer un tas de code et à stagner. Nous explorerons plus en détail comment créer ce modèle et effectuer une isolation des problèmes via des fonctions légères dans Isolation des problèmes à l'aide d'expressions Lambda à la page 63.
évaluation paresseuse
Lors du développement d'applications au niveau de l'entreprise, nous pouvons interagir avec des services WEB, appeler des bases de données, traiter du XML, etc. Nous devons effectuer de nombreuses opérations, mais elles ne sont pas toutes nécessaires à tout moment. Éviter certaines opérations ou au moins retarder certaines opérations temporairement inutiles est l'un des moyens les plus simples d'améliorer les performances ou de réduire le démarrage et le temps de réponse du programme.
Ce n'est qu'une petite chose, mais cela demande beaucoup de travail pour l'implémenter de manière purement POO. Afin de retarder l'initialisation de certains objets lourds, nous devons gérer diverses références d'objets, vérifier les pointeurs nuls, etc.
Cependant, si vous utilisez la nouvelle classe Optinal et certaines API de style fonctionnel qu'elle fournit, ce processus deviendra très simple et le code sera plus clair. Nous en discuterons dans l'initialisation paresseuse à la page 105.
Améliorer la testabilité
Moins le code a de logique de traitement, moins il est probable que les erreurs soient corrigées. De manière générale, le code fonctionnel est plus facile à modifier et à tester.
De plus, tout comme le chapitre 4, Conception avec des expressions Lambda et le chapitre 5, Utilisation des ressources, les expressions lambda peuvent être utilisées comme objet fictif léger pour rendre les tests d'exception plus clairs et plus faciles à comprendre. Les expressions lambda peuvent également constituer une aide précieuse aux tests. De nombreux cas de test courants peuvent accepter et gérer les expressions lambda. Les cas de test écrits de cette manière peuvent capturer l'essence de la fonctionnalité qui doit être testée par régression. Dans le même temps, diverses implémentations qui doivent être testées peuvent être complétées en transmettant différentes expressions lambda.
Les cas de tests automatisés du JDK sont également un bon exemple d'application d'expressions lambda - si vous souhaitez en savoir plus, vous pouvez consulter le code source dans le référentiel OpenJDK. Grâce à ces programmes de test, vous pouvez voir comment les expressions lambda paramétrent les comportements clés du scénario de test ; par exemple, elles construisent le programme de test comme ceci, "Créer un conteneur pour les résultats", puis "Ajouter des postconditions paramétrées Vérifier".
Nous avons vu que la programmation fonctionnelle nous permet non seulement d'écrire du code de haute qualité, mais résout également avec élégance divers problèmes au cours du processus de développement. Cela signifie que le développement de programmes deviendra plus rapide et plus facile, avec moins d'erreurs - à condition de suivre quelques directives que nous présenterons plus tard.
Section 4 : Évolution et non révolution
Nous n'avons pas besoin de passer à un autre langage pour profiter des avantages de la programmation fonctionnelle ; tout ce que nous devons changer, c'est la façon dont nous utilisons Java. Les langages tels que C++, Java et C# prennent tous en charge la programmation impérative et orientée objet. Mais maintenant, ils commencent à adopter la programmation fonctionnelle. Nous venons d'examiner les deux styles de code et de discuter des avantages que la programmation fonctionnelle peut apporter. Examinons maintenant certains de ses concepts clés et exemples pour nous aider à apprendre ce nouveau style.
L'équipe de développement du langage Java a consacré beaucoup de temps et d'énergie à ajouter des capacités de programmation fonctionnelle au langage Java et au JDK. Pour profiter des avantages qu’il apporte, nous devons d’abord introduire quelques nouveaux concepts. Nous pouvons améliorer la qualité de notre code à condition de suivre les règles suivantes :
1. Déclaratif
2. Promouvoir l'immuabilité
3. Évitez les effets secondaires
4. Préférez les expressions aux déclarations
5. Conception utilisant des fonctions d'ordre supérieur
Jetons un coup d’œil à ces directives pratiques.
déclaratif
Le cœur de ce que nous connaissons sous le nom de programmation impérative est la variabilité et la programmation pilotée par commandes. Nous créons des variables puis modifions continuellement leurs valeurs. Nous fournissons également des instructions détaillées à exécuter, comme générer le flag d'index de l'itération, incrémenter sa valeur, vérifier si la boucle est terminée, mettre à jour le Nième élément du tableau, etc. Dans le passé, en raison des caractéristiques des outils et des limitations matérielles, nous ne pouvions écrire du code que de cette manière. Nous avons également vu que sur une collection immuable, la méthode déclarative contain est plus facile à utiliser que la méthode impérative. Tous les problèmes difficiles et opérations de bas niveau sont implémentés dans les fonctions de la bibliothèque, et nous n'avons plus à nous soucier de ces détails. Par souci de simplicité, nous devrions également utiliser la programmation déclarative. L'immuabilité et la programmation déclarative sont l'essence de la programmation fonctionnelle, et maintenant Java en fait enfin une réalité.
Promouvoir l'immuabilité
Le code avec des variables mutables aura de nombreux chemins d'activité. Plus vous modifiez de choses, plus il est facile de détruire la structure d'origine et d'introduire davantage d'erreurs. Le code avec plusieurs variables modifiées est difficile à comprendre et à paralléliser. L'immuabilité élimine essentiellement ces soucis. Java prend en charge l'immuabilité mais ne l'exige pas - mais nous le pouvons. Nous devons changer la vieille habitude de modifier l’état des objets. Nous devrions utiliser autant que possible des objets immuables. Lorsque vous déclarez des variables, des membres et des paramètres, essayez de les déclarer comme finaux, tout comme le célèbre dicton de Joshua Bloch dans "Effective Java", "Traitez les objets comme immuables". Lors de la création d'objets, essayez de créer des objets immuables, tels que String. Lors de la création d'une collection, essayez de créer une collection immuable ou non modifiable, par exemple en utilisant des méthodes telles que Arrays.asList() et unmodifiableList() de Collections. En évitant la variabilité, nous pouvons écrire des fonctions pures, c'est-à-dire des fonctions sans effets secondaires.
éviter les effets secondaires
Supposons que vous écriviez un morceau de code pour récupérer le prix d’une action sur Internet et l’écrivez dans une variable partagée. Si nous avons de nombreux prix à obtenir, nous devons effectuer ces opérations chronophages en série. Si nous voulons profiter de la puissance du multithreading, nous devons faire face aux problèmes de threading et de synchronisation pour éviter les conditions de concurrence. Le résultat final est que les performances du programme sont très mauvaises et que les gens oublient de manger et de dormir afin de maintenir le fil. Si les effets secondaires étaient éliminés, nous pourrions complètement éviter ces problèmes. Une fonction sans effets secondaires favorise l'immuabilité et ne modifie aucune entrée ou quoi que ce soit d'autre dans sa portée. Ce type de fonction est très lisible, comporte peu d’erreurs et est facile à optimiser. Puisqu’il n’y a pas d’effets secondaires, il n’y a pas lieu de s’inquiéter des conditions de concurrence ou des modifications simultanées. De plus, nous pouvons facilement exécuter ces fonctions en parallèle, dont nous parlerons à la page 145.
Préférer les expressions
Les déclarations sont une patate chaude car elles obligent à des modifications. Les expressions favorisent l’immuabilité et la composition des fonctions. Par exemple, nous utilisons d’abord l’instruction for pour calculer le prix total après remises. Un tel code conduit à de la variabilité et à un code verbeux. L’utilisation de versions plus expressives et déclaratives des méthodes map et sum évite non seulement les opérations de modification, mais permet également d’enchaîner les fonctions. Lorsque vous écrivez du code, vous devriez essayer d’utiliser des expressions plutôt que des instructions. Cela rend le code plus simple et plus facile à comprendre. Le code sera exécuté selon la logique métier, tout comme lorsque nous avons décrit le problème. Une version concise est sans aucun doute plus facile à modifier si les exigences changent.
Conception utilisant des fonctions d'ordre supérieur
Java n'impose pas l'immuabilité comme les langages fonctionnels tels que Haskell, mais il nous permet de modifier les variables. Java n’est donc pas et ne sera jamais un langage de programmation purement fonctionnel. Cependant, nous pouvons utiliser des fonctions d’ordre supérieur pour la programmation fonctionnelle en Java. Les fonctions d'ordre supérieur font passer la réutilisation au niveau supérieur. Avec des fonctions d’ordre élevé, nous pouvons facilement réutiliser du code mature, petit, spécialisé et hautement cohérent. En POO, nous avons l'habitude de passer des objets aux méthodes, de créer de nouveaux objets dans les méthodes, puis de renvoyer les objets. Les fonctions d’ordre supérieur font les mêmes choses aux fonctions que les méthodes aux objets. Avec des fonctions d’ordre supérieur, nous le pouvons.
1. Passer d'une fonction à l'autre
2. Créez une nouvelle fonction dans la fonction
3. Renvoie des fonctions dans les fonctions
Nous avons déjà vu un exemple de passage de paramètres d'une fonction à une autre fonction, et plus tard nous verrons des exemples de création et de renvoi de fonctions. Regardons à nouveau l'exemple de « passage de paramètres à une fonction » :
Copiez le code comme suit :
prix.stream()
.filter(prix -> price.compareTo(BigDecimal.valueOf(20)) > 0) .map(price -> price.multiply(BigDecimal.valueOf(0.9)))
rapporter l'erratum • discuter
.reduce(BigDecimal.ZERO, BigDecimal::add);
Dans ce code, nous passons la fonction price -> price.multiply(BigDecimal.valueOf(0.9)) à la fonction map. La fonction transmise est créée lorsque la mappe de fonctions d'ordre supérieur est appelée. De manière générale, une fonction a un corps de fonction, un nom de fonction, une liste de paramètres et une valeur de retour. Cette fonction créée à la volée possède une liste de paramètres suivie d'une flèche (->), puis d'un court corps de fonction. Les types de paramètres sont déduits par le compilateur Java, et le type de retour est également implicite. C'est une fonction anonyme, elle n'a pas de nom. Mais nous ne l’appelons pas une fonction anonyme, nous l’appelons une expression lambda. Passer des fonctions anonymes en tant que paramètres n'a rien de nouveau en Java ; nous avons souvent déjà transmis des classes internes anonymes. Même si une classe anonyme n’a qu’une seule méthode, nous devons quand même suivre le rituel de création d’une classe et de son instanciation. Avec les expressions lambda, nous pouvons profiter d'une syntaxe légère. Non seulement cela, nous avons toujours été habitués à abstraire certains concepts dans divers objets, mais nous pouvons désormais abstraire certains comportements dans des expressions lambda. Programmer avec ce style de codage nécessite encore une certaine réflexion. Nous devons transformer notre pensée impérative déjà enracinée en pensée fonctionnelle. Cela peut être un peu pénible au début, mais vous vous y habituerez vite. Au fur et à mesure que vous approfondissez, ces API non fonctionnelles seront progressivement laissées pour compte. Arrêtons d'abord ce sujet. Voyons comment Java gère les expressions lambda. Avant, nous passions toujours des objets aux méthodes, nous pouvons désormais stocker des fonctions et les transmettre. Jetons un coup d'œil au secret de la capacité de Java à prendre des fonctions comme paramètres.
Section 5 : Ajout d'un peu de sucre de syntaxe
Cela peut également être réalisé en utilisant les fonctions originales de Java, mais les expressions lambda ajoutent du sucre syntaxique, économisant ainsi certaines étapes et simplifiant notre travail. Le code ainsi écrit se développe non seulement plus rapidement, mais exprime également mieux nos idées. De nombreuses interfaces que nous utilisions dans le passé n'avaient qu'une seule méthode : Runnable, Callable, etc. Ces interfaces peuvent être trouvées partout dans la bibliothèque JDK, et là où elles sont utilisées, elles peuvent généralement être réalisées avec une fonction. Les fonctions de bibliothèque qui ne nécessitaient auparavant qu'une interface à méthode unique peuvent désormais transmettre des fonctions légères, grâce au sucre syntaxique fourni par les interfaces fonctionnelles. L'interface fonctionnelle est une interface avec une seule méthode abstraite. Regardez ces interfaces avec une seule méthode, Runnable, Callable, etc., cette définition s'applique à elles. Il existe d'autres interfaces de ce type dans JDK8 - Fonction, Prédicat, Consommateur, Fournisseur, etc. (page 157, l'Annexe 1 contient une liste d'interfaces plus détaillée). Les interfaces fonctionnelles peuvent avoir plusieurs méthodes statiques et méthodes par défaut, qui sont implémentées dans l'interface. Nous pouvons utiliser l'annotation @FunctionalInterface pour annoter une interface fonctionnelle. Le compilateur n'utilise pas cette annotation, mais il peut identifier plus clairement le type de cette interface. Non seulement cela, si nous annotons une interface avec cette annotation, le compilateur vérifie avec force s'il est conforme aux règles des interfaces fonctionnelles. Si une méthode reçoit une interface fonctionnelle en tant que paramètre, les paramètres que nous pouvons passer comprennent:
1. Classes intérieures anonymes, la manière la plus ancienne
2.Lambda Expression, tout comme nous l'avons fait dans la méthode de la carte
3. Référence à une méthode ou un constructeur (nous en parlerons plus tard)
Si le paramètre de méthode est une interface fonctionnelle, le compilateur acceptera joyeusement une expression de Lambda ou une référence de méthode en tant que paramètre. Si nous passons une expression de lambda à une méthode, le compilateur convertira d'abord l'expression en une instance de l'interface fonctionnelle correspondante. Cette transformation est plus qu'une simple génération d'une classe intérieure. Les méthodes de cette instance générée de manière synchrone correspondent aux méthodes abstraites de l'interface fonctionnelle du paramètre. Par exemple, la méthode MAP reçoit la fonction d'interface fonctionnelle comme un paramètre. Lorsque vous appelez la méthode MAP, le compilateur Java le générera de manière synchrone, comme indiqué dans la figure ci-dessous.
Les paramètres de l'expression de lambda doivent correspondre aux paramètres de la méthode abstraite de l'interface. Cette méthode générée renverra le résultat de l'expression de lambda. Si le type de retour ne correspond pas directement à la méthode abstraite, cette méthode convertira la valeur de retour au type approprié. Nous avons déjà eu un aperçu de la façon dont les expressions de lambda sont transmises aux méthodes. Prenons rapidement de quoi nous venons de parler, puis commençons notre exploration des expressions de Lambda.
Résumer
Il s'agit d'un tout nouveau domaine de Java. Grâce à des fonctions d'ordre supérieur, nous pouvons désormais écrire un code de style fonctionnel élégant et fluide. Le code écrit de cette manière est concis et facile à comprendre, a peu d'erreurs et est propice à la maintenance et à la parallélisation. Le compilateur Java fonctionne sa magie et où nous recevons des paramètres d'interface fonctionnelle, nous pouvons passer des expressions de lambda ou des références de méthode. Nous pouvons désormais entrer dans le monde des expressions de Lambda et les bibliothèques JDK adaptées pour eux de ressentir leur plaisir. Dans le chapitre suivant, nous commencerons par les opérations d'ensemble les plus courantes en programmation et libérerons la puissance des expressions de lambda.