Optional
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 êtrenull,ofNullable()quand elle vient d'un code qui peut renvoyernull.
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 :
Optionaln'est pasSerializable, 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
nullau lieu d'un Optional : une méthode qui renvoieOptionalne doit jamais renvoyernull. - ❌
get()sans vérification : utiliseorElseThrow()avec une exception explicite. - ❌
Optional<Integer>pour des primitifs : utiliseOptionalInt,OptionalLongouOptionalDouble, pour éviter le boxing.
En résumé
Optionalen type de retour quand un résultat peut manquer.- Jamais de
nullà la place d'un Optional. - Pas de
isPresent()+get(): utilisemap,filter,orElse,orElseGetetorElseThrow. orElseGet()dès que la valeur par défaut coûte quelque chose.- Pas d'Optional en attribut, en paramètre ni autour d'une collection.