Optional

Publié le · Mis à jour le

Le problème : null

En Java, une méthode qui ne trouve rien renvoie souvent null. Rien dans sa signature ne l'indique, et l'appelant l'oublie… jusqu'au NullPointerException en production.

User user = userRepository.findByEmail(email); // peut-il être null ? on ne sait pas
String ville = user.getAdresse().getVille();   // 💥 NullPointerException

Optional<T> (Java 8) est une boîte qui contient une valeur ou rien. Le type de retour dit clairement : « il se peut qu'il n'y ait pas de résultat, gère ce cas ».

Optional<User> user = userRepository.findByEmail(email);

Créer un Optional

Optional<String> plein  = Optional.of("Karl");        // valeur obligatoire (NPE si null)
Optional<String> peutEtre = Optional.ofNullable(nom);  // null → Optional vide
Optional<String> vide   = Optional.empty();            // explicitement vide

Règle simple : of() quand la valeur ne peut pas être null, ofNullable() quand elle vient d'un code qui peut renvoyer null.

Récupérer la valeur

❌ La mauvaise façon

if (user.isPresent()) {
    return user.get().getNom();
}
return "Inconnu";

C'est exactement le if (user != null) d'avant, en plus verbeux. Et get() sur un Optional vide lève une NoSuchElementException.

✅ Les bonnes façons

// Valeur par défaut
String nom = user.map(User::getNom).orElse("Inconnu");

// Valeur par défaut calculée seulement si besoin
String nom = user.map(User::getNom).orElseGet(() -> calculerNomParDefaut());

// Absence = erreur métier
User u = userRepository.findById(id)
        .orElseThrow(() -> new UserNotFoundException(id));

// Juste agir si présent
user.ifPresent(u -> mailService.envoyerBienvenue(u));

// Agir dans les deux cas (Java 9+)
user.ifPresentOrElse(
        u -> log.info("Trouvé : {}", u.getNom()),
        () -> log.warn("Aucun utilisateur pour {}", email)
);

orElse vs orElseGet : le piège

user.orElse(creerUtilisateurParDefaut());          // ⚠️ creerUtilisateurParDefaut() est TOUJOURS appelé
user.orElseGet(() -> creerUtilisateurParDefaut()); // ✅ appelé seulement si l'Optional est vide

orElse() évalue son argument avant l'appel, même si la valeur est présente. Si la valeur par défaut coûte cher (requête SQL, appel HTTP, création d'objet), utilise orElseGet().

Transformer sans if

map : transformer la valeur si elle existe

Optional<String> ville = user
        .map(User::getAdresse)
        .map(Adresse::getVille);
// Si user, l'adresse ou la ville est absente → Optional.empty(), sans NPE

flatMap : quand la méthode renvoie déjà un Optional

// User::getTelephone renvoie Optional<String>
Optional<String> tel = user.flatMap(User::getTelephone);       // ✅ Optional<String>
Optional<Optional<String>> raté = user.map(User::getTelephone); // ❌ Optional imbriqué

filter : garder la valeur seulement si une condition est vraie

Optional<User> admin = user.filter(User::isAdmin);

Tout ensemble

String email = userRepository.findById(id)
        .filter(User::isActif)
        .map(User::getEmail)
        .map(String::toLowerCase)
        .orElseThrow(() -> new IllegalStateException("Utilisateur inactif ou introuvable"));

Les ajouts de Java 9 à 11

Méthode Version Usage
or(() -> autreOptional) 9 Essayer une autre source si vide
ifPresentOrElse(…, …) 9 Agir dans les deux cas
stream() 9 Transformer en Stream de 0 ou 1 élément
orElseThrow() 10 Comme get(), mais au nom explicite
isEmpty() 11 L'inverse de isPresent()
// Chercher en cache, puis en base
Optional<Produit> p = cache.find(id).or(() -> repository.findById(id));

// Garder seulement les valeurs présentes d'une liste d'Optional
List<String> emails = users.stream()
        .map(User::getEmailSecondaire) // Optional<String>
        .flatMap(Optional::stream)
        .toList();

Avec Spring Data JPA

Les repositories Spring Data renvoient déjà des Optional :

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

Dans le service, on transforme l'absence en erreur métier, que le @ControllerAdvice traduira en 404 :

public UserDto getUser(Long id) {
    return userRepository.findById(id)
            .map(userMapper::toDto)
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
}

Ce qu'il ne faut PAS faire

Optional a été conçu pour les valeurs de retour, pas pour tout le reste.

  • ❌ En attribut d'entité ou de DTO : Optional n'est pas Serializable, et JPA ou Jackson le gèrent mal.
    private Optional<String> telephone; // ❌
    private String telephone;           // ✅ + getter qui renvoie Optional.ofNullable(telephone)
    
  • ❌ En paramètre de méthode : l'appelant doit emballer sa valeur, et peut toujours passer null à la place de l'Optional. Préfère deux méthodes, ou un paramètre nullable documenté.
  • ❌ Pour une collection : renvoie une liste vide, pas Optional<List<…>>.
  • ❌ Renvoyer null au lieu d'un Optional : une méthode qui renvoie Optional ne doit jamais renvoyer null.
  • ❌ get() sans vérification : utilise orElseThrow() avec une exception explicite.
  • ❌ Optional<Integer> pour des primitifs : utilise OptionalInt, OptionalLong ou OptionalDouble, pour éviter le boxing.

En résumé

  1. Optional en type de retour quand un résultat peut manquer.
  2. Jamais de null à la place d'un Optional.
  3. Pas de isPresent() + get() : utilise map, filter, orElse, orElseGet et orElseThrow.
  4. orElseGet() dès que la valeur par défaut coûte quelque chose.
  5. Pas d'Optional en attribut, en paramètre ni autour d'une collection.

Avis sur l'article

Pas encore de note : soyez le premier à donner votre avis.

Les avis sont publiés après validation par le modérateur.

Votre note