W3docs

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 empty

La 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 isPresent

Vous 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 empty

opt.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 exception
  • orElse(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ève NoSuchElementException si absent. La forme sans argument de Java 10+ est l'équivalent moderne de opt.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 orElseor 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.

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 pas Serializable, cela coûte une allocation par champ, et un champ null est 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 être null, avec un getter qui retourne Optional.
  • Un paramètre de méthode. N'acceptez pas Optional<String> comme argument. Surchargez la méthode, ou acceptez String et documentez que null signifie 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 être null et un emballage supplémentaire. Utilisez List<T> et filtrez les nulls à la frontière, ou utilisez flatMap(Optional::stream) pour supprimer les absents dans un pipeline.
  • Un moyen d'éviter tous les null. Java a encore null dans chaque type de référence ; Optional est 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.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Les trois constructeurs of, empty, ofNullable correspondent à 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.
  • orElse a évalué son argument à chaque fois, même lorsque l'optional était présent. Le fournisseur de orElseGet n'a été exécuté que lorsque nécessaire. Utilisez orElse pour les littéraux bon marché et orElseGet pour tout ce qui alloue, interroge ou lève des exceptions.
  • map et flatMap ont rendu toute la chaîne userById(...).flatMap(User::address).map(Address::city) lisible comme un seul pipeline — pas de vérifications null, pas de ifs imbriqués, et toute étape vide court-circuite vers Optional.empty() à la fin.
  • flatMap(Optional::stream) a transformé un Stream<Optional<User>> en un Stream<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.
  • OptionalInt est ce que les terminaux de streams primitifs comme IntStream.findFirst retournent. 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 champ Optional<Address> — acceptable car l'exemple voulait démontrer l'API, mais dans du code en production, le champ serait une Address pouvant être null avec un getter Optional<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.

Pratique

Pratique
Vous avez `Optional<String> opt` et avez besoin d'une valeur par défaut quand elle est vide, où la valeur par défaut est un appel coûteux à `loadDefaultFromDb()`. Lequel est correct *et* évite d'exécuter l'appel coûteux quand `opt` est présent ?
Vous avez `Optional<String> opt` et avez besoin d'une valeur par défaut quand elle est vide, où la valeur par défaut est un appel coûteux à `loadDefaultFromDb()`. Lequel est correct *et* évite d'exécuter l'appel coûteux quand `opt` est présent ?
Was this page helpful?