Czy moduły pobierające Java 8 powinny zwracać typ opcjonalny?

288

Optional Typ wprowadzony w Javie 8 to nowość dla wielu programistów.

Czy metoda zwracająca metodę gettera Optional<Foo>zamiast klasyki Foojest dobrą praktyką? Załóżmy, że wartość może być null.

leonprou
źródło
8
Chociaż może to przyciągać opinie, to dobre pytanie. Nie mogę się doczekać odpowiedzi z prawdziwymi faktami na ten temat.
Justin
8
Pytanie brzmi, czy nie można uniknąć nullablitity. Komponent może mieć właściwość, która może mieć wartość NULL, ale programista korzystający z tego komponentu może zdecydować o ścisłym zachowaniu tej właściwości null. Dlatego programista nie powinien wtedy mieć do czynienia Optional. Innymi słowy, tak nullnaprawdę reprezentuje brak wartości jak w wyniku wyszukiwania (gdzie Optionaljest to właściwe) lub jest nulltylko jednym członkiem zestawu możliwych wartości.
Holger
1
Zobacz także dyskusję na temat @NotNulladnotacji: stackoverflow.com/q/4963300/873282
koppor

Odpowiedzi:

516

Oczywiście ludzie będą robić, co chcą. Ale dodaliśmy tę funkcję, mieliśmy wyraźny zamiar i nie miała ona być celem ogólnym. Może typ, tak jak wiele osób chciałoby, abyśmy to zrobili. Naszym zamiarem było udostępnienie ograniczonego mechanizmu dla typów zwracanych metod bibliotecznych, w których potrzebny był wyraźny sposób reprezentowania „braku rezultatu”, a użycie nulldo takich celów było w przeważającej mierze przyczyną błędów.

Na przykład prawdopodobnie nigdy nie należy go używać do czegoś, co zwraca tablicę wyników lub listę wyników; zamiast tego zwraca pustą tablicę lub listę. Prawie nigdy nie powinieneś używać go jako pola czegoś lub parametru metody.

Myślę, że rutynowe używanie go jako wartości zwracanej dla osób pobierających byłoby zdecydowanie nadmiernym wykorzystaniem.

Z Opcjonalnym nie ma nic złego , że należy tego unikać, po prostu nie jest to, czego wielu ludzi sobie życzy, i dlatego byliśmy dość zaniepokojeni ryzykiem gorliwego nadużywania.

