Java Optional
Exprimez l'absence possible d'une valeur en Java avec Optional et évitez NullPointerException par conception.
Optional<T> est un conteneur qui détient soit une valeur de type T soit rien — et vous indique lequel, au niveau du type, afin que le compilateur puisse vous forcer à gérer le cas absent. Il a été ajouté dans Java 8 aux côtés des streams, et les deux sont conçus pour fonctionner ensemble : findFirst, findAny, min, max, reduce retournent tous Optional<T> précisément parce que la réponse pourrait ne pas exister, et l'API vous offre des moyens fluides de continuer à calculer sans jamais écrire if (x != null).
Optional n'est pas un remplacement universel de null, et le JDK a des opinions bien arrêtées sur où il est approprié. Ce chapitre parcourt l'API de bout en bout, puis les trois situations où Optional est le mauvais choix.
Construire un Optional
Trois constructeurs, chacun avec une signification précise :
Optional<String> a = Optional.of("hello"); // present; null arg throws NPE
Optional<String> b = Optional.empty(); // absent
Optional<String> c = Optional.ofNullable(maybeNull); // present if non-null, else emptyLa distinction est importante. Optional.of(x) est l'assertion « cette valeur est définitivement présente » — si vous passez null, cela lève immédiatement une NullPointerException, ce qui est souhaitable (un bug détecté à la source, pas trois niveaux en aval). Optional.ofNullable(x) est l'adaptateur que vous enveloppez autour d'une API héritée qui retourne null pour signifier « absent ».
Vous ne construisez presque jamais un Optional manuellement à l'intérieur d'un pipeline de stream — les terminaux comme findFirst et Collectors.maxBy les produisent pour vous.
Vérifier si une valeur est présente
Les deux méthodes de requête :
Optional<String> opt = lookup(id);
boolean has = opt.isPresent(); // true if a value is held
boolean none = opt.isEmpty(); // Java 11+ -- the opposite of isPresentVous verrez ces méthodes dans du code en production, mais elles sont généralement un signe de code mal structuré : la plupart des codes qui appellent isPresent puis get se liraient mieux avec l'une des méthodes opérer-dessus ci-dessous. Les méthodes de requête sont pour le code de frontière où vous avez vraiment besoin d'un boolean — une clause de garde, une décision de route, une branche de journalisation d'avertissement.
Lire la valeur en toute sécurité
La mauvaise façon :
String name = opt.get(); // throws NoSuchElementException if emptyopt.get() est la lecture non vérifiée. C'est la façon de transformer un Optional en une valeur et une exception à l'exécution, exactement ce que le type était censé prévenir. Utilisez-la uniquement après avoir prouvé que l'optional est présent (ou après findFirst().orElseThrow() depuis un pipeline où vide serait un bug de programmeur, pas un cas attendu).
Les bonnes façons, par ordre de préférence :
String name1 = opt.orElse("anonymous"); // default value
String name2 = opt.orElseGet(() -> expensiveDefault()); // lazy default
String name3 = opt.orElseThrow(); // NoSuchElementException
String name4 = opt.orElseThrow(() -> new MyDomainError(id)); // custom exceptionorElse(value)— fournit une valeur par défaut. La valeur est toujours évaluée, même lorsque l'optional est présent, donc ne passez pas d'expression coûteuse.orElseGet(supplier)— fournit une valeur par défaut paresseusement. Le fournisseur ne s'exécute que lorsque l'optional est vide. Utilisez ceci pour tout défaut qui coûte plus qu'un littéral.orElseThrow()— lèveNoSuchElementExceptionsi absent. La forme sans argument de Java 10+ est l'équivalent moderne deopt.get()quand « ceci devrait absolument être présent » est la seule interprétation sensible au site d'appel.orElseThrow(supplier)— lève une exception spécifique au domaine. La façon standard de traduire « absent » en « 404 non trouvé ».
Transformer la valeur — map
Si l'optional est présent, appliquer une fonction ; sinon rester vide :
Optional<String> upper = opt.map(String::toUpperCase);
Optional<Integer> len = opt.map(String::length);La signature est Optional<T>.map(Function<T, R>) -> Optional<R>. La fonction ne s'exécute que lorsqu'une valeur est présente — pas de vérification null, pas de if, et pas de else. C'est l'opération qui rend Optional rentable en termes de caractères : la plupart des chaînes « si non-null, faire ceci ; si non-null, puis faire cela » se réduisent à .map(...).map(...).map(...).
Il y a un cas particulier que le JDK gère silencieusement : si votre fonction map retourne null (parce qu'elle enveloppe une API héritée qui retourne null pour « pas de résultat »), l'Optional résultant est empty() — pas Optional.of(null).
Composer des optionals — flatMap
Lorsque la fonction de mapping elle-même retourne un Optional, map produirait Optional<Optional<T>>. flatMap l'aplatit :
record User(String id, Optional<Address> address) {}
record Address(String city) {}
Optional<String> city = userById(id)
.flatMap(User::address) // Optional<Address>
.map(Address::city); // Optional<String>flatMap est l'opération qui vous permet d'enchaîner plusieurs recherches, dont chacune peut échouer, en un seul pipeline. Les deux cas d'échec se réduisent à Optional.empty() à la fin, et le consommateur les gère une seule fois avec orElse / orElseThrow.
Filtrer — filter
Teste la valeur contre un Predicate<T> ; retourne le même optional s'il passe, empty() s'il échoue :
Optional<String> nonBlank = opt.filter(s -> !s.isBlank());
Optional<Integer> positive = numberOpt.filter(n -> n > 0);Agit comme un garde à l'intérieur du pipeline optional. Utile quand la question est « j'ai une valeur, mais est-ce la bonne valeur pour continuer ? »
Effets de bord — ifPresent, ifPresentOrElse
Exécuter du code uniquement lorsque la valeur est présente :
opt.ifPresent(name -> log.info("hello, {}", name));Ou exécuter une branche quand présent et une autre quand vide (Java 9+) :
opt.ifPresentOrElse(
name -> log.info("hello, {}", name),
() -> log.warn("no name on the request"));Ce sont les bonnes façons d'exprimer « faire quelque chose en passant ». Elles remplacent entièrement le pattern if (opt.isPresent()) { use(opt.get()); }.
Connexion aux streams — Optional.stream()
(Java 9+) Transforme un Optional<T> en un Stream<T> de zéro ou un élément :
Stream<String> s = opt.stream();Utile à l'intérieur de flatMap sur un Stream<Optional<T>> :
List<String> presentCities = userIds.stream()
.map(this::userById) // Stream<Optional<User>>
.flatMap(Optional::stream) // Stream<User> -- empties drop, presents pass through
.map(User::city)
.toList();Cela remplace filter(Optional::isPresent).map(Optional::get) par un seul flatMap(Optional::stream). Même résultat, pipeline plus propre.
or — revenir à un autre Optional
(Java 9+) Si vide, utiliser un fournisseur d'un autre Optional :
Optional<User> u = primaryLookup(id)
.or(() -> fallbackLookup(id))
.or(() -> Optional.of(User.anonymous()));Se lit comme « essayer le principal ; si absent, essayer le secours ; si absent, utiliser anonyme ». Les trois sont Optional<User> ; la chaîne retourne le premier non-vide. Différent de orElse — or garde le résultat enveloppé ; orElse le désenveloppe avec une valeur par défaut de type T simple.
Spécialisations primitives
Il existe OptionalInt, OptionalLong, OptionalDouble pour les résultats primitifs — ce que IntStream.max() retourne, par exemple :
OptionalInt max = nums.stream().mapToInt(Integer::intValue).max();
int hi = max.orElse(0);Ils ont une API plus petite — pas de map/flatMap/filter — car ils se situent à la frontière du monde primitif. Utilisez-les pour lire les résultats de streams primitifs ; convertissez en Optional<Integer> si vous avez besoin de l'API complète.
Où Optional ne convient pas
L'intention de conception du JDK est étroite : Optional est un type de retour pour les méthodes dont la réponse pourrait ne pas exister. Ce n'est pas :
- Un type de champ. N'écrivez pas
private Optional<String> middleName;. Ce n'est pasSerializable, cela coûte une allocation par champ, et un champnullest plus court et plus clair pour « cette entité n'a pas de deuxième prénom ». Le bon choix est un champ non-Optional qui peut êtrenull, avec un getter qui retourneOptional. - Un paramètre de méthode. N'acceptez pas
Optional<String>comme argument. Surchargez la méthode, ou acceptezStringet documentez quenullsignifie absent. Les paramètres Optional obligent l'appelant à envelopper, ce qui est du bruit. - Un élément de collection.
List<Optional<T>>est presque toujours une liste avec des éléments pouvant êtrenullet un emballage supplémentaire. UtilisezList<T>et filtrez les nulls à la frontière, ou utilisezflatMap(Optional::stream)pour supprimer les absents dans un pipeline. - Un moyen d'éviter tous les
null. Java a encorenulldans chaque type de référence ;Optionalest pour la forme de retour du code qui produit des valeurs qui pourraient ne pas exister. Les types de référence simples conviennent pour tout le reste.
La règle courte : un Optional circulant hors d'une méthode est une bonne conception ; un Optional circulant dans est presque toujours wrong.
Un exemple concret : toutes les méthodes, plus les règles pratiques en code
Le programme ci-dessous construit un petit graphe utilisateur/adresse, parcourt chaque méthode sur Optional à son encontre, démontre le timing d'évaluation de orElse vs. orElseGet, le pont Optional.stream(), et la chaîne or.
Ce qu'il faut retenir de l'exécution :
- Les trois constructeurs
of,empty,ofNullablecorrespondent à trois intentions claires : définitivement présent, définitivement absent, et adaptateur-héritage, présent-si-non-null.Optional.of(null)lève une exception — et c'est l'échec souhaité, pas un bug à contourner. orElsea évalué son argument à chaque fois, même lorsque l'optional était présent. Le fournisseur deorElseGetn'a été exécuté que lorsque nécessaire. UtilisezorElsepour les littéraux bon marché etorElseGetpour tout ce qui alloue, interroge ou lève des exceptions.mapetflatMapont rendu toute la chaîneuserById(...).flatMap(User::address).map(Address::city)lisible comme un seul pipeline — pas de vérificationsnull, pas de ifs imbriqués, et toute étape vide court-circuite versOptional.empty()à la fin.flatMap(Optional::stream)a transformé unStream<Optional<User>>en unStream<User>avec tous les absents supprimés en une seule opération. C'est la façon propre de convertir une liste de recherches « pouvant échouer » en un stream de succès.OptionalIntest ce que les terminaux de streams primitifs commeIntStream.findFirstretournent. Il a sa propre petite API (getAsInt,orElse,ifPresent) et existe pour que les pipelines primitifs n'aient jamais à encadrer.- La règle des « mauvais endroits » est apparue implicitement :
User.addressétait un champOptional<Address>— acceptable car l'exemple voulait démontrer l'API, mais dans du code en production, le champ serait uneAddresspouvant êtrenullavec un getterOptional<Address> address()effectuant l'enveloppement.
Et ensuite
La partie 12 a couvert le vocabulaire fonctionnel de bout en bout : interfaces fonctionnelles, lambdas, références de méthodes, les intégrées, le pipeline de stream, chaque source, chaque intermédiaire, chaque terminal, les collecteurs, l'exécution parallèle, et enfin Optional comme expression au niveau du type de l'absence. Le prochain chapitre, Java Predicate Interface, fait un zoom arrière sur une seule interface fonctionnelle — Predicate<T> — et l'algèbre de combinateurs (and, or, negate, isEqual, not) qui vous permet d'assembler des prédicats sans jamais écrire la logique booléenne à la main. À partir de là, la partie continue avec Function, Consumer/Supplier, et la famille des opérateurs binaires — une interface par chapitre, chacune avec la même structure d'exemple travaillé que vous avez vue ici.