Pages

mercredi 20 août 2014

Les Optional introduit à partir de Java 8

J'ai déjà parlé de Java 8 dans plusieurs articles. Le plus récent a été fait après l'intervention de José Paumart au Lyon Jug. J'ai rapidement abordé le nouveau concept sur les Optional mais je vais essayer d'être un peu plus précis ici.

Qu'est ce qu'un Optional ?
Un Optional est un objet immutable qui peut contenir une référence non nulle vers un autre objet. Plus simplement, on peut dire qu'un Optional est un conteneur d'objet qui fournit différentes méthodes pour manipuler cet objet.

Les Optional sont nouveaux dans le langage Java mais le concept est plus vieux. Personnellement je l'avais découvert avec Guava (version > 10) mais il est aussi présent dans d'autres langages comme par exemple Scala.

Vous me direz c'est super... mais à quoi ça sert ?  Un Optional permet de représenter trois états : référence présente, référence absente, et null quand la référence n'existe pas. Nous pouvons faire un rapprochement avec le wrapper Boolean. Un booléen a normalement deux valeurs : vrai ou faux. Mais avec le wrapper Boolean, nous pouvons en représenter 3 : vrai, faux ou null. Ceci est très pratique niveau IHM pour savoir si un utilisateur a au moins cliquer une fois sur une case à cocher ou un bouton radio.

Un premier exemple
Il faut concrétiser ces explications par du code. Prenons l'exemple suivant.

public class PersonRepository {
    //Our names
    private List<Person> winners = Arrays.asList(
            new Person(25, "Paul"),
            new Person(35, "Sylvie"),
            new Person(15, "Mathilde")
    );

    public Person getWinnerByName(String name) {
        for (Person p : winners) {
            if (name.equals(p.getName())) {
                return p;
            }
        }
        return null;
    }
}

Les tests sont les suivants
assertThat(personRepository.getWinnerByName("Wendy")).isNull();
assertThat(personRepository.getWinnerByName("Mathilde").getAge()).isEqualTo(15);

Si je veux utiliser cette classe dans un service je vais devoir tester la nullité du résultat
public void sendMailToWinner(String name){
    Person person = personRepository.getWinnerByName(name);
    if(person!=null){
        sendMail(person.getMail(), "You win...");
    }
}

Maintenant voyons ce qu'ils se passe si nous utilisons Optional . Notre repository devient
public Optional<Person> getWinnerByName2(String name){
    for(Person p : winners){
        if(name.equals(p.getName())){
            return Optional.of(p);
        }
    }
    return Optional.empty();
}

L'API Java 8 Stream utilise des Optional
Comme nous faisons du Java 8, nous pouvons utiliser l'API Stream qui renvoie des Optional.
public Optional<Person> getWinnerByName3(String name){
    return winners.stream().filter(p -> name.equals(p.getName())).findFirst();
}

Le code est maintenant simplifié, et nous pouvons voir le résultat sur les tests. La méthode get() permet d'accéder au contenu de l'Optional

assertThat(personRepository.getWinnerByName3("Wendy")).isEqualTo(Optional.empty());
assertThat(personRepository.getWinnerByName3("Mathilde").get().getAge()).isEqualTo(15);

Attention, si vous essayez d'accéder à un Optional empty via un get() une exception NoSuchElementException est levée
Optional.empty().get(); ==> NoSuchElementException
assertThat(Optional.of("toto").get()).isEqualTo("toto");

Les méthodes isPresent et ifPresent
Vous serez amené à utiliser la méthode  isPresent() avant de faire un get()
public void sendMailToWinner(String name){
    Optional<Person> person = personRepository.getWinnerByName2(name);
    if(person.isPresent()){
        sendMail(person.get().getMail(), "You win...");
    }
}

Le code est similaire à ce que nous avions en testant le null, mais nous pouvons simplifier ici aussi l'écriture, en utilisant la méthode ifPresent()
public void sendMailToWinner(String name){
    personRepository.getWinnerByName2(name).ifPresent(p -> sendMail(p.getMail(), "You win..."));
}

Des alternatives si le résultat n'est pas présent...
D'autres méthodes sont disponibles pour vous aider à gérer le cas où la valeur n'est pas présente : orElse(), orElseGet(), orElseThrow() .

Par exemple
assertThat(personRepository.getWinnerByName3("Wendy")
        .orElse(new Person().setName("anonymous")).getName())
        .isEqualTo("anonymous");

assertThat(personRepository.getWinnerByName3("Wendy")
        .orElseGet(() -> personRepository.getWinnerByName3("Mathilde").get()).getName())
        .isEqualTo("Mathilde");

personRepository.getWinnerByName3("Wendy").orElseThrow(IllegalArgumentException::new); ==> throw IllegalArgumentException

Comment travailler sur le résultat ?
Nous avons vu plus haut la méthode  ifPresent() qui permet de lancer une action sur la donnée si elle est présente. Dans la même idée vous pouvez aussi utiliser filter() et map() pour filtrer ou transformer le résultat.

Par exemple
assertThat(Optional.of("toto").map( p -> p.toUpperCase()).get()).isEqualTo("TOTO");
assertThat(Optional.of("toto").filter( p -> p.equals("titi"))).isEqualTo(Optional.empty());

Conclusion
Les Optional permettent de construire des API plus expressives où les utilisateurs doivent se soucier des valeurs retournées. Sur l'appel d'une méthode classique on ne sait pas si l'objet retourné peut être null ou non. Si cette méthode renvoie un Optional on sait directement que le retour doit être testé.

Les Optional ne peuvent pas être utilisés dans tous les contextes. Comme ils ne sont pas sérialisables il ne faut pas les employer dans les POJO (DO, DTO...), dans les paramètres des méthodes et des constructeurs. Il faut également éviter d'utiliser les méthodes equals() et hashcode() des Optional.

Pour plus d'information je vous laisse consulter la Javadoc et le code source des exemples est dispo sous Github

1 commentaire:

  1. Merci Guillaume, un petit article comme on les aime qui explique clairement l'utilité des Optional avec des lambdas et du festAssert. J'adore...

    RépondreSupprimer

Remarque : Seul un membre de ce blog est autorisé à enregistrer un commentaire.