(Ogłoszenie o usłudze publicznej: NIGDY nie należy dzwonić, Optional.getchyba że można udowodnić, że nigdy nie będzie zerowe; zamiast tego należy użyć jednej z bezpiecznych metod takich jak orElselub ifPresent. Z perspektywy czasu powinniśmy nazwać getcoś takiego getOrElseThrowNoSuchElementExceptionlub coś, co sprawiłoby, że stało się jasne, że była to bardzo niebezpieczna metoda które podważyły ​​w ogóle cały cel Optional. Wyciągnięta lekcja. (AKTUALIZACJA: Java 10 ma Optional.orElseThrow(), co jest semantycznie równoważne get(), ale której nazwa jest bardziej odpowiednia).

Brian Goetz
źródło
24
(Odnośnie do ostatniej części)… i kiedy jesteśmy pewni, że nigdy nullnie skorzystamy z wartości orElseThrow(AssertionError::new), hmmm lub orElseThrow(NullPointerException::new)
Holger
25
Co byś zrobił inaczej, jeśli intencją nie było wprowadzenie ogólnego przeznaczenia Może albo jakiś rodzaj? Czy istnieje sposób, w jaki Opcjonalne nie pasuje do rachunku za jeden, czy tylko dlatego, że wprowadzenie Opcjonalnych w całym nowym interfejsie API sprawiłoby, że nie byłby to język Java?
David Moles
22
Przez „typ ogólnego przeznaczenia” miałem na myśli wbudowanie go w system typów językowych, a nie zapewnienie klasy biblioteki zbliżonej do niego. (Niektóre języki mają typy dla T? (T lub null) i T! (Niepozwalające T)) Opcjonalne to tylko klasa; nie możemy dokonywać niejawnych konwersji między Foo a Opcjonalnym <Foo>, co moglibyśmy uzyskać dzięki obsłudze języków.
Brian Goetz
11
Zastanawiałem się przez chwilę, dlaczego nacisk w świecie Java był na Opcjonalny, a nie na lepszą analizę statyczną. Opcjonalny ma pewne zalety, ale ogromną zaletą nulljest kompatybilność wsteczna; Map::getzwraca wartość null V, a nie an Optional<V>, i to się nigdy nie zmieni. Można to jednak łatwo opatrzyć adnotacjami @Nullable. Teraz mamy dwa sposoby wyrażenia braku wartości, a także mniejszą motywację do przeprowadzenia analizy statycznej, co wydaje się być gorszą
sytuacją
21
Nie miałem pojęcia, że ​​użycie go jako wartości zwracanej dla właściwości nie było tym, co zamierzaliście, dopóki nie przeczytam tej odpowiedzi tutaj na StackOverflow. W rzeczywistości doprowadzono mnie do przekonania, że ​​dokładnie to zamierzaliście po przeczytaniu tego artykułu na stronie Oracle: oracle.com/technetwork/articles/java/... Dziękuję za wyjaśnienie
Jason Thompson,
73

Po przeprowadzeniu własnych badań natknąłem się na wiele rzeczy, które mogą sugerować, kiedy jest to właściwe. Najbardziej wiarygodny jest następujący cytat z artykułu Oracle:

„Należy zauważyć, że intencją klasy Opcjonalnej nie jest zastąpienie każdego pojedynczego odwołania zerowego . Zamiast tego jej celem jest pomoc w projektowaniu bardziej zrozumiałych interfejsów API , aby po prostu czytając podpis metody, można stwierdzić, czy może oczekiwać wartości opcjonalnej. Zmusza to do aktywnego rozpakowania Opcjonalnego, aby poradzić sobie z brakiem wartości ”. - Masz dość wyjątków wskaźnika zerowego? Rozważ użycie opcjonalnej wersji Java SE 8!

Znalazłem również ten fragment Java 8 Opcjonalnie: jak go używać

„Opcjonalne nie jest przeznaczone do użycia w tych kontekstach, ponieważ nic nam nie kupi:

  • w warstwie modelu domeny (nie można serializować)
  • w DTO (ten sam powód)
  • w wejściowych parametrach metod
  • w parametrach konstruktora ”

Co również wydaje się podnosić niektóre ważne punkty.

Nie byłem w stanie znaleźć żadnych negatywnych skojarzeń ani czerwonych flag, które sugerowałyby, że Optionalnależy tego unikać. Myślę, że ogólną ideą jest to, że jeśli jest to pomocne lub poprawia użyteczność twojego API, użyj go.

Justin
źródło
11
stackoverflow.com/questions/25693309 - wygląda na to, że Jackson już to obsługuje, więc „nie można serializować” nie jest już ważnym powodem :)
Vlasec
1
Proponuję podać adres odpowiedzi, dlaczego użycie Optionalparametrów wejściowych metod (a dokładniej konstruktorów) „nic nam nie kupi”. dolszewski.com/java/java-8-optional-use-cases zawiera ładne wyjaśnienie.
Gili
Odkryłem, że curry opcjonalnych wyników, które należy przekształcić w inne interfejsy API, wymaga parametru opcjonalnego. Rezultatem jest dość zrozumiały interfejs API. Zobacz stackoverflow.com/a/31923105/105870
Karl the Pagan
1
Link jest zepsuty. Czy możesz zaktualizować odpowiedź?
softarn
2
Problem w tym, że często nie poprawia API, pomimo najlepszych intencji dewelopera. Zebrałem przykłady złych zastosowań Opcjonalnych , wszystkie zaczerpnięte z kodu produkcyjnego, które można sprawdzić.
MiguelMunoz,
20

Powiedziałbym ogólnie, że dobrym pomysłem jest użycie opcjonalnego typu dla zwracanych wartości, które mogą być zerowane. Jednak wrt do frameworków Zakładam, że zastąpienie klasycznych modułów pobierających opcjonalnymi typami spowoduje wiele problemów podczas pracy z ramami (np. Hibernacja), które opierają się na konwencjach kodowania dla modułów pobierających i ustawiających.

Claas Wilke
źródło
14
Ta rada jest dokładnie tym, co miałem na myśli przez „martwimy się o ryzyko gorliwego nadużywania” w stackoverflow.com/a/26328555/3553087 .
Brian Goetz
13

