Java8 a introduit une nouvelle classe, Optional, qui permet d’encapsuler un objet dont la valeur peut être null. L’intention des développeurs de l’API Java est d’offrir enfin à Java la possibilité d’exprimer plus clairement un type de retour de méthode. Au lieu d’utiliser une annotation Java, qui n’est pas vérifié à la compilation, Optional vous permet de booster vos méthodes et d’indiquer que votre méthode est susceptible de retourner des valeurs « vides » (et pas null). D’où Optional.
Comment s’en servir ?
Prenons le cas d’un service Conference, qui recherche la description d’une conférence pour un id donné. Soit une méthode findById qui n’a pas d’effet de bord, et qui cherche dans une base la conférence correspondant à l’id donné.
public static Conference findById(Integer id) { // DB ici Conference maybeConference = DB.findById(id); return maybeConference; }
Parfait, mais que faire si la conférence n’existe pas ? Faut-il retourner null ? Voyons comment utiliser Optional afin de modifier la signature de notre méthode.
public static Optional<Conference> findById(Integer id) { Conference maybeConference = DB.findById(id); return Optional.ofNullable(maybeConference); }
Optional.ofNullable permet de retourner soit un Optional<Conference> lorsque nous avons une valeur, sinon Optional.empty() pour signifier qu’il n’y a pas de Conference pour l’id spécifié
C’est donc d’abord un moyen d’exprimer plus clairement ce que fait votre code. Notez aussi le typage qui aide à la compilation pour débuger et s’assurer que le type est correct.
Peut-on s’en servir comme propriétés d’un bean Java ?
A priori, non d’après la javadoc, car Optional est une value-class. Optional est un indicateur de retour, un nouveau type destinée à remonter clairement au développeur le type de retour d’une fonction. Et c’est tout.
Optional a été pensé uniquement pour les types de retour, comme expliqué dans cet échange sur la liste OpenJDK par Brian Goetz en septembre 2013 :
The JSR-335 EG felt fairly strongly that Optional should not be on any more than needed to support the optional-return idiom only. (Someone suggested maybe even renaming it to OptionalReturn to beat users over the head with this design orientation; perhaps we should have taken that suggestion.) I get that lots of people want Optional to be something else. But, its not simply the case that the EG "forgot" to make it serializable; they explicitly chose not to. And there's certainly been no new data that has come to light that would motivate us to set that choice aside at this point.
D’ailleurs le même Brian Goetz le ré-affirme sur Stackoverflow
[…]you probably should never use it for something that returns an array of results, or a list of results; instead return an empty array or list. You should almost never use it as a field of something or a method parameter.
Optional n’est pas Serializable, ce qui rend donc son utilisation impossible si vous souhaitez sérialiser vos objets Java. De même, vous ne devez pas vous en servir comme paramètre de fonction.
Optional est-il une monade ?
A priorie, Optional est une monade car :
- c’est un type paramétré (ici Optional<Integer>)
- il dispose d’une fonction d’identité qui pour tout Integer, retourne un Optional<Integer> (la fonction Optional.of(…))
- Optional a une fonction de binding qui permet de passer d’un type à un autre, flatMap
L’intérêt des monades va apparaître aux développeurs Java lorsqu’ils seront amenés à enchaîner des appels de fonctions, susceptible de retourner une valeur ou non. Dès lors que ces fonctions s’appuient sur une monade (ici Optional), vous allez voir que votre code sera plus fonctionnel.
Soit une conférence pour développeur Java, qui dispose de 2 objets métiers : un sujet de conférence et un speaker. Une conférence peut avoir un 2ème speaker, mais ceci n’est pas obligatoire. Pour représenter cela, je vais construire une classe Java orientée Rich-domain (l’exact inverse d’un POJO).
Voici tout d’abord le code de la class Conference :
import java.util.Optional; /** * @author created by N.Martignole, Innoteria, on 07/11/2014. */ public class Conference { private Integer id; private String title; private String mainSpeaker; private String secondarySpeaker; private Conference(Integer id, String title, String mainSpeaker, String secondarySpeaker) { this.id = id; this.title = title; this.mainSpeaker = mainSpeaker; this.secondarySpeaker = secondarySpeaker; } private Conference(Integer id, String title, String mainSpeaker) { this(id, title, mainSpeaker, null); } public static Conference of(Integer id, String title, String mainSpeaker) { return new Conference(id, title, mainSpeaker); } public static Conference of(Integer id, String title, String mainSpeaker, String secondarySpeaker) { return new Conference(id, title, mainSpeaker, secondarySpeaker); } public String getMainSpeaker() { return this.mainSpeaker; } public Optional<String> getSecondarySpeaker() { return Optional.ofNullable(this.secondarySpeaker); } public static Optional<Conference> findById(Integer id) { // Ici acces à votre DB... switch (id) { case 100: return Optional.of(Conference.of(100, "Scala for Beginner", "Nic")); case 200: return Optional.of(Conference.of(200, "Spring 4.1", "John", "Bob")); default: return Optional.empty(); } } @Override public String toString() { return getSecondarySpeaker() .map(localSecondarySpeaker -> String.format("Conference[%s] %s by %s and %s", this.id, this.title, this.mainSpeaker, localSecondarySpeaker)) .orElse(String.format("Conference[%s] %s by %s", this.id, this.title, this.mainSpeaker)); } }
Voyons ce que cela donne avec quelques ID de conférences :
System.out.println(Conference.findById(100)); System.out.println(Conference.findById(200)); System.out.println(Conference.findById(4444)); Optional[Conference[100] Scala for Beginner by Nic] Optional[Conference[200] Spring 4.1 by John and Bob] Optional.empty
Introduisons maintenant une class Speaker avant de refactorer Conference. Un speaker peut avoir déjà fait d’autres présentations, et donc avoir un champ « références ».
import java.util.Optional; /** * @author created by N.Martignole, Innoteria, on 07/11/2014. */ public class Speaker { private Integer id; private String name; private String references; private Speaker(Integer id, String name, String references) { this.id = id; this.name = name; this.references = references; } private Speaker(Integer id, String name) { this(id, name, null); } public static Speaker of(Integer id, String name, String references) { return new Speaker(id, name, references); } public static Speaker of(Integer id, String name) { return new Speaker(id, name, null); } public String getName() { return this.name; } public Optional<String> getReferences() { return Optional.ofNullable(this.references); } public static Optional<Speaker> findById(Integer id) { // Ici acces à votre DB... switch (id) { case 888: return Optional.of(Speaker.of(888, "Nicolas","Speaker Devoxx France 2014")); case 777: return Optional.of(Speaker.of(777, "Bob")); case 444: return Optional.of(Speaker.of(444, "John")); default: return Optional.empty(); } } @Override public String toString() { return getReferences() .map(someReferences -> String.format("Speaker[%s] %s with references %s", this.id, this.name, someReferences)) .orElse(String.format("Speaker[%s] %s with no references", this.id, this.name)); } }
Quelques lignes pour tester :
System.out.println(Speaker.findById(888)); System.out.println(Speaker.findById(444)); System.out.println(Speaker.findById(123));
Optional[Speaker[888] Nicolas with references Speaker Devoxx France 2014] Optional[Speaker[444] John with no references] Optional.empty
Ok nous pouvons maintenant modifier Conference pour utiliser notre classe Speaker. Ne prenez pas trop en compte la méthode findById pour l’instant, l’important sera le code dans la classe principale :
import java.util.Optional; /** * @author created by N.Martignole, Innoteria, on 07/11/2014. */ public class Conference { private Integer id; private String title; private Speaker mainSpeaker; // nouveau private Speaker secondarySpeaker; // nouveau private Conference(Integer id, String title, Speaker mainSpeaker, Speaker secondarySpeaker) { this.id = id; this.title = title; this.mainSpeaker = mainSpeaker; this.secondarySpeaker = secondarySpeaker; } private Conference(Integer id, String title, Speaker mainSpeaker) { this(id, title, mainSpeaker, null); } public static Conference of(Integer id, String title, Speaker mainSpeaker) { return new Conference(id, title, mainSpeaker); } public static Conference of(Integer id, String title, Speaker mainSpeaker, Speaker secondarySpeaker) { return new Conference(id, title, mainSpeaker, secondarySpeaker); } public Speaker getMainSpeaker() { return this.mainSpeaker; } public Optional<Speaker> getSecondarySpeaker() { return Optional.ofNullable(this.secondarySpeaker); } public static Optional<Conference> findById(Integer id) { // Ici acces à votre DB... switch (id) { case 100: // DISCLAIMER : ne pas utiliser .get() // ici il s'agit d'un exemple pour constuire une conf de test return Optional.of( Conference.of(100, "Scala for Beginner", Speaker.findById(888).get()) // juste Nicolas ); case 200: // DISCLAIMER : ne pas utiliser .get() // ici il s'agit d'un exemple pour constuire une conf de test return Optional.of( Conference.of(200, "Spring 4.1", Speaker.findById(777).get(), // Bob Speaker.findById(888).get()) // Nicolas est 2nd speaker ); default: return Optional.empty(); } } @Override public String toString() { return getSecondarySpeaker() .map(localSecondarySpeaker -> String.format("Conference[%s] %s by %s and %s", this.id, this.title, this.mainSpeaker, localSecondarySpeaker)) .orElse(String.format("Conference[%s] %s by %s", this.id, this.title, this.mainSpeaker)); } }
Voyons ce que le code de ma classe principale nous retourne maintenant :
System.out.println(Conference.findById(100)); Optional[Conference[100] Scala for Beginner by Speaker[888] Nicolas with references Speaker Devoxx France 2014] System.out.println(Conference.findById(200)); Optional[Conference[200] Spring 4.1 by Speaker[555] Bob with no references and Speaker[888] Nicolas with references Speaker Devoxx France 2014] System.out.println(Conference.findById(4444)); Optional.empty
Ok parfait. Notez que pour la conférence « 200 », le 2ème speaker s’appelle Nicolas. Il a bien une référence et à priori il a déjà présenté à à Devoxx France 2014.
Nous avons ici un petit domaine tout simple, dans lequel nous allons pouvoir enfin voir la mise en oeuvre d’Optional, qui est une monade.
Comment feriez-vous pour afficher les références du deuxième speaker de n’importe quelle conférence ?
D’abord l’approche impérative, à laquelle nous sommes habitués, sauf que nous utiliserons .isPresent() afin de traiter les cas où nous avons une valeur.
Optional<Conference> maybeConference = Conference.findById(200); if (maybeConference.isPresent()) { Conference conf = maybeConference.get(); // Possible mais franchement dommage... Optional<Speaker> maybeSecondarySpeaker = conf.getSecondarySpeaker(); if (maybeSecondarySpeaker.isPresent()) { Speaker secondarySpeaker = maybeSecondarySpeaker.get(); Optional<String> maybeReference = secondarySpeaker.getReferences(); if (maybeReference.isPresent()) { String references = maybeReference.get(); System.out.println(String.format("J'ai enfin trouvé quelque chose %s", references)); } } }
Voici ci-dessous une autre façon d’implémenter et de résoudre exactement le même problème, que je préfère. Notez l’utilisation des flatMap, qui permettent d’enchaîner les appels si, et seulement si, la fonction retourne un Optional définit :
Conference.findById(200) .flatMap(c -> c.getSecondarySpeaker()) .flatMap(s2 -> s2.getReferences()) .ifPresent(System.out::println);
Enfin on peut terminer en utilisant les références de méthodes, ce qui rend le code plus expressif :
Conference.findById(200) .flatMap(Conference::getSecondarySpeaker) .flatMap(Speaker::getReferences) .ifPresent(System.out::println);
Ce code se lit de cette façon : recherche une conférence avec pour id=200. Si tu trouves une conférence, alors extrait le secondarySpeaker. S’il y en a, extrait ses références. Enfin affiche moi ce que tu as trouvé.
Notez aussi que lorsque vous souhaitez mapper un appel, et que vous êtes certain que le type de retour n’est pas null, la fonction map permet de mapper une conférence vers un Speaker comme ci-dessous :
Conference.findById(100) .map(Conference::getMainSpeaker) // il y a toujours un mainSpeaker dans notre domaine .flatMap(Speaker::getReferences) .ifPresent(System.out::println);
Et Scala ?
Nous avons accès à une librairie plus puissante. Tout d’abord il y a un type Option, qui permet d’exprimer clairement qu’une propriété est susceptible d’être non renseignée. Ensuite pour le cas que couvre Optional en Java, nous avons le choix entre Option et Either. Enfin pour ceux qui utilisent ScalaZ, il y a Maybe.
Au niveau Scala, nous définissons 2 case-class, puis 2 companions objects avec nos finder. L’approche est encore plus simple, car Scala propose les for-comprehension qui permettent d’écrire de façon différente une suite de map/flatMap sur des monades.
case class Speaker(id: Int, name: String, references: Option[String]) object Speaker { def findById(id: Int): Option[Speaker] = id match { case 888 => Some(Speaker(888, "Nicolas", Some("Speaker à DevoxxFR"))) case 777 => Some(Speaker(777, "Bob", None)) case 444 => Some(Speaker(444, "John", None)) case other => None } } case class Conference(id: Int, title: String, mainSpeaker: Speaker, secondarySpeaker: Option[Speaker]) object Conference{ def findById(id:Int):Option[Conference] = id match { case 100 => Some(Conference(100, "Scala for Beginner", Speaker.findById(888).get, None)) case 200 => Some(Conference(200, "Spring 4.1", Speaker.findById(777).get, Speaker.findById(888))) case other => None } } object Main extends App { println(Conference.findById(100)) println(Conference.findById(999).getOrElse("No conference for this id")) for(c <- Conference.findById(200) ; s <- c.secondarySpeaker ; reference <- s.references ) yield { println(s"Référence de conférence pour le 2e speaker $reference") } }
Conclusion
Optional en Java est d’abord destiné à améliorer l’expressivité de vos fonctions. Dès lors qu’un appel peut ou non retourner une valeur, c’est un moyen simple d’améliorer votre code. Dommage que l’intention et l’implémentation ne permette pas d’utiliser ce type pour des attributs d’une classe, ou même, pour ses arguments. Certes, vous pouvez le faire au sens « Java ». Mais comme vous venez de le voir, Optional n’a d’intérêt que si vous souhaitez écrire du code fonctionnel. Tout est donc question de ce que vous souhaitez faire avec votre code.
Quelques références :
- La classe Optional en Java 8
- La class Option en Scala
- Should Java 8 getters returns Optional ?
- Shouldn’t Optional be Serializable ?
- Tired of NullPointerException? Consider using Java SE 8 Optional
- Jetbrains : @Nullable et @NotNullable
Pour ma part, lorsque la conférence n’est pas trouvée par exemple, je lance une exception spécifique type NotFoundException. Du coup inutile de checker null ou pas. Si la fonction retourne une Conference, alors je sais qu’elle n’est pas nulle.
En quoi l’usage présenté ici est mieux/plus approprié ? (Aucun troll, je veux juste comprendre 🙂 )
@Chafik : en effet il est tout à fait possible d’utiliser une Checked exception ici. Surtout pas une RuntimeException pour ce cas d’erreur, qui est un cas métier (et pas un problème de programmation).
Cependant je trouve cela dommage et je décourage les développeurs Java d’utiliser uniquement les exceptions. Dès lors que ta fonction indique qu’elle peut (ou non) retourner un résultat, cela correspond plus à son fonctionnement. Tu peux en plus chaîner tes appels de fonctions, et cela rend d’abord le code « client » de ta fonction plus simple.
Anders Hejlsberg (créateur de Delphi, C#, Typescript) a d’ailleurs d’autres arguments intéressants à partager sur ce point : http://www.artima.com/intv/handcuffs.html
En effet l’article est très intéressant.
Toutefois, son exemple avec la modification d’une méthode en ajoutant
throws D
alors que la méthode lançait déjà A, B et C peut être simplement contourné si on respecte la règle – que j’ai découverte dans Sonar – mentionnant qu’une méthode ne devrait lancer qu’une exception. En l’occurrence, la méthode lancerait A. Et B, C et D étendent A.Enfin, dieu merci je ne fais jamais de
throws Exception
.Je vais voir pour appliquer ce que tu as montré dans mes projets. C’est vrai que ce qui faisait peur avant était la
NullPointerException
mais avecOptional
, le problème disparait.