En option ou en option en Java

137

J'ai travaillé avec le nouveau type facultatif dans Java 8 , et j'ai rencontré ce qui semble être une opération courante qui n'est pas prise en charge fonctionnellement: un "orElseOptional"

Considérez le modèle suivant:

Optional<Result> resultFromServiceA = serviceA(args);
if (resultFromServiceA.isPresent) return result;
else {
    Optional<Result> resultFromServiceB = serviceB(args);
    if (resultFromServiceB.isPresent) return resultFromServiceB;
    else return serviceC(args);
}

Il existe de nombreuses formes de ce modèle, mais cela revient à vouloir un "orElse" sur un optionnel qui prend une fonction produisant un nouveau optionnel, appelé uniquement si l'actuel n'existe pas.

Son implémentation ressemblerait à ceci:

public Optional<T> orElse(Supplier<Optional<? extends T>> otherSupplier) {
    return value != null ? this : other.get();
}

Je suis curieux de savoir s'il y a une raison pour laquelle une telle méthode n'existe pas, si j'utilise simplement Optionnel de manière non intentionnelle, et quelles autres façons les gens ont trouvées pour traiter ce cas.

Je dois dire que je pense que les solutions impliquant des classes / méthodes utilitaires personnalisées ne sont pas élégantes car les personnes travaillant avec mon code ne sauront pas nécessairement qu'elles existent.

De plus, si quelqu'un le sait, une telle méthode sera-t-elle incluse dans JDK 9, et où puis-je proposer une telle méthode? Cela me semble être une omission assez flagrante de l'API.

Yona Appletree
la source
13
Voir ce numéro . Pour clarifier: cela va déjà être dans Java 9 - sinon dans une future mise à jour de Java 8.
Obicere
C'est! Merci, je n'ai pas trouvé ça dans ma recherche.
Yona Appletree
2
@Obicere Ce problème ne s'applique pas ici car il s'agit d'un comportement sur facultatif vide, pas d' un résultat alternatif . Facultatif a déjà orElseGet()pour ce dont OP a besoin, mais il ne génère pas une belle syntaxe en cascade.
Marko Topolnik le
1
Grands tutoriels sur Java Facultatif: codeflex.co/java-optional-no-more-nullpointerexception
John Detroit

Réponses:

86

Cela fait partie du JDK 9 sous la forme de or, qui prend un Supplier<Optional<T>>. Votre exemple serait alors:

return serviceA(args)
    .or(() -> serviceB(args))
    .or(() -> serviceC(args));

Pour plus de détails, consultez le Javadoc ou ce post que j'ai écrit.

Nicolai
la source
Agréable. Cet ajout doit remonter à un an et je ne l'ai pas remarqué. En ce qui concerne la question dans votre blog, changer le type de retour briserait la compatibilité binaire, car les instructions d'invocation de bytecode se réfèrent à la signature complète, y compris le type de retour, il n'y a donc aucune chance de changer le type de retour de ifPresent. Mais de toute façon, je pense que le nom ifPresentn'est pas bon de toute façon. Pour toutes les autres méthodes ne portant pas « d' autre » dans le nom (comme map, filter, flatMap), il est sous - entendu qu'ils ne font rien si aucune valeur est présent, alors pourquoi ifPresent...
Holger
Donc en ajoutant une Optional<T> perform(Consumer<T> c)méthode pour permettre le chaînage perform(x).orElseDo(y)( orElseDocomme alternative à votre proposition ifEmpty, pour être cohérent elsedans le nom de toute méthode qui pourrait faire quelque chose pour les valeurs absentes). Vous pouvez imiter cela performdans Java 9 via stream().peek(x).findFirst()bien que ce soit un abus de l'API et il n'y a toujours aucun moyen d'exécuter un Runnablesans spécifier un Consumeren même temps…
Holger
65

L'approche «essayer les services» la plus propre étant donné l'API actuelle serait:

Optional<Result> o = Stream.<Supplier<Optional<Result>>>of(
    ()->serviceA(args), 
    ()->serviceB(args), 
    ()->serviceC(args), 
    ()->serviceD(args))
.map(Supplier::get)
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();

L'aspect important n'est pas la chaîne (constante) d'opérations que vous devez écrire une seule fois mais la facilité avec laquelle il est d'ajouter un autre service (ou de modifier la liste des services est générale). Ici, ajouter ou supprimer un single ()->serviceX(args)suffit.

En raison de l'évaluation paresseuse des flux, aucun service ne sera appelé si un service précédent a renvoyé une valeur non vide Optional.

Holger
la source
13
vient de l'utiliser dans un projet, Dieu merci, nous ne faisons pas de révision de code.
Ilya Smagin
3
C'est beaucoup plus propre qu'une chaîne de "orElseGet", mais c'est aussi beaucoup plus difficile à lire.
slartidan
4
C'est certainement correct ... mais honnêtement, il me faut une seconde pour l'analyser et je ne repars pas avec certitude que c'est correct. Remarquez, je me rends compte que cet exemple est, mais je pourrais imaginer un petit changement qui le ferait ne pas être évalué paresseusement ou aurait une autre erreur qui serait impossible à distinguer d'un coup d'œil. Pour moi, cela tombe dans la catégorie des fonctions qui seraient décentes en tant qu'utilité.
Yona Appletree
3
Je me demande si changer .map(Optional::get)avec .findFirst()rendra plus facile la "lecture", par exemple .filter(Optional::isPresent).findFirst().map(Optional::get)peut être "lu" comme "trouver le premier élément dans le flux pour lequel Optional :: isPresent est vrai et l'aplatir ensuite en appliquant Optional :: get"?
schatten
3
C'est drôle, j'ai posté une solution très similaire pour une question similaire il y a quelques mois. C'est la première fois que je rencontre ça.
shmosel
34

