Vous allez finir par les aimer les `Optionals` ?

2025, Jan 01    
NOTE: Ceci est une retranscription par l'excellent Emmanuel Plumas de la conférence que j'ai donné TouraineTech 2023 , un très grand merci à lui ❤️.

D'après la documentation officielle d' Oracle, un optional est un conteneur d’objet qui peut (ou pas) être null.

Une fois qu'on a dit ça, on n'est pas bien avancé !

Si les optionals existent c'est avant tout pour donner une vision à un tiers, pour lui indiquer une intention, certainement pas pour éviter des NullPointerException.

Pinky and the brain vont nous aider à prendre tout ça en main.

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

Coucou ! 👋

/onepoint-tech/static/assets/images/optionals/brain.png The brain

Oui voilà, c'est ça, bonjour.

Quand les utiliser ?

On peut par exemple les utiliser quand on représente le monde extérieur. Il ne nous est en effet pas possible de le contrôler, en tout cas, nous n'en avons pas encore trouver le moyen.

Ici par exemple pour représenter une configuration externe :

public class LeMondeExterieurConfig {
	private Optional<String> login;
	private Optional<String> password;
	private Optional<Boolean> skiplogin;
}

Cela veut dire que je sais que ce qui arrive de l’extérieur peut être null. Remarquez que ces propriétés sont private, ce qui signifie qu'il est fort probable que je ne les laisserai pas sortir de ma classe en l'état.

On verra plus tard ce que pinky faif de ces données.

On peut également les utiliser quand on accède soi-même au monde extérieur, par exemple dans un repository.

public interface LeMondeExterieurAcces {
	Optional<Person> findById(UUID id);
	List<Person> findByName(String name);
	Person findByNir(String nir) throws NotFoundException;
}

Ces trois signatures indiquent trois intentions différentes :

  1. findById peut renvoyer la valeur null et selon les cas les traitements pourraient être différents. Si je veux en effet accéder à l'utilisateur, alors il y a un problème. Mais si je veux juste valider qu'il n'existe pas avant de l'insérer, alors la nullité n'est pas un problème.

  2. findbyName renvoie un type list, aucun bonne raison de renvoyer null, un liste vide fera largement l'affaire.

  3. findByNir renvoie directement un type Person, ici on indique clairement que le nullité n'est pas possible, ça sera une donnée ou une exception.

Qu’est-ce que j'en fais et comment j’utilise les Optionals

Nous allons illustrer plusieurs cas de traitement d'optional au travers de l'exemple d'une liste de course pour notre prochaine raclette.

/onepoint-tech/static/assets/images/optionals/brain.png The brain

C'est gentil de me laisser enfin la parole chez Jérôme.

Puisque qu'on en est là, Pinky, merci d'aller faire les courses pour la raclette. Voilà la liste:

Recette de raclette

Des patates Bintjes, sinon des Amandines: orElse()

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

Ahhhh ok j'ai tout compris, du coup c'est comme ça que je dois faire.

Patate patate;
Optional<Patate> maybePatate = getBintje()
if (maybePatate.isPresent()){
	patate = maybePatate.get();
} else {
	patate = amandine
}

/onepoint-tech/static/assets/images/optionals/brain.png The brain

Non Pinky! Comme d'habitude tu fais tout à l'envers.C'est comme ça cher Pinky que tu devrais faire. Arrête tes bêtises maintenant.

private Optional<Patate> getBintje() { ...}
Patate patate = getBintje().orElse(amandine);

Dans le cas de la représentation de la configuration externe, il est très probable que cette méthode orElse() soit celle que nous aurions utilisée pour assigner des valeurs par défaut.

Si le boucher du centre a de la charcuterie prends-en, sinon va chez le boucher beaucoup plus loin : orElseGet()

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

Cette fois-ci, je fais tout bien regarde.

private Optional<Charcuterie> getCharcuterieDuCentre () {....}
private Charcuterie getCharcuteriePlusloin() {....}

Charcuterie = getCharcuterieDuCentre().orElse(getCharcuteriePlusLoin());

/onepoint-tech/static/assets/images/optionals/brain.png The brain

Cela peut marcher avec un appel de méthode mais Java est ainsi fait que les paramètres d'une méthode sont évalués avant d'invoquer les méthodes !

On invoquera donc getCharcuteriePlusLoin() coûteuse en mémoire/en temps/ce qu'on veut, alors qu'on ne sait même pas si on en a besoin.

La méthode orElseGet() qui prend en paramètre un Supplier nous permet de n'invoquer la méthode coûteuse qu'une fois qu'on est certain que c'est nécessaire.

