Aller au contenu principal

Optional — En finir avec NullPointerException

La NullPointerException est l'erreur la plus fréquente en Java. Optional<T> est un conteneur introduit en Java 8 pour représenter proprement une valeur qui peut être absente.

Notions théoriques

Le problème de null

// Sans Optional : risque de NullPointerException
Personnage p = dao.trouverParNom("Inconnu");
System.out.println(p.getForce()); // NullPointerException si p == null !

Avec Optional, l'absence de valeur est explicite dans le type de retour. L'appelant est forcé de gérer le cas vide.

Créer un Optional

// Valeur garantie non-null (lève NullPointerException si null)
Optional<Personnage> opt1 = Optional.of(aragorn);

// Valeur qui peut être null (retourne Optional.empty() si null)
Optional<Personnage> opt2 = Optional.ofNullable(resultatPeutEtreNull);

// Optional explicitement vide
Optional<Personnage> opt3 = Optional.empty();

Tester la présence

Optional<Personnage> opt = dao.trouverParNom("Gandalf");

if (opt.isPresent()) {
System.out.println("Trouvé : " + opt.get().getNom());
}

// Inverse de isPresent() (Java 11+)
if (opt.isEmpty()) {
System.out.println("Personnage introuvable");
}
attention

N'utilisez get() qu'après avoir vérifié isPresent(). Appeler get() sur un Optional vide lève une NoSuchElementException. En pratique, préférez toujours orElse ou ifPresent.

Extraire la valeur en sécurité

MéthodeComportement si vide
get()Lève NoSuchElementException
orElse("défaut")Retourne la valeur de remplacement
orElseGet(() -> calcul())Exécute un Supplier pour calculer le défaut
orElseThrow(RuntimeException::new)Lève l'exception fournie
// Valeur de remplacement fixe
String nom = opt.map(Personnage::getNom).orElse("Inconnu");

// Valeur calculée paresseusement (plus efficace si le calcul est coûteux)
Personnage defaut = opt.orElseGet(() -> new Guerrier("Anonyme", 50, 10));

// Lever une exception métier
Personnage p = opt.orElseThrow(() -> new PersonnageNotFoundException("Gandalf"));

Transformer sans extraire

Optional<Personnage> opt = dao.trouverParNom("Aragorn");

// map : transformer la valeur si présente
Optional<String> nom = opt.map(Personnage::getNom);

// filter : garder uniquement si condition vraie
Optional<Personnage> guerrier = opt.filter(p -> p instanceof Guerrier);

// ifPresent : consommer si présent (sans retour)
opt.ifPresent(p -> System.out.println("Force : " + p.getForce()));

Règles d'utilisation

info

Ne jamais stocker un Optional comme attribut de classe. Optional est conçu exclusivement comme type de retour d'une méthode de recherche.

// BIEN : Optional comme type de retour
public Optional<Personnage> trouverParNom(String nom) {
for (Personnage p : liste) {
if (p.getNom().equals(nom)) {
return Optional.of(p);
}
}
return Optional.empty();
}

// MAL : Optional comme attribut (ne pas faire)
private Optional<Personnage> personnageActif; // NON

Exemple pratique

import java.util.*;

public class PersonnageDAO {

private List<Personnage> personnages = new ArrayList<>();

public PersonnageDAO() {
personnages.add(new Guerrier("Aragorn", 150, 30));
personnages.add(new Mage("Gandalf", 100, 20, 50));
personnages.add(new Guerrier("Boromir", 130, 28));
}

// Retourne Optional — force l'appelant à gérer l'absence
public Optional<Personnage> trouverParNom(String nom) {
return personnages.stream()
.filter(p -> p.getNom().equals(nom))
.findFirst();
}

// Retourne le personnage le plus fort, ou vide si liste vide
public Optional<Personnage> lePlusFort() {
return personnages.stream()
.max(Comparator.comparingInt(Personnage::getForce));
}
}

public class Main {

public static void main(String[] args) {
PersonnageDAO dao = new PersonnageDAO();

// Cas 1 : personnage trouvé
dao.trouverParNom("Aragorn")
.ifPresent(p -> System.out.println("Trouvé : " + p.getNom()));

// Cas 2 : personnage absent avec valeur par défaut
String nom = dao.trouverParNom("Sauron")
.map(Personnage::getNom)
.orElse("Personnage inconnu");
System.out.println(nom);

// Cas 3 : le plus fort
dao.lePlusFort()
.ifPresent(p -> System.out.println("Le plus fort : " + p.getNom()));
}
}

Test de mémorisation/compréhension


Que lève Optional.of(null) ?


Quelle méthode d'Optional exécute une action uniquement si la valeur est présente, sans retourner de résultat ?


Quelle est la différence entre orElse() et orElseGet() ?


Dans quel cas doit-on utiliser Optional ?


Que retourne la méthode filter() d'Optional si la condition n'est pas satisfaite ?


TP pour réfléchir et résoudre des problèmes

Vous allez refactorer la classe PersonnageDAO pour qu'elle retourne des Optional au lieu de null.

Étape 1 — Refactorer trouverParNom pour retourner Optional

Modifiez la signature et l'implémentation de trouverParNom pour retourner Optional<Personnage>.

public class PersonnageDAO {
private List<Personnage> personnages = new ArrayList<>();

// AVANT (dangereux)
public Personnage trouverParNom(String nom) {
for (Personnage p : personnages) {
if (p.getNom().equals(nom)) return p;
}
return null; // risque NullPointerException à l'appelant
}
}

Bonne pratique - findFirst() retourne déjà un Optional

Pas besoin d'envelopper le résultat dans Optional.of(). findFirst() retourne naturellement Optional<T>. Retourner directement le Stream terminal simplifie le code.

Étape 2 — Utiliser l'Optional côté appelant

Dans le main, appelez trouverParNom et gérez proprement le cas absent avec orElseThrow.


Bonne pratique - orElseThrow pour les cas obligatoires

Quand vous savez que l'absence est une erreur de logique (le personnage doit exister), préférez orElseThrow avec un message clair. C'est plus explicite qu'un test if (p == null) et garantit que le problème est signalé immédiatement.

Étape 3 — Chaîner avec map pour transformer sans extraire

Affichez uniquement le nom du personnage trouvé, ou "Inconnu" si absent, sans appeler get().


Bonne pratique - map() évite d'extraire prématurément

Enchaîner map() sur un Optional permet de rester dans le "monde Optional" sans risquer NullPointerException. N'extrayez la valeur (avec get() ou orElse()) qu'en toute fin de chaîne.

📌 Une solution