Comment gérer l’absence de valeur en Java avec Optional ?

Supposons que vous suiviez un cours d’introduction à Java et que vous receviez des notes de 90 et 100 aux deux premiers examens. Maintenant, on vous demande quelle est votre moyenne. Une moyenne se calcule en additionnant les scores et en divisant par le nombre de scores, donc vous avez (90+100)/2. Cela donne 190/2, donc vous répondez 95. Parfait !

Maintenant, supposons que vous suiviez votre deuxième cours sur Java, et que ce soit le premier jour de cours. Nous vous demandons quelle est votre moyenne dans ce cours qui vient de commencer. Vous n’avez pas encore passé d’examens, donc vous n’avez rien à moyenner. Il ne serait pas exact de dire que votre moyenne est zéro. Cela semble mauvais et n’est pas vrai. Il n’y a tout simplement pas de données, donc vous n’avez pas de moyenne.

Comment exprimons-nous cette réponse “nous ne savons pas” ou “non applicable” en Java ? Nous utilisons le type Optional. Un Optional est créé en utilisant une fabrique. Vous pouvez soit demander un Optional vide, soit passer une valeur que l’Optional va envelopper. Pensez à un Optional comme à une boîte qui pourrait avoir quelque chose dedans ou pourrait être vide.

Créer un Optional

Voici comment coder notre méthode de moyenne :

public static Optional<Double> moyenne(int... scores) {
    if (scores.length == 0) return Optional.empty();
    int somme = 0;
    for (int score: scores) somme += score;
    return Optional.of((double) somme / scores.length);
}

La condition de retour renvoie un Optional vide lorsque nous ne pouvons pas calculer une moyenne. Les lignes suivantes additionnent les scores. Il existe une façon de faire ce calcul en programmation fonctionnelle, mais nous y reviendrons plus tard. En fait, toute la méthode pourrait être écrite en une ligne, mais cela ne vous apprendrait pas comment fonctionne Optional ! La dernière ligne crée un Optional pour envelopper la moyenne.

L’appel de la méthode montre ce qui se trouve dans nos deux boîtes :

System.out.println(moyenne(90, 100)); // Optional[95.0]
System.out.println(moyenne()); // Optional.empty

Vous pouvez voir qu’un Optional contient une valeur et l’autre est vide. Normalement, nous voulons vérifier si une valeur est présente et/ou la récupérer. Voici une façon de faire :

Optional<Double> opt = moyenne(90, 100);
if (opt.isPresent())
    System.out.println(opt.get()); // 95.0

D’abord, nous vérifions si l’Optional contient une valeur. Ensuite, nous l’imprimons. Que se passerait-il si nous ne faisions pas la vérification et que l’Optional était vide ?

Optional<Double> opt = moyenne();
System.out.println(opt.get()); // NoSuchElementException

Nous obtiendrions une exception puisqu’il n’y a pas de valeur à l’intérieur de l’Optional.

java.util.NoSuchElementException: No value present

Lors de la création d’un Optional, il est courant de vouloir utiliser empty() lorsque la valeur est null. Vous pouvez le faire avec une instruction if ou un opérateur ternaire. Nous utilisons l’opérateur ternaire (?:) pour simplifier le code.

Optional o = (valeur == null) ? Optional.empty() : Optional.of(valeur);

Si valeur est null, o se voit assigner l’Optional vide. Sinon, nous enveloppons la valeur. Comme ce modèle est si courant, Java fournit une méthode de fabrique pour faire la même chose.

Optional o = Optional.ofNullable(valeur);

Cela couvre les méthodes statiques que vous devez connaître sur Optional. Le tableau suivant résume la plupart des méthodes d’instance sur Optional que vous devez connaître. Il y en a quelques autres qui impliquent le chaînage. Nous les couvrirons plus tard.

MéthodeQuand Optional est videQuand Optional contient une valeur
get()Lève une exceptionRenvoie la valeur
ifPresent(Consumer c)Ne fait rienAppelle Consumer avec la valeur
isPresent()Renvoie falseRenvoie true
orElse(T other)Renvoie le paramètre otherRenvoie la valeur
orElseGet(Supplier s)Renvoie le résultat de l’appel du SupplierRenvoie la valeur
orElseThrow()Lève NoSuchElementExceptionRenvoie la valeur
orElseThrow(Supplier s)Lève l’exception créée par l’appel du SupplierRenvoie la valeur

