NOTE: Ceci est une retranscription par l'excellent
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
.
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 je fais 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 :
-
findById
peut renvoyer la valeurnull
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. -
findbyName
renvoie un typelist
, aucun bonne raison de renvoyernull
, un liste vide fera largement l'affaire. -
findByNir
renvoie directement un typePerson
, 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.
Des patates Bintjes, sinon des Amandines: orElse()
private Optional<Patate> getBintje() { ...}
Patate patate = getBintje().orElse(amandine);
On n'écrit surtout pas :
Patate patate;
Optional<Patate> maybePatate = getBintje()
if (maybePatate.isPresent()){
patate = maybePatate.get();
} else {
patate = 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()
private Optional<Charcuterie> getCharcuterieDuCentre () {....}
private Charcuterie getCharcuteriePlusloin() {....}
Charcuterie = getCharcuterieDuCentre().orElse(getCharcuteriePlusLoin());
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*
Fromage morbier = maybeFromage
.orElseThrow(() -> new ThreadDeath());
On peut remarquer 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é, je n'ai pas trouvé d'exemple dans ma liste de course..
Si on trouve le prix du cadeau on donne le prix, sinon on donne 20€ : .map().orElse()
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...
On peut utiliser la méthode map()
afin d'effectuer cette transformation et ensuite lui appliquer un orElse()
.
Long participation = cadeau
.map(cadeau -> cadeau.getPrix)
.orElse(20L);
Si le caviste a du Touraine, on en prend : ifPresent()
caviste.getTouraine().ifPresent(bouteille -> onEnPrend(bouteille));
La méthode ifPresent()
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: filter().ifPresent()
caviste.getTouraine()
.filter(bouteille -> pasTropCher(bouteille)
.ifPresent(bouteille -> onEnPrend(bouteille));
La méthode filter()
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: flatmap().orElse()
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: ifPresentOrElse()
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...