private Optional<Charcuterie> getCharcuterieDuCentre () {....}
private Charcuterie getCharcuteriePlusloin() {....}
Charcuterie = getCharcuterieDuCentre()
		.orElseGet(() -> getCharcuteriePlusLoin());

Le supplier ne sera exécuté que s’il y en a besoin !

Fromage à raclette (ou panic) : orElseThrow*

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

Fromage morbier = maybeFromage.orElseThrow(() -> new ThreadDeath());

/onepoint-tech/static/assets/images/optionals/brain.png The brain

Ahh je suis fier de toi. Tu remarques que la méthode orElseThrow() prend également un supplier.

On aurait pu faire un orElseThrow() qui prend directement en paramètre une instance d'exception, mais leur instanciation étant coûteuse (notamment à cause du mécanisme de création de stack trace), les développeurs de l'API Java, ont là aussi choisi le pattern du Supplier pour retarder son instanciation.

D'autres utilisations avancées

Désolé, la digestion de la raclette a été un peu difficile, et les exemples suivants ne viennent pas de ma liste de course.

Liste de tâche à faire

Si on trouve le prix du cadeau, on donne le prix, sinon on donne 20€

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

Alors là je suis désolé, je ne vois pas bien comment faire ça.

/onepoint-tech/static/assets/images/optionals/brain.png The brain

Ici, en réalité nous ne sommes pas intéressés directement par le cadeaux mais uniquement par son prix, on ne veut pas traiter un Optional<Cadeau>, on aimerait un Òptional<Long>...

On peut utiliser la méthode map() afin d'effectuer cette transformation et ensuite lui appliquer un orElse().

Long participation = cadeau
	.map(Cadeau::getPrix)
	.orElse(20L);

Si le caviste a du Touraine, on en prend

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

caviste.getTouraine()
   .map(...);

Heuu non, il va encore falloir m'aider

/onepoint-tech/static/assets/images/optionals/brain.png The brain

La méthode ifPresent() est là pour toi !

caviste.getTouraine()
  .ifPresent(bouteille -> onEnPrend(bouteille));

Elle prend en paramètre un Consumer !

La méthode onEnPrend() n’a plus à se poser la question de la nullité de bouteille : on fait un appel conditionnel à la méthode !

Si le caviste a du Touraine ET qu’il n’est pas trop cher, on en prend

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

caviste.getTouraine()
  .ifPresent(bouteille -> {
    if (pasTropCher(bouteille))
        onEnPrend(bouteille)
  });

Là, j'ai bon, non ?

/onepoint-tech/static/assets/images/optionals/brain.png The brain

Tu vas trop vite, tu ne lis pas la documentation. Encore une fois, une méthode est là pour toi, la méthode filter.

caviste.getTouraine()
	.filter(bouteille -> pasTropCher(bouteille)
	.ifPresent(bouteille -> onEnPrend(bouteille));

Elle prend un Predicate.

Les plus attentif d'entre vous auront remarqué que l'API des Optional rappelle beaucoup celle des Stream

Si le primeur est ouvert ET qu’il a de la mangue, on prend, sinon, on prend de l’ananas

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

Gif d'un oiseau représentant l'incapcité à faire.

/onepoint-tech/static/assets/images/optionals/brain.png The brain

Rhââââââ

maybePrimeur //Optional<Primeur>
	.map(primeur -> primeur.getMangue()); //Optional<Optional<Fruit>>

maybePrimeur //Optional<Primeur>
	.flatmap(primeur -> primeur.getMangue()) //Optional<Fruit>
	.orElse(new Ananas());

Comme sur un stream, ces opérations ne sont pas terminales, mais seront executées au moment où on fait un get() ou un orElse(). En fait, on programme un pipeline de traitement.

Si je trouve les clefs dans mon sac je les utilise, sinon je passe par la fenêtre

/onepoint-tech/static/assets/images/optionals/pinky.png Pinky

Gif de pinky.

Tu vas encore essayer de conquérire le monde ?

/onepoint-tech/static/assets/images/optionals/brain.png The brain

Tais-toi, sot.

maybeClef
  .ifPresentOrElse(
     clef -> utilise(cle),
	() -> passeParLaFenetre());

Malheureusement la méthode ifAbsent() n’existe pas sur les Optionals, On est obligé de faire du isEmpty()ce qui sera toujours mieux que !isPresent().

Ce qu'on ne veut plus jamais voir...

if(optional.isPresent()) {
    var value = optional.get();
}
String code = Optional.ofNullable(app.getCodeImputationDefaut())
	.orElse("");

Non et non, c'est au service de fournir la valeur par défaut...