Dernière modification : 23/08/2022

 Utilisation des Optional - Java

 

Dans ce chapitre, nous allons aborder l'utilisation des Optional en java, qui ont été introduits dans la version 8.

Ils sont apparus pour pallier aux nombreux problèmes liées au mot-clé null (problèmes de lisibilité et de maintenabilité). Ils permettent de vérifier facilement la présence d'un élément et de réaliser des traitements sur celui-ci.

1. Création d'un Optional

1.1 Optional Vide

Afin de créer un Optional vide, utilisez la méthode Optional.empty.

// Création d'un Optional vide
Optional myOptional = Optional.empty();

Remarque : Ne jamais créer un Optional à null, il perdrait tout son intérêt.

1.2 Optional d'un élément non null

Afin de créer un Optional d'un élément non null, utilisez la méthode Optional.of.

String element = "Alex";
Optional myOptional = Optional.of(element);

Remarque : Si la variable élément est à null, alors un NPE est levé.

1.3 Optional d'un élément éventuellement null

Afin de créer un Optional d'un élément possiblement null, utilisez la méthode Optional.ofNullable.

String element = "Alex";
Optional myOptional = Optional.ofNullable(element);
Optional myOptional2 = Optional.ofNullable(null);

Remarque : Cette méthode est à préférer car elle null-safe.

 

2. Vérifier la présence d'un élément

Après avoir créé un Optional, nous pouvons vérifier s'il est vide ou non (valeur contenue à null) via les méthodes isEmpty (à partir de java 11) et isPresent.

// élément non null
Optional myOptional = Optional.ofNullable("Alex");
System.out.println(myOptional.isEmpty()); // false
System.out.println(myOptional.isPresent()); // true

// élément null
Optional myOptional2 = Optional.ofNullable(null);
System.out.println(myOptional2.isEmpty()); // true
System.out.println(myOptional2.isPresent()); // false

Remarque : Si aucun élément n'a été fourni (Optional.empty), les deux méthodes renverront les mêmes résultats que s'il était initialisé avec null.

 

3. Récupération de l'élément

3.1 Récupération basique

La méthode la plus rapide (mais à éviter) est d'utiliser la méthode Optional.get. Cette méthode retourne l'élément désiré, mais lève une exception du type NoSuchElementException s'il est null. Exemple :

// élément non null
Optional myOptional = Optional.ofNullable("Alex");
System.out.println(myOptional.get()); // Alex

// élément null
Optional myOptional2 = Optional.ofNullable(null);
System.out.println(myOptional2.get());
// Exception in thread "main" java.util.NoSuchElementException: No value present

3.2 Récupération avec une valeur par défaut

Il est possible de renvoyer une valeur par défaut dans le cas ou l'Optional serait vide via la méthode Optional.orElse.

// élément non null
Optional myOptional = Optional.ofNullable("Alex");
System.out.println(myOptional.orElse("Claire")); // Alex

// élément null
Optional myOptional2 = Optional.ofNullable(null);
System.out.println(myOptional2.orElse("Claire")); // Claire

Remarque : Il est possible de mettre null en valeur par défaut.

3.3 Récupération avec une valeur par défaut issue d'un Supplier

La méthode Optional.orElseGet permet de renvoyer une valeur par défaut dans le cas ou l'Optional serait vide. Elle prend en entrée un Supplier (ici lambda).

// élément non null
Optional myOptional = Optional.ofNullable("Alex");
System.out.println(myOptional.orElseGet(() -> "Claire")); // Alex

// élément null
Optional myOptional2 = Optional.ofNullable(null);
System.out.println(myOptional2.orElseGet(() -> "Claire")); // Claire

Remarque : Cette méthode permet d'optimiser le programme. En effet, si la méthode de récupération d'un élément par défaut prend un temps conséquent, avec la méthode OrElse ce temps sera obligatoirement consommé, Optional vide ou non, contrairement à la méthode orElseGet. En effet, le supplier ne sera exécuté que si l'élément est présent.

3.4 Lever une exception si absence de valeur

La méthode Optional.orElseThrow permet de lever une exception dans le cas d'une absence de valeur dans l'Optional :

String firstName = Optional.ofNullable(element).orElseThrow(); // NoSuchElementException;

String firstName2 = Optional.ofNullable(element)
                    .orElseThrow(() -> new Exception("L'element ne doit pas etre vide")); // Exception;

 

4. Les filtres

De la même manière que les Stream, les Optional permettent de filtrer le contenu via un prédicat. Si le contenu ne correspond pas, alors il ne sera pas conservé dans la suite des opérations de l'Optional.

Si l'Optional (ou le résultat de l'opération précédente (filtre, map) le rend) vide, le filtre ne sera pas appelé. Donc, il n'est pas possible d'obtenir en entrée du filtrage de valeur null. Les filtres sont effectués via la méthode Optional.filter :