Przyczyną Optionaldodania do Java jest to, że:

return Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
    .stream()
    .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
    .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
    .filter(m -> Objects.equals(m.getReturnType(), returnType))
    .findFirst()
    .getOrThrow(() -> new InternalError(...));

jest czystszy niż to:

Method matching =
    Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
    .stream()
    .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
    .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
    .filter(m -> Objects.equals(m.getReturnType(), returnType))
    .getFirst();
if (matching == null)
  throw new InternalError("Enclosing method not found");
return matching;

Chodzi mi o to, że Opcjonalne zostało napisane w celu obsługi programowania funkcjonalnego , które zostało dodane do Javy w tym samym czasie. (Przykład pochodzi z bloga Briana Goetza . Lepszym przykładem może być orElse()metoda, ponieważ ten kod i tak wygeneruje wyjątek, ale otrzymujesz obraz.)

Ale teraz ludzie używają Opcjonalnego z zupełnie innego powodu. Używają go, aby naprawić lukę w projekcie języka. Wada jest taka: nie ma sposobu, aby określić, który z parametrów API i zwracane wartości mogą mieć wartość null. Może to być wspomniane w javadocs, ale większość programistów nawet nie pisze javadocs dla swojego kodu i niewielu sprawdzi javadocs podczas pisania. Prowadzi to do dużej ilości kodu, który zawsze sprawdza wartości zerowe przed ich użyciem, nawet jeśli często nie mogą być zerowe, ponieważ były już sprawdzane wielokrotnie dziewięć lub dziesięć razy w górę stosu wywołań.

Myślę, że było prawdziwe pragnienie rozwiązania tej wady, ponieważ tak wielu ludzi, którzy widzieli nową klasę Opcjonalną, przyjęło, że jej celem było zwiększenie przejrzystości interfejsów API. Dlatego ludzie zadają pytania typu „czy osoby pobierające powinny zwracać Opcjonalne?” Nie, prawdopodobnie nie powinny, chyba że spodziewasz się użycia gettera w programowaniu funkcjonalnym, co jest bardzo mało prawdopodobne. W rzeczywistości, jeśli spojrzysz na to, gdzie Opcjonalny jest używany w API Java, to głównie w klasach Stream, które są rdzeniem programowania funkcjonalnego. (Nie sprawdziłem bardzo dokładnie, ale klasy Stream mogą być jedynym miejscem, w którym są używane).

Jeśli planujesz używać modułu pobierającego w odrobinie kodu funkcjonalnego, dobrym pomysłem może być standardowy moduł pobierający i drugi, który zwraca Opcjonalne.

Aha, a jeśli chcesz, aby twoja klasa była serializowalna, absolutnie nie powinieneś używać Opcjonalnego.

Opcjonalne są bardzo złym rozwiązaniem wady API, ponieważ a) są bardzo gadatliwe, i b) nigdy nie miały na celu rozwiązania tego problemu.

O wiele lepszym rozwiązaniem usterki API jest moduł sprawdzania nieważności . Jest to procesor adnotacji, który pozwala określić, które parametry i zwracane wartości mogą być zerowe, poprzez opatrzenie ich adnotacją @Nullable. W ten sposób kompilator może zeskanować kod i dowiedzieć się, czy wartość, która może być równa null, jest przekazywana do wartości, w której null jest niedozwolony. Domyślnie zakłada się, że nic nie może mieć wartości null, chyba że jest to opatrzone adnotacjami. W ten sposób nie musisz się martwić o wartości zerowe. Przekazanie wartości zerowej do parametru spowoduje błąd kompilatora. Testowanie obiektu na wartość NULL, która nie może być NULL, powoduje wygenerowanie ostrzeżenia kompilatora. Efektem tego jest zmiana NullPointerException z błędu środowiska wykonawczego na błąd czasu kompilacji.

To zmienia wszystko.

