Opcjonalne lubElse Opcjonalne w Javie

137

Pracowałem z nowym typem opcjonalnym w Javie 8 i natknąłem się na coś, co wygląda na typową operację, która nie jest obsługiwana funkcjonalnie: „orElseOptional”

Rozważ następujący wzór:

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);
}

Istnieje wiele form tego wzorca, ale sprowadza się on do chęci użycia „orElse” na opcji, która przyjmuje funkcję produkującą nową opcję, wywoływaną tylko wtedy, gdy bieżąca nie istnieje.

Jego implementacja wyglądałaby tak:

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

Ciekaw jestem, czy istnieje powód, dla którego taka metoda nie istnieje, czy po prostu używam Optional w niezamierzony sposób i jakie inne sposoby wymyślili ludzie, aby poradzić sobie z tym przypadkiem.

Powinienem powiedzieć, że uważam, że rozwiązania obejmujące niestandardowe klasy / metody narzędziowe nie są eleganckie, ponieważ ludzie pracujący z moim kodem niekoniecznie wiedzą, że istnieją.

Poza tym, jeśli ktoś wie, czy taka metoda zostanie uwzględniona w JDK 9 i gdzie mogę zaproponować taką metodę? Wydaje mi się to dość rażącym pominięciem API.

Yona Appletree
źródło
13
Zobacz ten numer . Dla wyjaśnienia: to będzie już w Javie 9 - jeśli nie w przyszłej aktualizacji Java 8.
Obicere
To jest! Dzięki, nie znalazłem tego podczas moich poszukiwań.
Yona Appletree
2
@Obicere Ten problem nie dotyczy tutaj, ponieważ dotyczy zachowania na pustym polu Opcjonalnym, a nie alternatywnego wyniku . Opcjonalny już ma orElseGet()to, czego potrzebuje OP, ale nie generuje ładnej kaskadowej składni.
Marko Topolnik
1
Świetne samouczki na temat języka Java Opcjonalnie: codeflex.co/java-optional-no-more-nullpointerexception
John Detroit

Odpowiedzi:

86

Jest to część JDK 9 w postaci or, która ma rozszerzenie Supplier<Optional<T>>. Twój przykład byłby wtedy:

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

Aby uzyskać szczegółowe informacje, zobacz Javadoc lub ten post, który napisałem.

Nicolai
źródło
Miły. Ten dodatek musi mieć rok i nie zauważyłem. Jeśli chodzi o pytanie w Twoim blogu, zmiana typu zwracanego zepsułaby zgodność binarną, ponieważ instrukcje wywołania kodu bajtowego odnoszą się do pełnego podpisu, w tym typu zwracanego, więc nie ma szans na zmianę zwracanego typu ifPresent. W każdym razie myślę, że nazwa i ifPresenttak nie jest dobra. Dla wszystkich innych metod nie mając „else” w nazwie (jak map, filter, flatMap), to zakłada się, że nie robią nic, jeśli wartość nie jest obecna, więc dlaczego ifPresent...
Holger
A więc dodanie Optional<T> perform(Consumer<T> c)metody umożliwiającej łączenie w łańcuch perform(x).orElseDo(y)( orElseDojako alternatywa dla proponowanej ifEmpty, aby być spójnym w elsenazwie wszystkich metod, które mogą zrobić coś dla nieobecnych wartości). Możesz to naśladować performw Javie 9, stream().peek(x).findFirst()chociaż jest to nadużycie interfejsu API i nadal nie ma możliwości wykonania polecenia Runnablebez określenia a Consumerw tym samym czasie…
Holger
65

Najczystszym podejściem „wypróbuj usługi” przy obecnym API byłoby:

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();

Ważnym aspektem nie jest (stały) łańcuch operacji, który trzeba raz napisać, ale to, jak łatwo jest dodać kolejną usługę (lub zmodyfikować listę usług jest ogólna). Tutaj ()->serviceX(args)wystarczy dodać lub usunąć singiel .

Ze względu na leniwą ocenę strumieni żadna usługa nie zostanie wywołana, jeśli poprzednia usługa zwróciła wartość niepustą Optional.

Holger
źródło
13
właśnie użyłem tego w projekcie, dzięki Bogu nie robimy jednak recenzji kodu.
Ilya Smagin
3
Jest dużo czystszy niż łańcuch „orElseGet”, ale jest też dużo trudniejszy do odczytania.
slartidan
4
To jest z pewnością poprawne ... ale szczerze, przeanalizowanie go zajmuje mi sekundę i nie jestem pewien, czy jest poprawny. Pamiętaj, że zdaję sobie sprawę, że ten przykład jest, ale mogę sobie wyobrazić małą zmianę, która sprawiłaby, że nie byłaby ona leniwie oceniana lub zawierała jakiś inny błąd, który byłby nie do odróżnienia na pierwszy rzut oka. Moim zdaniem należy to do kategorii niedoszłych przyzwoitych funkcji użytkowych.
Yona Appletree
3
Zastanawiam się, czy przełączenie .map(Optional::get)z .findFirst()ułatwi „czytanie”, np. Czy .filter(Optional::isPresent).findFirst().map(Optional::get)można „czytać” jak ”znaleźć pierwszy element w strumieniu, dla którego parametr Optional :: isPresent ma wartość true, a następnie spłaszczyć go stosując Optional :: get”?
schatten
3
Zabawne, kilka miesięcy temu zamieściłem bardzo podobne rozwiązanie na podobne pytanie. To jest pierwszy raz, kiedy się z tym spotykam.
shmosel
34