Optional myOptional = Optional.ofNullable(30);
System.out.println(myOptional.filter(age -> age > 20).isPresent()); // true
System.out.println(myOptional.filter(age -> age > 40).filter(age -> age < 100).isPresent()); // false
System.out.println(myOptional.filter(age -> age < 100).isPresent()); // true

Remarque: Les filtres peuvent s'enchaîner et ne modifient pas l'Optional d'origine.

 

5. Le mapping

5.1 La méthode map

De la même manière que les Stream, les Optional permettent de mapper le contenu d'un type dans un autre (ou de transformer l'élément courant) via une Function (ici un lambda).

Si l'Optional (ou le résultat de l'opération précédente (filtre, map) le rend) vide, le mapping ne sera pas appelé. Donc, il n'est pas possible d'obtenir en entrée du mapping de valeur null. Les mapping sont effectués via la méthode Optional.map. Pour cet exemple, nous avons crée une classe Person contenant un champ firstName.

class Person {
	private String firstName;

	public Person(String firstName) {
		super();
		this.firstName = firstName;
	}

	public String getFirstName() {
		return firstName;
	}
}

Exemple d'utilisation :

Optional myOptional = Optional.ofNullable("Alex");
System.out.println(myOptional.map(String::toUpperCase).orElse("Claire"));

Person person = new Person("Angel");
String firstName = Optional.ofNullable(person).map(Person::getFirstName).orElse("Claire");

Remarque : Il est possible de combiner des maps et des filtres.

 

5.2 Modification d'un Optional avec flatMap

À la différence d'un mapping classique, la méthode Optional.flatMap récupère la valeur de l'objet et permet de renvoyer une autre instance d'Optional (vide, autre type etc).

private Optional toLowerCase(String input) {
    return Optional.ofNullable(input.toLowerCase()); // Création d'un nouveau Optional
}

Optional myNullable = Optional.ofNullable("Alex");
System.out.println(myNullable.flatMap(this::toLowerCase)); // Optional[alex]

 

6. Action conditionnelle

Il est possible de réaliser des actions uniquement si l'Optional n'est pas vide via la méthode Optional.ifPresent :

// Optional non vide
Optional myNullable = Optional.ofNullable("Angel");
myNullable.ifPresent(x -> System.out.println(x)); // Angel
myNullable.ifPresent(System.out::println); // Angel

// Optional vide
Optional myNullable2 = Optional.empty();
myNullable2.ifPresent(x -> System.out.println(x)); // lambda non exécutée

 

Introduit avec Java 9, la méthode Optional.ifPresentOrElse permet de définir une action à réaliser si l'Optional est vide et s'il ne l'est pas :

// Optional non vide
Optional myNullable = Optional.ofNullable("Claire");
myNullable.ifPresentOrElse(System.out::println, () -> System.out.println("Optional vide"));
// Claire

// Optional vide
Optional myNullable2 = Optional.empty();
myNullable2.ifPresentOrElse(System.out::println, () -> System.out.println("Optional vide"));
// Optional vide

 

7. Optional alternatif

Introduit dans Java 9, la méthode Optional.or permet de définir un nouvel Optional si celui d'origine est vide. Dans le cas contraire, celui d'origine est conservé :

Optional myNullable = Optional.empty();
System.out.println(myNullable.or(() -> Optional.ofNullable("Claire")));
// Optional[Claire]

// Avec Java 8, les alternatives suivantes sont possibles :
// Arrays.asList("Claire", "Alexandre") : Alternative a Stream.of
// .stream() : Alternative a Stream.of
Stream.of("Claire", "Alexandre")
        .filter(Objects::nonNull)
        .findFirst();
// Optional[Claire]

// Ou encore avec un Supplier
Stream.>of(() -> "Claire", () -> "Alexandre")
        .map(Supplier::get)
        .filter(Objects::nonNull)
        .findFirst();
// Optional[Claire]

 

8. Conversion d'une collection en Stream via un Optional

Plusieurs méthodes permettent de convertir une collection en Stream via un Optional.

Avec Java 8 :

List myList = Arrays.asList(1, 2, 3, 4, null);
Stream myStream = Optional.ofNullable(myList)
                           .map(Collection::stream)
                           .orElseGet(Stream::empty);

Avec Java 9 et la méthode Optional.stream :

List myList = Arrays.asList(1, 2, 3, 4, null);
Stream myStream = Optional.ofNullable(myList)
                           .stream()
                           .flatMap(List::stream);

 

9. Conclusion

Un Optional peut être utilisé plusieurs fois, mais il est nécessaire de ne pas se mélanger entre les différentes instances gardées. De plus, un Optional n'est pas sérialisable, en effet, il n'implémente pas la classe java.io.Serializable.

L'Optional est un excellent outil, avec les Stream, pour mettre en oeuvre une approche de la programmation fonctionnelle et éviter les NPE.

Pour aller plus loin, cet article donne de très bonnes pratiques sur le sujet : "26 Reasons Why Using Optional Correctly Is Not Optional"