Vous avez déjà vu get() et isPresent(). Les autres méthodes vous permettent d’écrire du code qui utilise un Optional en une ligne sans avoir à utiliser l’opérateur ternaire. Cela rend le code plus facile à lire. Au lieu d’utiliser une instruction if, que nous avons utilisée pour vérifier la moyenne précédemment, nous pouvons spécifier un Consumer à exécuter lorsqu’il y a une valeur à l’intérieur de l’Optional. Quand il n’y en a pas, la méthode saute simplement l’exécution du Consumer.

Optional<Double> opt = moyenne(90, 100);
opt.ifPresent(System.out::println);

L’utilisation de ifPresent() exprime mieux notre intention. Nous voulons que quelque chose soit fait si une valeur est présente. Vous pouvez le considérer comme une instruction if sans else.

Gérer un Optional vide

Les méthodes restantes vous permettent de spécifier quoi faire si une valeur n’est pas présente. Il y a quelques choix. Les deux premiers vous permettent de spécifier une valeur de retour soit directement, soit en utilisant un Supplier.

Optional<Double> opt = moyenne();
System.out.println(opt.orElse(Double.NaN));
System.out.println(opt.orElseGet(() -> Math.random()));

Cela imprime quelque chose comme :

NaN
0.49775932295380165

La première ligne montre que vous pouvez renvoyer une valeur ou une variable spécifique. Dans notre cas, nous imprimons la valeur “pas un nombre”. La deuxième ligne montre l’utilisation d’un Supplier pour générer une valeur à l’exécution à renvoyer à la place. Je suis content que nos professeurs ne nous donnent pas une moyenne aléatoire, cependant !

Alternativement, nous pouvons faire en sorte que le code lève une exception si l’Optional est vide.

Optional<Double> opt = moyenne();
System.out.println(opt.orElseThrow());

Cela imprime quelque chose comme :

Exception in thread "main" java.util.NoSuchElementException:
No value present
at java.base/java.util.Optional.orElseThrow(Optional.java:382)

Sans spécifier un Supplier pour l’exception, Java lèvera une NoSuchElementException. Alternativement, nous pouvons faire en sorte que le code lève une exception personnalisée si l’Optional est vide. N’oubliez pas que la trace de la pile semble étrange car les lambdas sont générées plutôt que des classes nommées.

Optional<Double> opt = moyenne();
System.out.println(opt.orElseThrow(
    () -> new IllegalStateException()));

Cela imprime quelque chose comme :

Exception in thread "main" java.lang.IllegalStateException
at optionals.Methods.lambda$orElse$1(Methods.java:31)
at java.base/java.util.Optional.orElseThrow(Optional.java:408)

La ligne avec la lambda montre l’utilisation d’un Supplier pour créer une exception qui devrait être levée. Notez que nous n’écrivons pas throw new IllegalStateException(). La méthode orElseThrow() se charge de lever l’exception quand nous l’exécutons.

Les deux méthodes qui prennent un Supplier ont des noms différents. Voyez-vous pourquoi ce code ne compile pas ?

System.out.println(opt.orElseGet(
    () -> new IllegalStateException())); // NE COMPILE PAS

La variable opt est un Optional<Double>. Cela signifie que le Supplier doit renvoyer un Double. Puisque ce Supplier renvoie une exception, le type ne correspond pas.

Le dernier exemple avec Optional est vraiment facile. Qu’en pensez-vous ?

Optional<Double> opt = moyenne(90, 100);
System.out.println(opt.orElse(Double.NaN));
System.out.println(opt.orElseGet(() -> Math.random()));
System.out.println(opt.orElseThrow());

Cela imprime 95.0 trois fois. Puisque la valeur existe, il n’est pas nécessaire d’utiliser la logique “ou sinon”.

Optional est-il identique à null ?

Une alternative à Optional est de renvoyer null. Il y a quelques lacunes avec cette approche. L’une d’elles est qu’il n’y a pas de moyen clair d’exprimer que null pourrait être une valeur spéciale. En revanche, renvoyer un Optional est une déclaration claire dans l’API qu’il pourrait ne pas y avoir de valeur.

Un autre avantage d’Optional est que vous pouvez utiliser un style de programmation fonctionnelle avec ifPresent() et les autres méthodes plutôt que d’avoir besoin d’une instruction if.

Enfin, vous verrez vers la fin que vous pouvez chaîner les appels Optional.