Ce n'est pas joli, mais cela fonctionnera:

return serviceA(args)
  .map(Optional::of).orElseGet(() -> serviceB(args))
  .map(Optional::of).orElseGet(() -> serviceC(args))
  .map(Optional::of).orElseGet(() -> serviceD(args));

.map(func).orElseGet(sup)est un modèle assez pratique à utiliser avec Optional. Cela signifie "Si cela Optionalcontient de la valeur v, donnez-moi func(v), sinon donnez-moi sup.get()".

Dans ce cas, nous appelons serviceA(args)et obtenons un fichier Optional<Result>. Si cela Optionalcontient une valeur v, nous voulons obtenir Optional.of(v), mais si elle est vide, nous voulons obtenir serviceB(args). Rincer-répéter avec plus d'alternatives.

D'autres utilisations de ce modèle sont

  • .map(Stream::of).orElseGet(Stream::empty)
  • .map(Collections::singleton).orElseGet(Collections::emptySet)
Misha
la source
1
hein, quand j'utilise cette stratégie, eclipse dit: "La méthode orElseGet (Supplier <? extend String>) dans le type Optional <String> n'est pas applicable pour les arguments (() -> {})" Cela ne semble pas envisager de renvoyer une stratégie facultative valide, sur la chaîne ??
chrismarx
@chrismarx () -> {}ne renvoie pas de fichier Optional. Qu'est-ce que vous essayez d'accomplir?
Misha
1
J'essaye juste de suivre l'exemple. Le chaînage des appels de carte ne fonctionne pas. Mes services renvoient des chaînes, et bien sûr, il n'y a pas d'option .map () disponible alors
chrismarx
1
Excellente alternative lisible au prochain or(Supplier<Optional<T>>)Java 9
David M.
1
@Sheepy vous vous trompez. .map()sur un vide Optionalproduira un vide Optional.
Misha
27

C'est peut-être ce que vous recherchez: obtenez de la valeur à partir de l'un ou l'autre facultatif

Sinon, vous voudrez peut-être jeter un coup d'œil à Optional.orElseGet. Voici un exemple de ce que je pense que vous recherchez:

result = Optional.ofNullable(serviceA().orElseGet(
                                 () -> serviceB().orElseGet(
                                     () -> serviceC().orElse(null))));
aioobe
la source
2
C'est ce que font habituellement les génies. Évaluer l'option facultative comme nullable et l'envelopper avec ofNullableest la chose la plus cool que j'ai jamais vue.
Jin Kwon
5

En supposant que vous soyez toujours sur JDK8, il existe plusieurs options.

Option n ° 1: créez votre propre méthode d'aide

Par exemple:

public class Optionals {
    static <T> Optional<T> or(Supplier<Optional<T>>... optionals) {
        return Arrays.stream(optionals)
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .findFirst()
                .orElseGet(Optional::empty);
    }
}

Pour que vous puissiez faire:

return Optionals.or(
   ()-> serviceA(args),
   ()-> serviceB(args),
   ()-> serviceC(args),
   ()-> serviceD(args)
);

Option 2: utiliser une bibliothèque

Par exemple, l'option facultative de google guava prend en charge une or()opération correcte (tout comme JDK9), par exemple:

return serviceA(args)
  .or(() -> serviceB(args))
  .or(() -> serviceC(args))
  .or(() -> serviceD(args));

(Où chacun des services revient com.google.common.base.Optional, plutôt que java.util.Optional).

Sheepy
la source
Je n'ai pas trouvé Optional<T>.or(Supplier<Optional<T>>)dans la documentation Guava. Avez-vous un lien pour cela?
Tamas Hegedus
.orElseGet renvoie T mais Optional :: empty renvoie Optional <T>. Facultatif.ofNullable (........ orElse (null)) a l'effet souhaité tel que décrit par @aioobe.
Miguel Pereira
@TamasHegedus Vous voulez dire dans l'option n ° 1? C'est une implémentation personnalisée qui se trouve au-dessus. ps: désolé pour la réponse tardive
Sheepy
@MiguelPereira .orElseGet dans ce cas renvoie Optional <T> car il fonctionne sur Optional <Optional <T>>
Sheepy
2

Cela ressemble à un bon ajustement pour la correspondance de modèles et une interface Option plus traditionnelle avec des implémentations Some et None (telles que celles de Javaslang , FunctionalJava ) ou une implémentation paresseuse Maybe dans cyclops-react Je suis l'auteur de cette bibliothèque.

Avec cyclops-react, vous pouvez également utiliser la correspondance de motifs structurels sur les types JDK. Pour Optionnel, vous pouvez faire correspondre les cas présents et absents via le modèle de visiteur . ça ressemblerait à quelque chose comme ça -

  import static com.aol.cyclops.Matchables.optional;

  optional(serviceA(args)).visit(some -> some , 
                                 () -> optional(serviceB(args)).visit(some -> some,
                                                                      () -> serviceC(args)));
John McClean
la source