Jeśli chodzi o osoby pobierające, nie używaj opcji opcjonalnej. I spróbuj zaprojektować swoje klasy, aby żaden z członków nie mógł być zerowy. I może spróbuj dodać Nullness Checker do swojego projektu i zadeklarować parametry pobierające i ustawiające @ Nullable, jeśli są potrzebne. Zrobiłem to tylko z nowymi projektami. Prawdopodobnie generuje wiele ostrzeżeń w istniejących projektach napisanych z mnóstwem zbędnych testów na wartość zerową, więc może być trudno go zmodernizować. Ale również złapie wiele błędów. Kocham to. Mój kod jest dzięki temu znacznie bardziej przejrzysty i bardziej niezawodny.

(Istnieje również nowy język, który rozwiązuje ten problem. Kotlin, który kompiluje się do kodu bajtu Java, pozwala określić, czy obiekt może być pusty, gdy go deklarujesz. To czystsze podejście.)

Dodatek do oryginalnego postu (wersja 2)

Po długim zastanowieniu niechętnie doszedłem do wniosku, że zwrot jest możliwy Opcjonalny pod jednym warunkiem: że odzyskana wartość może być zerowa. Widziałem dużo kodu, w którym ludzie rutynowo zwracają Opcjonalne od programów pobierających, które nie mogą zwrócić wartości null. Widzę to jako bardzo złą praktykę kodowania, która tylko zwiększa złożoność kodu, co zwiększa prawdopodobieństwo błędów. Ale gdy zwracana wartość może być równa null, śmiało zapakuj ją w Opcjonalne.

Należy pamiętać, że metody zaprojektowane do programowania funkcjonalnego i wymagające odwołania do funkcji będą (i powinny) zostać napisane w dwóch formach, z których jedna używa opcji opcjonalnej. Na przykład, Optional.map()i Optional.flatMap()oba odniesienia funkcyjne podjąć. Pierwsza odwołuje się do zwykłego gettera, a druga zwraca wartość Opcjonalną. Więc nie robisz nikomu przysługi, zwracając Opcjonalne, gdzie wartość nie może być zerowa.

Powiedziawszy to wszystko, nadal widzę, że podejście stosowane przez Kontroler Nullness jest najlepszym sposobem radzenia sobie z zerami, ponieważ zmieniają wyjątki NullPointerException od błędów środowiska wykonawczego w kompilowanie błędów czasu.

MiguelMunoz
źródło
2
Ta odpowiedź wydaje mi się najbardziej odpowiednia. Opcjonalne zostało dodane tylko w Javie 8, gdy dodano strumienie. I tylko funkcje strumienia zwracają opcjonalne, o ile widziałem.
Archit.
1
Myślę, że to jedna z najlepszych odpowiedzi. Ludzie wiedzą, co jest opcjonalne i jak to działa. Ale najbardziej mylącą częścią jest / było, gdzie go używać i jak go używać. czytając to rozwiewa wiele wątpliwości.
ParagFlume
3

Jeśli używasz nowoczesnych serializatorów i innych frameworków, które rozumieją, Optionalto zauważyłem, że te wytyczne działają dobrze podczas pisania Entityziaren i warstw domen:

  1. Jeśli warstwa serializacji (zwykle DB) zezwala na nullwartość komórki w kolumnie BARw tabeli FOO, to moduł pobierający Foo.getBar()może zwrócić Optionalwskazując programistom, że można oczekiwać, że wartość ta będzie równa null i powinni to obsłużyć. Jeśli DB gwarantuje, że wartość nie będzie null, to moduł pobierający nie powinien zawijać tego w Optional.
  2. Foo.barpowinno być privatei nie być Optional. Naprawdę nie ma powodu, żeby tak Optionalbyło private.
  3. Seter Foo.setBar(String bar)powinien przyjąć typ bari nie Optional . Jeśli możesz użyć nullargumentu, wpisz go w komentarzu JavaDoc. Jeśli to nie jest OK, aby używać nullsię IllegalArgumentExceptionlub jakąś odpowiednią logikę biznesową jest IMHO, bardziej odpowiednie.
  4. Konstruktory nie potrzebują Optionalargumentów (z powodów podobnych do punktu 3). Zasadniczo w konstruktorze uwzględniam tylko argumenty, które muszą być niepuste w bazie danych serializacji.

Aby powyższe bardziej wydajny, może chcesz edytować szablony IDE do generowania pobierające i odpowiadające szablony toString(), equals(Obj o)itd. Lub pól mechanicznych bezpośrednio dla osób (większość generatorów IDE już do czynienia z null).

znak
źródło