Optional<T> est un conteneur introduit en Java 8 pour gérer explicitement l’absence de valeur et éviter les redoutées NullPointerException. Au lieu de retourner null, vous retournez un Optional qui peut être vide ou contenir une valeur.
Dans cet article, vous découvrirez :
- Ce qu’est
Optionalet pourquoi l’utiliser - Comment créer et manipuler des
Optional - Les méthodes essentielles (
map,flatMap,filter,orElse,ifPresent) - Les anti-patterns à éviter
- Les bonnes pratiques et cas d’usage
- L’intégration avec Spring Data, Stream API et records
Pré-requis : Java 8 ou plus récent (certaines méthodes sont disponibles à partir de Java 9 ou 11, indiqué au cas par cas).
Le problème : NullPointerException
NullPointerException (NPE) est l’erreur la plus fréquente en Java. Elle survient quand on tente d’accéder à une méthode ou un champ sur une référence null.
Exemple classique sans Optional
public String getUserEmail(Long userId) {
User user = userRepository.findById(userId);
if (user != null) {
Address address = user.getAddress();
if (address != null) {
return address.getEmail();
}
}
return "unknown@example.com";
}
Problèmes :
- Code verbeux avec des checks
!= nullimbriqués - Facile d’oublier un check et déclencher une NPE
- Pas d’indication explicite qu’une valeur peut être absente
La solution : Optional
Optional<T> est un conteneur qui :
- Contient une valeur de type
T(Optional “présent”) - Ou ne contient rien (Optional “vide”)
import java.util.Optional;
public Optional<String> getUserEmail(Long userId) {
return userRepository.findById(userId)
.map(User::getAddress)
.map(Address::getEmail);
}
Avantages :
- Code concis et lisible
- Type-safe : le compilateur force la gestion de l’absence
- Moins de NPE en production
Créer un Optional
Optional.of(value)
Crée un Optional contenant value. Lance une NPE si value est null.
Optional<String> opt = Optional.of("Hello");
// Optional<String> opt = Optional.of(null); // NPE !
Usage : quand vous êtes certain que la valeur n’est jamais null.
Optional.ofNullable(value)
Crée un Optional contenant value, ou un Optional vide si value est null.
String name = getName(); // peut retourner null
Optional<String> opt = Optional.ofNullable(name);
Usage : quand la valeur peut être null (cas le plus courant).
Optional.empty()
Crée un Optional vide.
Optional<String> opt = Optional.empty();
System.out.println(opt.isPresent()); // false
Usage : pour signaler explicitement l’absence de valeur.
Vérifier la présence d’une valeur
isPresent() et isEmpty()
Optional<String> opt = Optional.of("Hello");
if (opt.isPresent()) {
System.out.println(opt.get()); // Hello
}
// Java 11+
if (opt.isEmpty()) {
System.out.println("Vide");
}
Anti-pattern : utiliser
isPresent()+get()revient à faire un check!= null. Préférez les méthodes fonctionnelles ci-dessous.
Extraire la valeur
get()
Retourne la valeur si présente, sinon lance NoSuchElementException.
Optional<String> opt = Optional.of("Hello");
String value = opt.get(); // Hello
Optional<String> empty = Optional.empty();
// String value = empty.get(); // NoSuchElementException !
À éviter : préférez les méthodes sûres ci-dessous.
orElse(defaultValue)
Retourne la valeur si présente, sinon defaultValue.
String name = Optional.ofNullable(getName())
.orElse("Anonyme");
Attention : defaultValue est toujours évaluée, même si l’Optional est présent.
orElseGet(Supplier)
Retourne la valeur si présente, sinon appelle le Supplier.
String name = Optional.ofNullable(getName())
.orElseGet(() -> fetchDefaultName()); // appelé seulement si absent
Performance : préférez orElseGet si le calcul de la valeur par défaut est coûteux.
orElseThrow()
Lance une exception si l’Optional est vide.
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User " + id + " not found"));
Usage : quand l’absence de valeur est une erreur métier.
Transformation avec map()
map() applique une fonction à la valeur si présente, retourne un Optional du résultat.
Optional<String> name = Optional.of("alice");
Optional<String> upper = name.map(String::toUpperCase);
// Optional["ALICE"]
Optional<Integer> length = name.map(String::length);
// Optional[5]
Chaînage :
Optional<User> user = findUser(id);
Optional<String> email = user
.map(User::getAddress)
.map(Address::getEmail)
.map(String::toLowerCase);
Si user, getAddress() ou getEmail() retourne null ou Optional vide, la chaîne retourne Optional.empty().
Aplatissement avec flatMap()
flatMap() est utilisé quand la fonction retourne déjà un Optional.
Problème avec map()
Optional<User> user = findUser(id); // retourne Optional<User>
// map retourne Optional<Optional<Address>>
Optional<Optional<Address>> address = user.map(User::getOptionalAddress);
Solution avec flatMap()
Optional<Address> address = user.flatMap(User::getOptionalAddress);
// retourne directement Optional<Address>
Exemple complet :
public Optional<String> getUserCityName(Long userId) {
return userRepository.findById(userId) // Optional<User>
.flatMap(User::getAddress) // Optional<Address>
.flatMap(Address::getCity) // Optional<City>
.map(City::getName); // Optional<String>
}
Filtrage avec filter()
filter() garde la valeur si elle satisfait le prédicat, sinon retourne Optional.empty().
Optional<String> name = Optional.of("Alice");
Optional<String> longName = name.filter(n -> n.length() > 3);
// Optional["Alice"]
Optional<String> shortName = name.filter(n -> n.length() > 10);
// Optional.empty
Cas d’usage : validation conditionnelle.
public Optional<User> getActiveUser(Long id) {
return userRepository.findById(id)
.filter(User::isActive);
}
Exécuter une action avec ifPresent()
ifPresent(Consumer) exécute le Consumer si la valeur est présente.
Optional<User> user = findUser(id);
user.ifPresent(u -> System.out.println("User: " + u.getName()));
ifPresentOrElse() (Java 9+)
user.ifPresentOrElse(
u -> System.out.println("User: " + u.getName()),
() -> System.out.println("User not found")
);
Combinaison avec or() (Java 9+)
or(Supplier<Optional>) retourne l’Optional si présent, sinon appelle le Supplier.
Optional<User> user = findUserInCache(id)
.or(() -> findUserInDatabase(id))
.or(() -> findUserInBackup(id));
Équivalent à un fallback en cascade.
Conversion en Stream (Java 9+)
stream() convertit un Optional en Stream de 0 ou 1 élément.
List<String> emails = users.stream()
.map(User::getEmail) // Stream<Optional<String>>
.flatMap(Optional::stream) // Stream<String> (filtre les empty)
.collect(Collectors.toList());
Avant Java 9, on utilisait :
.filter(Optional::isPresent)
.map(Optional::get)
Anti-patterns à éviter
Utiliser get() sans vérification
Mauvais :
String name = optional.get(); // peut lancer NoSuchElementException
Bon :
String name = optional.orElse("default");
String name = optional.orElseThrow(() -> new RuntimeException("Absent"));
isPresent() + get() (check null déguisé)
Mauvais :
if (optional.isPresent()) {
return optional.get();
}
return "default";
Bon :
return optional.orElse("default");
Optional imbriqués : Optional<Optional>
Mauvais :
Optional<User> user = findUser(id);
Optional<Optional<Address>> address = user.map(User::getOptionalAddress);
Bon :
Optional<Address> address = user.flatMap(User::getOptionalAddress);
Optional en paramètre de méthode
Mauvais :
public void setName(Optional<String> name) {
// ...
}
Bon :
// Utilisez @Nullable ou surcharge
public void setName(String name) { /* ... */ }
public void setName() { /* sans nom */ }
// Ou avec annotation
public void setName(@Nullable String name) { /* ... */ }
Optional en champ de classe
Mauvais :
public class User {
private Optional<String> middleName;
}
Bon :
public class User {
private String middleName; // peut être null
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
}
Retourner null au lieu d’Optional.empty()
Mauvais :
public Optional<User> findUser(Long id) {
if (notFound) {
return null; // DANGER !
}
return Optional.of(user);
}
Bon :
public Optional<User> findUser(Long id) {
if (notFound) {
return Optional.empty();
}
return Optional.of(user);
}
Bonnes pratiques
À faire
- Retourner
Optionaldans les méthodes publiques quand l’absence de valeur est possible et normale.
public Optional<User> findUserByEmail(String email) {
// ...
}
- Utiliser
orElseGetpour les calculs coûteux
.orElseGet(() -> database.queryDefault())
- Chaîner avec
map/flatMap/filter
return user
.flatMap(User::getAddress)
.map(Address::getCity)
.filter(city -> city.getPopulation() > 100_000)
.orElse("Unknown");
À éviter
- Retourner
Optionald’une collection ; retournez plutôt une collection vide.
// NON
public Optional<List<User>> getUsers() { ... }
// OUI
public List<User> getUsers() {
return users != null ? users : Collections.emptyList();
}
- Utiliser
Optionalpour des champs de classe
// NON
private Optional<String> middleName;
// OUI
private String middleName; // peut être null
Optional avec Spring Data
Spring Data JPA supporte Optional nativement dans les repositories.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findById(Long id);
Optional<User> findByEmail(String email);
}
Usage :
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public UserDTO getUser(Long id) {
return userRepository.findById(id)
.map(this::toDTO)
.orElseThrow(() -> new UserNotFoundException(id));
}
private UserDTO toDTO(User user) {
return new UserDTO(user.getId(), user.getName(), user.getEmail());
}
}
Optional avec Stream API
Optional s’intègre parfaitement avec les Streams.
List<User> users = Arrays.asList(user1, user2, user3);
// Extraire les emails présents
List<String> emails = users.stream()
.map(User::getEmail) // Stream<Optional<String>>
.flatMap(Optional::stream) // Java 9+
.collect(Collectors.toList());
// Trouver le premier utilisateur actif
Optional<User> firstActive = users.stream()
.filter(User::isActive)
.findFirst();
Optional avec Records (Java 16+)
Les records s’intègrent bien avec Optional pour les champs optionnels.
public record UserDTO(
Long id,
String name,
Optional<String> middleName, // anti-pattern
String email
) {}
// Préférez :
public record UserDTO(
Long id,
String name,
String middleName, // peut être null
String email
) {
// Méthode accesseur pour Optional
public Optional<String> middleName() {
return Optional.ofNullable(middleName);
}
}
Ou mieux encore, gardez le record simple :
public record UserDTO(Long id, String name, String email) {}
// Classe service gère les Optional
public Optional<UserDTO> findUser(Long id) {
return userRepository.findById(id)
.map(user -> new UserDTO(user.getId(), user.getName(), user.getEmail()));
}
Performances
Optional ajoute un léger overhead (allocation d’objet). Pour du code critique en performance :
- Évitez
Optionaldans des boucles très fréquentes - Préférez
Optionalpour les API publiques (lisibilité avant micro-optimisation) - Dans 99% des cas, l’overhead est négligeable
Exemples concrets
Configuration optionnelle
public class AppConfig {
private String host;
private Integer port;
public Optional<Integer> getPort() {
return Optional.ofNullable(port);
}
public int getPortOrDefault() {
return getPort().orElse(8080);
}
}
Parsing sécurisé
public Optional<Integer> parseInteger(String value) {
try {
return Optional.of(Integer.parseInt(value));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
// Usage
int port = parseInteger(input)
.filter(p -> p > 0 && p < 65536)
.orElse(8080);
Recherche en cascade
public User getUser(Long id) {
return cache.get(id)
.or(() -> database.find(id))
.or(() -> backup.find(id))
.orElseThrow(() -> new UserNotFoundException(id));
}
Transformation conditionnelle
public String formatName(User user) {
return Optional.ofNullable(user.getMiddleName())
.map(middle -> user.getFirstName() + " " + middle + " " + user.getLastName())
.orElse(user.getFirstName() + " " + user.getLastName());
}
Conclusion
Optional est un outil puissant pour rendre le code Java plus sûr et expressif en gérant explicitement l’absence de valeur. En suivant les bonnes pratiques, vous réduirez drastiquement les NullPointerException et améliorerez la lisibilité.
Points clés à retenir :
- Utilisez
Optionaldans les retours de méthodes quand l’absence est possible - Privilégiez
map,flatMap,filter,orElseplutôt queisPresent()+get() - N’utilisez pas
Optionalen paramètres de méthodes ou en champs de classe - Intégration native avec Spring Data et Stream API
- Performance acceptable pour la quasi-totalité des cas
Pour aller plus loin
- JDK 8 Optional Javadoc
- JEP 303: Optional improvements (Java 9)
- Oracle Tutorial: Optional
- Baeldung: Guide to Java Optional