Chapitre 10: Utilisation de l’Optional
Ce chapitre couvre
- Quel est le problème avec les références nulles et pourquoi vous devriez les éviter
- Du null à l’Optional: réécrire votre domaine modèle d’une manière à le protéger des NullPointerException
- Mettre les Optional au travail: supprimer les vérifications nuls de votre code
- Différentes façons de lire la valeur éventuellement contenue dans un Optional.
- Repenser la programmation en fonction des valeurs potentiellement manquantes
Levez la main si vous avez déjà reçu une NullPointerException durant votre vie de développeur Java. Continuez à lire si c’est l’exception que vous rencontrez le plus fréquemment. Malheureusement, nous ne pouvons pas vous voir en ce moment, mais nous croyons qu’il y a une très forte probabilité que votre main soit levée maintenant. J’imagine aussi que vous pensez peut-être quelque chose du genre « Oui, je suis d’accord, NullPointerExceptions est pénible pour tout développeur Java, novice ou expert, mais nous ne pouvons pas faire grand-chose à leur sujet. »C’est un sentiment commun dans le monde de la programmation (impératif); néanmoins, ce n’est peut-être pas toute la vérité mais plutôt un parti pris avec des racines historiques solides.
10.1. Comment modélisez-vous l’absence de valeur?
Imaginez que vous ayez la structure d’objet imbriquée suivante pour une personne possédant une voiture et ayant une assurance automobile.
Listing 10.1. Le modèle de données Personne / Voiture / Assurance
Ensuite, ce qui est peut-être problématique avec le code suivant?
Ce code semble assez raisonnable, mais beaucoup de gens ne possèdent pas de voiture. Alors, quel est le résultat de l’appel de la méthode getCar? Une pratique malheureuse courante consiste à retourner la référence nulle pour indiquer l’absence d’une valeur, ici pour indiquer l’absence d’une voiture. En conséquence, l’appel à getInsurance renverra l’assurance d’une référence nulle, ce qui entraînera une exception NullPointerException à l’exécution et empêchera votre programme de continuer à fonctionner. Mais ce n’est pas tout. Et si la personne était nulle? Que faire si la méthode getInsurance a retournée elle aussi null?
10.1.1. Réduction de NullPointerExceptions avec vérification défensive
Que pouvez-vous faire pour éviter de tomber sur une exception NullPointerException inattendue? Typiquement, vous pouvez ajouter des contrôles nuls si nécessaire (et parfois, dans un excès de programmation défensive, même si ce n’est pas nécessaire) et souvent avec des styles différents. Une première tentative d’écriture d’une méthode empêchant une exception NullPointerException est présentée dans la liste suivante.
Listing 10.2. Tentative null-safe 1: doutes profonds
Cette méthode effectue une vérification nulle chaque fois qu’elle déréférence une variable, renvoyant la chaîne « Inconnu » si l’une des variables traversées dans cette chaîne de déréférencement est une valeur nulle. La seule exception à cela est que vous ne vérifiez pas si le nom de la compagnie d’assurance est nul parce que, comme toute autre entreprise, vous savez qu’il doit avoir un nom. Notez que vous pouvez éviter cette dernière vérification uniquement en raison de votre connaissance du domaine métier, mais cela ne se reflète pas dans les classes Java qui modélisent vos données.
Nous avons étiqueté la méthode dans la liste 10.2 « doutes profonds » parce qu’il montre un modèle récurrent: chaque fois que vous avez un doute qu’une variable pourrait être nulle, vous êtes obligé d’ajouter un autre bloc imbriqué, augmentant le niveau d’indentation du code . Cette échelle évolue mal et compromet la lisibilité, alors peut-être que vous aimeriez essayer une autre solution. Essayons d’éviter ce problème en faisant quelque chose de différent dans la liste suivante.
Listing 10.3. Tentative Null-safe 2: trop de sorties
Dans cette deuxième tentative, vous essayez d’éviter d’imbriquer les blocs si profondément, en adoptant une stratégie différente: chaque fois que vous rencontrez une variable nulle, vous renvoyez la chaîne « Inconnu ». Néanmoins, cette solution est également loin d’être idéale; maintenant la méthode a quatre points de sortie distincts, ce qui la rend difficilement maintenable. Pire encore, la valeur par défaut à renvoyer en cas de null, la chaîne « Unknown », est répétée à trois endroits – et j’espère n’est pas mal orthographié! Bien sûr, vous voudrez peut-être l’extraire en une constante pour éviter ce problème.
En outre, c’est un processus sujet aux erreurs; Et si vous oubliez de vérifier qu’une propriété pourrait être nulle? Nous disons dans ce chapitre que l’utilisation de null pour représenter l’absence d’une valeur est la mauvaise approche. Ce dont vous avez besoin est une meilleure façon de modéliser l’absence et la présence d’une valeur.
10.1.2. Problèmes avec null
Pour récapituler notre discussion jusqu’à présent, l’utilisation de références nulles en Java pose à la fois des problèmes théoriques et pratiques:
- C’est une source d’erreur. NullPointerException est de loin l’exception la plus courante en Java.
- Cela bloque votre code. Cela aggrave la lisibilité en obligeant à remplir votre code avec des vérifications nuls souvent profondément imbriquées.
- C’est sans signification. Il n’a aucune signification sémantique, et en particulier il représente la mauvaise façon de modéliser l’absence de valeur dans un langage typé statiquement.
- Cela brise la philosophie Java. Java cache toujours les pointeurs aux développeurs sauf dans un cas: le pointeur NULL.
- Cela crée un trou dans le système de types. null ne contient aucun type ou autre information, ce qui signifie qu’il peut être affecté à n’importe quel type de référence. C’est un problème parce que, quand il est propagé à une autre partie du système, vous n’avez aucune idée de ce que ce null était censé être initialement.
Pour fournir un contexte pour savoir quelles autres solutions existent pour ce problème, examinons brièvement ce que d’autres langages de programmation peuvent offrir.
10.1.3. Quelles sont les alternatives à null dans d’autres langues?
Ces dernières années, d’autres langages comme Groovy ont travaillé autour de ce problème en introduisant un opérateur de navigation sûr, représenté par?., pour naviguer en toute sécurité à travers des valeurs potentiellement nulles. Pour comprendre comment cela fonctionne, considérez le code Groovy suivant pour récupérer le nom de la compagnie d’assurance utilisée par une personne donnée pour assurer leur voiture:
Ce que cette déclaration fait est assez clair. Une personne peut ne pas avoir de voiture et vous avez tendance à modéliser cette possibilité en attribuant une valeur nulle à la référence de voiture de l’objet Personne. De même, une voiture pourrait ne pas avoir d’assurance. L’opérateur de navigation sécurisé Groovy vous permet de naviguer en toute sécurité à travers ces références potentiellement nulles sans lancer une exception NullPointerException, en propageant simplement la référence null via la chaîne d’invocations, en retournant une valeur null dans le cas où une valeur de la chaîne est null.
Une fonctionnalité similaire a été proposée puis supprimée pour Java 7. Cependant, nous ne semblons pas manquer un opérateur de navigation sûr en Java; la première tentation de tous les développeurs Java face à une exception NullPointerException est de la réparer rapidement en ajoutant une instruction if, en vérifiant qu’une valeur n’est pas nulle avant d’invoquer une méthode sur celle-ci. Si vous résolvez ce problème sans vous demander s’il est correct que votre algorithme ou votre modèle de données puisse présenter une valeur nulle dans cette situation spécifique, vous ne corrigez pas un bogue mais le masquez, ce qui rend sa découverte et sa résolution beaucoup plus difficiles, pour celui qui sera appelé à travailler dessus la prochaine fois; ce sera très probablement même peut être vous, la semaine prochaine ou le mois prochain. Vous êtes en train de balayer la saleté sous le tapis. L’opérateur de déréférencement null-safe de Groovy n’est qu’un balai plus gros et plus puissant pour faire cette erreur, sans trop s’inquiéter de ses conséquences.
D’autres langages fonctionnels, tels que Haskell et Scala, adoptent un point de vue différent. Haskell inclut un type Maybe, qui encapsule essentiellement une valeur facultative. Une valeur de type Maybe peut contenir une valeur d’un type donné ou rien. Il n’y a pas de concept de référence nulle. Scala a une construction similaire appelée Option [T] pour encapsuler la présence ou l’absence d’une valeur de type T, dont nous discutons au chapitre 15. Vous devez alors vérifier explicitement si une valeur est présente ou non en utilisant les opérations disponibles sur le type Option , ce qui renforce l’idée de « vérification nulle ». Vous ne pouvez plus oublier de le faire car il est appliqué par le système de types.
D’accord, nous avons divergé un peu, et tout cela semble assez abstrait. Vous vous demandez peut-être «Alors, qu’en est-il de Java 8?» En fait, Java 8 s’inspire de cette idée d’une «valeur optionnelle» en introduisant une nouvelle classe appelée java.util.Optional <T>! Dans ce chapitre, nous montrons les avantages de l’utiliser pour modéliser des valeurs potentiellement absentes au lieu de leur assigner une référence nulle. Nous clarifierons également comment cette migration des valeurs nulles vers les Optional nécessite de repenser la façon dont vous traitez les valeurs optionnelles dans votre domaine modèle. Enfin, nous explorerons les fonctionnalités de cette nouvelle classe optionnelle et fournirons quelques exemples pratiques montrant comment l’utiliser efficacement. En fin de compte, vous apprendrez comment concevoir de meilleures API dans lesquelles, simplement en lisant la signature d’une méthode, les utilisateurs peuvent dire s’il faut s’attendre à une valeur optionnelle.
10.2. Présentation de la classe optionnelle
Java 8 introduit une nouvelle classe appelée java.util.Optional <T> inspirée des idées de Haskell et Scala. C’est une classe qui encapsule une valeur optionnelle. Cela signifie, par exemple, que si vous savez qu’une personne peut ou non avoir une voiture, la variable de voiture à l’intérieur de la classe Person ne doit pas être déclarée de type Car et affectée à une référence nulle lorsque la personne ne possède pas de voiture, mais devrait plutôt être de type Optional <Car>, comme illustré dans la figure 10.1.
Figure 10.1. Une voiture optionnelle
Lorsqu’une valeur est présente, la classe Optional l’enveloppe simplement. Inversement, l’absence d’une valeur est modélisée avec un optionnel « vide » renvoyé par la méthode Optional.empty. C’est une méthode factory statique qui renvoie une instance singleton spéciale de la classe Optional. Vous pourriez vous demander quelle est la différence entre une référence nulle et optionnel .empty (). Sémantiquement, ils peuvent être considérés comme la même chose, mais dans la pratique, la différence est énorme: essayer de déréférencer un null provoquera invariablement une exception NullPointer, alors que Optional.empty () est un objet valide, utilisable de type Optional qui peut être invoqué de manière utile. Vous verrez bientôt comment.
Une différence sémantique pratique et importante dans l’utilisation des optional à la place des valeurs nulles est que dans le premier cas, déclarer une variable de type Optional <Car> au lieu de Car indique clairement qu’une valeur manquante y est permise. Inversement, toujours utiliser le type Car et éventuellement assigner une référence null à une variable de ce type implique que vous n’avez pas d’aide, autre que votre connaissance du business model, pour comprendre si le null appartient au domaine valide de ce donné variable ou non.
Dans cet esprit, vous pouvez retravailler le modèle d’origine de la liste 10.1 en utilisant la classe Optional comme suit.
Listing 10.4. Redéfinir le modèle de données Personne / Voiture / Assurance en utilisant Optional
Notez comment l’utilisation de la classe Optional enrichit la sémantique de votre modèle. Le fait qu’une personne fasse référence à un Optional<Car> et à une assurance Optional<Insurance> rend explicite dans le domaine qu’une personne pourrait ou non posséder une voiture, et cette voiture pourrait ou non être assurée.
Dans le même temps, le fait que le nom de la compagnie d’assurance soit déclaré de type String au lieu de Optional <String> rend évident qu’il est obligatoire pour une compagnie d’assurance d’avoir un nom. De cette façon, vous savez avec certitude si vous obtiendrez une exception NullPointerException lorsque vous déréférencer le nom d’une compagnie d’assurance; vous n’avez pas besoin d’ajouter une vérification nulle, car cela cacherait simplement le problème au lieu de le corriger. Une compagnie d’assurance doit avoir un nom, donc si vous en trouvez un sans, vous devrez trouver ce qui ne va pas dans vos données au lieu d’ajouter un morceau de code couvrant cette circonstance. Il est important de noter que l’intention de la classe Optional n’est pas de remplacer chaque référence null. Au lieu de cela, son but est de vous aider à concevoir des API plus compréhensibles afin qu’en lisant simplement la signature d’une méthode, vous puissiez dire si vous attendez une valeur optionnelle. Cela vous oblige à déplier activement un Optional pour faire face à l’absence de valeur.
10.3. Pattern pour l’adoption de l’API Optional
Jusqu’ici tout va bien; Vous avez appris à utiliser les Optional dans les types pour clarifier votre modèle de domaine et les avantages que cela offre par rapport aux valeurs manquantes avec des références nulles. Mais comment pouvez-vous les utiliser maintenant? Que pouvez-vous faire avec eux, ou plus précisément comment pouvez-vous réellement utiliser une valeur enveloppée dans un Optional?
10.3.1. Création d’objets facultatifs
La première étape avant de travailler avec Optional est d’apprendre à créer des objets optionnels! Il y a plusieurs façons.
Optional vide
Comme mentionné précédemment, vous pouvez obtenir un objet Optional vide en utilisant la méthode factory statique Optional.empty():
Optional à partir d’une valeur non nulle
Vous pouvez également créer un Optional à partir d’une valeur non nulle avec la méthode statique Optional.of:
Si la voiture était nulle, une exception NullPointerException serait immédiatement lancée (plutôt que d’avoir une erreur latente lorsque vous essayez d’accéder aux propriétés de la voiture).
Optional à partir de null
Enfin, en utilisant la méthode factory statique Optional.ofNullable, vous pouvez créer un objet Optional pouvant contenir une valeur null:
Si la voiture était null, l’objet Optional résultant serait vide.
Vous pouvez imaginer que nous continuerons en enquêtant sur «comment obtenir une valeur à partir d’un optionnel». En particulier, il y a une méthode get qui fait précisément cela, et nous en reparlerons plus tard. Mais get déclenche une exception lorsque l’option est vide, et donc l’utiliser de manière mal disciplinée recrée efficacement tous les problèmes de maintenance causés par l’utilisation de null. Nous commençons donc par chercher des moyens d’utiliser des valeurs optionnelles qui évitent les tests explicites; ceux-ci sont inspirés des opérations similaires sur les Streams.
10.3.2. Extraction et transformation de valeurs à partir d’options avec une carte
Un modèle commun consiste à extraire des informations d’un objet. Par exemple, vous pouvez extraire le nom d’une compagnie d’assurance. Vous devez vérifier si l’assurance est nulle avant d’extraire le nom comme suit:
Optional prend en charge une méthode map pour ce pattern. Cela fonctionne comme suit (à partir de maintenant, nous utilisons le modèle présenté dans la liste 10.4):
Il est conceptuellement similaire à la méthode map du flux que vous avez vue dans les chapitres 4 et 5. L’opération map applique la fonction fournie à chaque élément d’un flux. Vous pouvez également considérer un objet Optional comme une collection particulière de données, contenant au plus un seul élément. Si le paramètre facultatif contient une valeur, la fonction transmise en tant qu’argument à mapper transforme cette valeur. Si l’option est vide, rien ne se passe.
Cela semble utile, mais comment pouvez-vous l’utiliser pour écrire le code précédent, qui enchaînait plusieurs appels de méthode?
Nous devons regarder une autre méthode supportée par Optional: flatMap!
10.3.3. Chaînage des objets Optional avec flatMap
Parce que vous avez appris à utiliser la méthode map, votre première réaction peut être de réécrire le code précédent en utilisant la méthode map comme suit:
Malheureusement, ce code ne compile pas. Pourquoi? La variable optPeople est de type Optional <People>, donc il est parfaitement possible d’appeler la méthode map dessus. Mais getCar renvoie un objet de type Optional <Car> (comme présenté dans la liste 10.4). Cela signifie que le résultat de l’opération map est un objet de type Optional <Optional <Car >>. Par conséquent, l’appel à getInsurance n’est pas valide car l’Optional la plus externe contient un objet Optional, qui bien entendu, ne prend pas en charge la méthode getInsurance. La Figure 10.3 illustre la structure imbriquée que vous obtiendriez.
Figure 10.3. Optional à 2 niveaux
Alors, comment pouvons-nous résoudre ce problème? Encore une fois, nous pouvons regarder un modèle que vous avez utilisé précédemment avec les flux: la méthode flatMap. Avec les flux, la méthode flatMap prend une fonction en tant qu’argument, ce qui renvoie un autre flux. Cette fonction est appliquée à chaque élément d’un flux, ce qui entraînerait un flux de flux. Mais flatMap a pour effet de remplacer chaque flux généré par le contenu de ce flux. En d’autres termes, tous les flux séparés générés par la fonction sont fusionnés ou aplatis en un seul flux.
Utilisation d’Optional dans un domaine modèle et pourquoi elles ne sont pas sérialisables
Dans la liste 10.5, nous avons montré comment utiliser Optionals. Cependant, les concepteurs de la classe Optionnelle l’ont développée à partir de différentes hypothèses et avec un cas d’utilisation différent à l’esprit.
Étant donné que la classe Optional n’était pas destinée à être utilisée en tant que type de champ, elle n’implémente pas non plus l’interface Serializable. Pour cette raison, l’utilisation d’Optionals dans votre domaine modèle peut interrompre des applications qui fonctionnent à l’aide d’outils ou de frameworks nécessitant un modèle sérialisable. Néanmoins, je crois que nous avons montré pourquoi l’utilisation d’Optionals comme type approprié dans votre domaine est une bonne idée, surtout quand vous devez traverser un graphe d’objets qui pourraient être, en tout ou en partie, potentiellement inexistants. Alternativement, si vous avez besoin d’un modèle de domaine sérialisable, nous vous suggérons au moins de fournir une méthode permettant d’accéder également à toute valeur éventuellement manquante en retournant plutôt une Optional, comme dans l’exemple suivant:
10.3.4. Actions par défaut et déballage d’une optional
La classe Optional offre plusieurs méthodes d’instance permettant de lire la valeur contenue dans une Optional.
- get() est la plus simple mais aussi la moins sûre de ces méthodes. Elle retourne la valeur encapsulée si elle est présente mais provoque une NoSuchElementException dans le cas contraire. Pour cette raison, l’utilisation de cette méthode est presque toujours une mauvaise idée, sauf si vous êtes vraiment sûr que l’option contient une valeur. De plus, ce n’est pas vraiment une amélioration par rapport aux vérifications nuls imbriquées.
- orElse(T other) est la méthode utilisée dans la liste 10.5, et comme nous l’avons notée ici, elle vous permet de fournir une valeur par défaut lorsque l’Optional ne contient pas de valeur.
- orElseGet(Supplier<? extends T> autre) est la contrepartie de la méthode orElse, car le Supplier est appelé uniquement si l’Optional ne contient aucune valeur. Vous devez utiliser cette méthode lorsque la création de la variable par défaut prends beaucoup de temps ou si vous voulez être sûre que la création ne sera faite que si l’Optional est vide.
- orElseThrow (Fournisseur <? extends X> exceptionSupplier) est similaire à la méthode get en ce sens qu’elle déclenche une exception lorsque l’Optional est vide, mais dans ce cas, elle vous permet de choisir le type d’exception à lancer.
- ifPresent (Consumer <? super T> consumer) vous permet d’exécuter l’action donnée en argument si une valeur est présente; sinon aucune action n’est exécutée.
Les analogies entre la classe Optional et l’interface Stream ne sont pas limitées aux méthodes map et flatMap. Il y a une troisième méthode, filter, qui se comporte de la même manière, et nous l’explorons dans la section 10.3.6.
10.3.5. Combiner deux Optionals
Supposons maintenant que vous ayez une méthode qui permet à une personne et à une voiture d’interroger des services externes et d’implémenter une logique métier assez complexe pour trouver la compagnie d’assurance offrant la politique la moins chère pour cette combinaison:
Supposons également que vous souhaitiez développer une version de cette méthode avec une sécurité sur les valeurs nulles. Elle prendrait deux Optionals comme arguments et retournerait une Optional<Insurance> qui sera vide si au moins une des valeurs qui lui est passée en paramètre est également vide. La classe Optional fournit également une méthode isPresent qui retourne true si l’Optional contient une valeur, donc votre première tentative pourrait être d’implémenter cette méthode comme suit:
Cette méthode a l’avantage de préciser dans sa signature que les valeurs de la personne et de la voiture qui lui ont été transmises peuvent être manquantes et que, pour cette raison, elle ne peut renvoyer aucune valeur. Malheureusement, son implémentation ressemble trop aux vérifications nuls que vous écririez si la méthode prenait comme arguments une Personne et une Voiture et ces deux arguments pourraient être potentiellement nuls. Existe-t-il un moyen meilleur et plus idiomatique d’implémenter cette méthode en utilisant les fonctionnalités de la classe Optional? Prenez quelques minutes pour passer par le Quiz 10.1 et essayez de trouver une solution élégante.
Quiz 10.1: Combiner deux options sans les déballer
En utilisant une combinaison des méthodes map et flatMap que vous avez apprises dans cette section, réécrivez l’implémentation de l’ancienne méthode nullSafeFindCheapestInsurance() dans une instruction unique.
Réponse:
Vous pouvez implémenter cette méthode dans une instruction unique et sans utiliser de constructions conditionnelles comme l’opérateur ternaire comme suit:
Ici vous appelez un flatMap sur le premier optional, donc si c’est vide, l’expression lambda qui lui est passée ne sera pas exécutée du tout et cette invocation retournera juste un optional vide. Inversement, si la personne est présente, elle l’utilise comme entrée d’une fonction renvoyant une Optional<Insurance> comme requis par la méthode flatMap. Le corps de cette fonction invoque une Map sur le second optional, donc s’il ne contient aucune voiture, la fonction renverra un Optional vide et ainsi la méthode nullSafeFindCheapestInsurance renvera une Optional vide. Enfin, si la personne et la voiture sont présentes, l’expression lambda transmise en tant qu’argument à la méthode map peut invoquer la méthode findCheapestInsurance originale avec elles.
Les analogies entre la classe Optional et l’interface Stream ne sont pas limitées aux méthodes map et flatMap. Il y a une troisième méthode, filter, qui se comporte de la même manière sur les deux classes.
10.3.6. Rejet de certaines valeurs avec des filtres
Souvent, vous devez appeler une méthode sur un objet et vérifier certaines propriétés. Par exemple, vous devrez peut-être vérifier si le nom de l’assurance est égal à « Cambridge-Insurance ». Pour cela, vous devez d’abord vérifier si la référence pointant sur un objet Insurance est nulle, puis appeler la méthode getName , comme suit:
Ce pattern peut être réécrit à l’aide de la méthode de filtre sur un objet Optional, comme suit:
La méthode de filtre prend un prédicat comme argument. Si une valeur est présente dans l’objet Optional et correspond au prédicat, la méthode de filtre renvoie cette valeur; sinon, il renvoie un objet Optional vide. Si vous vous souvenez que vous pouvez considérer un optional comme un flux contenant au plus un seul élément, le comportement de cette méthode devrait être assez clair. Si l’optional est déjà vide, cela n’a aucun effet; sinon, il applique le prédicat à la valeur contenue dans l’option. Si cette application renvoie true, l’optional renvoie inchangé; sinon, la valeur est filtrée, laissant un Optional vide . Vous pouvez tester votre compréhension du fonctionnement de la méthode de filtrage à l’aide du Quiz 10.2.
Quiz 10.2: Filtrage optionnel
En supposant que la classe Person de notre modèle Person/Car/Insurance possède également une méthode getAge pour accéder à l’âge de la personne, modifiez la méthode dans la liste 10.5 en utilisant la signature suivante
de sorte que le nom de la compagnie d’assurance est retourné seulement si la personne a un âge supérieur ou égal à l’argument minAge.
Réponse:
Dans la section suivante, nous étudions les fonctionnalités restantes de la classe Optional et fournissons des exemples plus pratiques illustrant diverses techniques que vous pouvez utiliser pour réimplémenter le code que vous écrivez afin de gérer les valeurs manquantes.
Le tableau 10.1 résume les méthodes de la classe Optional.
10.4. Exemples pratiques d’utilisation optionnelle
Comme vous l’avez appris, l’utilisation efficace de la nouvelle classe Optional implique une refonte complète de la façon dont vous traitez les valeurs potentiellement manquantes. Cette refonte implique non seulement le code que vous écrivez, mais peut-être même plus encore, la façon dont vous interagissez avec les API Java natives.
En effet, nous pensons que beaucoup de ces API auraient été écrites différemment si la classe Optional était disponible au moment où elles ont été développées. Pour des raisons de compatibilité descendante, les anciennes API Java ne peuvent pas être modifiées pour utiliser correctement les Optional, mais tout n’est pas perdu. Vous pouvez corriger, ou au moins contourner ce problème en ajoutant dans votre code de petites méthodes utilitaires qui vous permettent de bénéficier de la puissance des Optional. Vous verrez comment faire cela avec quelques exemples pratiques.
10.4.1. Envelopper une valeur potentiellement nulle dans une option
Une API Java existante renvoie presque toujours une valeur nulle pour signaler l’absence de la valeur requise ou que le calcul pour l’obtenir a échoué pour une raison quelconque. Par exemple, la méthode get d’une Map renvoie null comme valeur si elle ne contient aucune valeur pour la clé demandée. Mais pour les raisons que nous avons énumérées plus tôt, dans la plupart des cas comme ceci, vous préféreriez que ces méthodes puissent renvoyer une option. Vous ne pouvez pas modifier la signature de ces méthodes, mais vous pouvez facilement envelopper la valeur qu’elles renvoient avec une option. En continuant avec l’exemple Map, et en supposant que vous avez une Map<String, Object>, puis en accédant à la valeur indexée par key avec
renverra null s’il n’y a pas de valeur dans la Map associée à cette clé. Vous pouvez améliorer cela en enveloppant dans un optionnal la valeur retournée par la map. Vous pouvez le faire de deux façons: soit avec un if-then-else moche en ajoutant à la complexité du code ou en utilisant la méthode Optional.ofNullable dont nous avons parlé plus haut:
Vous pouvez utiliser cette méthode chaque fois que vous souhaitez transformer en toute sécurité une valeur potentiellement nulle en un Optional.
10.4.2. Exceptions vs. Optional
Lancer une exception est une autre alternative courante dans l’API Java pour renvoyer une valeur nulle lorsque, pour une raison quelconque, une valeur ne peut pas être fournie. Un exemple typique de ceci est la conversion de String en un int, fourni par la méthode statique Integer.parseInt (String). Dans ce cas, si la chaîne ne contient pas un nombre entier analysable, cette méthode lève une exception NumberFormatException. L’effet net est une fois de plus que le code signale un argument invalide dans le cas d’une chaîne ne représentant pas un entier, la seule différence étant que cette fois vous devez le vérifier avec un bloc try / catch au lieu d’utiliser une condition if une valeur n’est pas nulle.
Vous pouvez également modéliser la valeur invalide causée par les chaînes non convertibles avec un caractère Optional vide. Au final parseInt renverrait un Optional. Vous ne pouvez pas modifier la méthode Java d’origine, mais rien ne vous empêche d’implémenter une petite méthode d’utilitaire, de l’encapsuler et de renvoyer un Optional , comme indiqué dans la liste suivante.
Listing 10.6. Conversion d’une chaîne en un entier renvoyant un optional
Notre suggestion est de collecter plusieurs méthodes similaires à ceci dans une classe utilitaire; appelons-la OptionalUtility. De cette façon, à partir de maintenant vous serez toujours autorisé à convertir une chaîne en Optional<Integer>, en utilisant cette méthode OptionalUtility.stringToInt.
Optionals primitifs et pourquoi vous ne devriez pas les utiliser
Notez que, comme les flux, les Optionals ont aussi des contreparties primitives – OptionalInt, OptionalLong, et OptionalDouble – donc la méthode dans la liste 10.6 aurait pu renvoyer un OptionalInt au lieu de Optional<Integer>. Au chapitre 5, nous avons encouragé l’utilisation de flux primitifs, surtout lorsqu’ils pouvaient contenir un grand nombre d’éléments, pour des raisons de performance, mais comme un optional peut avoir au plus une seule valeur, cette justification ne s’applique pas ici.
Nous déconseillons l’utilisation des Optionals primaires, car elles ne disposent pas des méthodes map, flatMap et filter, qui, comme vous l’avez vu dans la section 10.2, sont les méthodes les plus utiles de la classe Optional. De plus, comme pour les flux, un optionnel ne peut pas être composé avec sa contrepartie primitive, ainsi, par exemple, si la méthode de l’image 10.6 renvoyait OptionalInt, vous ne pourriez pas la passer comme référence de méthode à la méthode flatMap d’un autre optional .
10.4.3. Mettre tous ensemble
Pour démontrer comment les méthodes de la classe Optional présentées jusqu’à présent peuvent être utilisées ensemble dans un cas d’utilisation plus convaincant, supposons que certaines propriétés soient passées en arguments à votre programme. Pour les besoins de cet exemple et pour tester le code que vous allez développer, créez quelques exemples de propriétés comme ceci:
Supposons maintenant que votre programme doive lire une valeur de ces propriétés qu’il interprétera comme une durée en secondes. Parce qu’une durée doit être un nombre positif, vous aurez besoin d’une méthode avec la signature suivante
telle que, lorsque la valeur d’une propriété donnée est une chaîne représentant un entier positif, elle renverra cet entier, mais renverra zéro dans tous les autres cas. Pour clarifier cette besoin, vous pouvez le formaliser avec quelques asserionsJUnit:
Ces assertions reflètent le besoin d’origine: la méthode readDuration renvoie 5 pour la propriété « a » car la valeur de cette propriété est une chaîne convertible en un nombre positif; il retourne 0 pour « b » parce que ce n’est pas un nombre, retourne 0 pour « c » parce que c’est un nombre mais il est négatif, et retourne 0 pour « d » parce qu’une propriété avec ce nom n’existe pas. Essayons d’implémenter la méthode répondant à ce besoin dans un style impératif, comme indiqué dans la liste suivante.
Listing 10.7. Lecture de la durée d’une propriété impérativement
Comme vous pouvez vous y attendre, l’implémentation qui en résulte est assez compliquée et pas très lisible, présentant plusieurs conditions imbriquées codées à la fois comme des instructions et comme un bloc try/catch. Prenez quelques minutes pour comprendre comment vous pouvez obtenir le même résultat en utilisant ce que vous avez appris dans ce chapitre via le questionnaire 10.3.
En utilisant les fonctionnalités de la classe Optional et la méthode utilitaire du schéma 10.6, essayez de réimplémenter la méthode impérative du schéma 10.7 avec une seule instruction fluide.
Réponse:
Étant donné que la valeur renvoyée par la méthode Properties.getProperty (String) est null lorsque la propriété requise n’existe pas, il est pratique de transformer cette valeur en Optional avec la méthode ofNullable. Vous pouvez ensuite convertir l’Optional<String> en un Optional<Integer>, en passant à sa méthode flatMap une référence à la méthode OptionalUtility.stringToInt développée dans le schéma 10.6. Enfin, vous pouvez facilement filtrer le nombre négatif. De cette façon, si l’une de ces opérations renverra un Optional vide, la méthode retournera le 0 qui a été transmis comme valeur par défaut à la méthode orElse; sinon, il renverra l’entier positif contenu dans l’Optional. Ceci est alors simplement implémenté comme suit:
Notez le style commun dans l’utilisation des optional et des flux; les deux sont des réminiscences d’une requête de base de données où plusieurs opérations sont chaînées ensemble.
10.5. Résumé
Dans ce chapitre, vous avez appris ce qui suit:
- Les références nulles ont été historiquement introduites dans les langages de programmation pour signaler généralement l’absence de valeur.
- Java 8 introduit la classe java.util.Optional<T> pour modéliser la présence ou l’absence d’une valeur.
Vous pouvez créer des objets facultatifs avec les méthodes statiques Optional.empty, Optional.of et Optional.ofNullable. - La classe Optional prend en charge de nombreuses méthodes telles que map, flatMap et filter, qui sont conceptuellement similaires aux méthodes d’un flux.
- L’utilisation de Optional vous oblige à faire face au risque potentiel d’absence de valeur; par conséquent, vous protégez votre code contre les nullPointeurException inattendues.
- L’utilisation de Optional peut vous aider à concevoir de meilleures API dans lesquelles, en lisant simplement la signature d’une méthode, les utilisateurs peuvent dire s’il faut s’attendre à une valeur qui peut être nulle ou pas.