To nie jest ładne, ale to zadziała:

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)jest dość poręcznym wzorem do użytku z Optional. To znaczy „Jeśli to Optionalzawiera wartość v, daj mi func(v), w przeciwnym razie daj mi sup.get()”.

W takim przypadku dzwonimy serviceA(args)i otrzymujemy Optional<Result>. Jeśli Optionalzawiera wartość v, chcemy otrzymać Optional.of(v), ale jeśli jest pusta, chcemy pobrać serviceB(args). Powtórz płukanie z większą liczbą alternatyw.

Inne zastosowania tego wzoru to

  • .map(Stream::of).orElseGet(Stream::empty)
  • .map(Collections::singleton).orElseGet(Collections::emptySet)
Misha
źródło
1
huh, kiedy używam tej strategii, eclipse mówi: „Metoda orElseGet (Supplier <? extends String>) w typie Optional <String> nie ma zastosowania do argumentów (() -> {})” Nie wydaje się rozważyć zwrócenie opcjonalnej prawidłowej strategii w ciągu ??
chrismarx
@chrismarx () -> {}nie zwraca pliku Optional. Co próbujesz osiągnąć?
Misha,
1
Po prostu próbuję podążać za przykładem. Łączenie wywołań mapy nie działa. Moje usługi zwracają ciągi i oczywiście nie ma wtedy dostępnej opcji .map ()
chrismarx
1
Doskonała czytelna alternatywa dla nadchodzącej or(Supplier<Optional<T>>)Java 9
David M.
1
@Sheepy, mylisz się. .map()na pustym Optionalutworzy pusty Optional.
Misha
27

Być może właśnie tego szukasz: Uzyskaj wartość z jednego lub drugiego opcjonalnego

W przeciwnym razie warto rzucić okiem Optional.orElseGet. Oto przykład tego, czego myślę , że szukasz:

result = Optional.ofNullable(serviceA().orElseGet(
                                 () -> serviceB().orElseGet(
                                     () -> serviceC().orElse(null))));
aioobe
źródło
2
Tak zwykle robią geniusze. Oceniaj opcjonalne jako null, a zawijanie go ofNullableto najfajniejsza rzecz, jaką kiedykolwiek widziałem.
Jin Kwon
5

Zakładając, że nadal korzystasz z JDK8, istnieje kilka opcji.

Opcja nr 1: stwórz własną metodę pomocniczą

Na przykład:

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);
    }
}

Abyś mógł:

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

Opcja nr 2: użyj biblioteki

Np. Opcja opcjonalna google guava obsługuje prawidłowe or()działanie (podobnie jak JDK9), np .:

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

(Gdzie każda z usług powraca com.google.common.base.Optional, a nie java.util.Optional).

Owca
źródło
Nie znalazłem Optional<T>.or(Supplier<Optional<T>>)w dokumentach Guava. Czy masz do tego link?
Tamas Hegedus
.orElseGet zwraca T, ale Optional :: empty zwraca Optional <T>. Optional.ofNullable (........ lub Else (null)) ma pożądany efekt, zgodnie z opisem @aioobe.
Miguel Pereira
@TamasHegedus Masz na myśli opcję nr 1? Jest to niestandardowa implementacja, która jest wycięta powyżej. ps: przepraszam za spóźnioną odpowiedź
Sheepy
@MiguelPereira .orElseGet w tym przypadku zwraca parametr Optional <T>, ponieważ działa on w trybie Optional <Optional <T>>
Sheepy
2

Wygląda na to, że dobrze pasuje do dopasowywania wzorców i bardziej tradycyjnego interfejsu Option z implementacjami Some i None (takich jak te w Javaslang , FunctionalJava ) lub leniwą implementacją Może w cyclops-react. Jestem autorem tej biblioteki.

Z cyklopem-reagować możesz także używać strukturalnego dopasowywania wzorców na typach JDK. W przypadku opcji Opcjonalne można dopasować do obecnych i nieobecnych przypadków według wzorca gości . wyglądałoby to mniej więcej tak -

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

  optional(serviceA(args)).visit(some -> some , 
                                 () -> optional(serviceB(args)).visit(some -> some,
                                                                      () -> serviceC(args)));
John McClean
źródło