Jak efektywniej korzystać z adnotacji @Nullable i @Nonnull?

140

Widzę to @Nullablei @Nonnulladnotacje mogą być pomocne w zapobieganiu NullPointerExceptions, ale nie rozprzestrzeniają się zbyt daleko.

  • Skuteczność tych adnotacji spada całkowicie po jednym poziomie pośredniej, więc jeśli dodasz tylko kilka, nie będą one rozprzestrzeniać się zbyt daleko.
  • Ponieważ te adnotacje nie są dobrze egzekwowane, istnieje niebezpieczeństwo, że wartość oznaczona znakiem @Nonnullnie jest zerowa, a co za tym idzie, nie będzie przeprowadzać kontroli zerowej.

Poniższy kod powoduje, że parametr oznaczony symbolem @Nonnulljest nullbez zgłaszania zastrzeżeń. Rzuca, NullPointerExceptiongdy jest uruchomiony.

public class Clazz {
    public static void main(String[] args){
        Clazz clazz = new Clazz();

        // this line raises a complaint with the IDE (IntelliJ 11)
        clazz.directPathToA(null);

        // this line does not
        clazz.indirectPathToA(null); 
    }

    public void indirectPathToA(Integer y){
        directPathToA(y);
    }

    public void directPathToA(@Nonnull Integer x){
        x.toString(); // do stuff to x        
    }
}

Czy istnieje sposób na bardziej rygorystyczne egzekwowanie i / lub dalsze rozpowszechnianie tych adnotacji?

Mike Rylander
źródło
1
Podoba mi się pomysł @Nullablelub @Nonnull, ale jeśli są tego warte, jest bardzo „prawdopodobne, że zachęcą do debaty”
Maarten Bodewes,
Myślę, że sposobem na przejście do świata, w którym powoduje to błąd kompilatora lub ostrzeżenie, jest wymaganie rzutowania @Nonnullpodczas wywoływania @Nonnullmetody ze zmienną dopuszczającą wartość null. Oczywiście rzutowanie z adnotacją nie jest możliwe w Javie 7, ale Java 8 będzie dodawać możliwość stosowania adnotacji do użycia zmiennej, w tym rzutowania. Więc może to stać się możliwe do zaimplementowania w Javie 8.
Theodore Murdock,
1
@TheodoreMurdock, tak, w Javie 8 rzutowanie (@NonNull Integer) yjest syntaktycznie możliwe, ale kompilator nie może emitować żadnego określonego kodu bajtowego na podstawie adnotacji. W przypadku asercji w czasie wykonywania wystarczające są małe metody pomocnicze, omówione w bugs.eclipse.org/442103 (np. directPathToA(assertNonNull(y))) - ale pamiętaj, to tylko pomaga szybko zawieść. Jedynym bezpiecznym sposobem jest wykonanie rzeczywistego sprawdzenia wartości null (oraz, miejmy nadzieję, alternatywnej implementacji w gałęzi else).
Stephan Herrmann
1
W tym pytaniu pomocne byłoby określenie, o których @Nonnullio @Nullablektórej mówisz, ponieważ istnieje wiele podobnych adnotacji (patrz to pytanie ). Czy mówisz o adnotacjach w pakiecie javax.annotation?
James Dunn
1
@TJamesBoone W kontekście tego pytania nie ma znaczenia, chodziło o to, jak efektywnie wykorzystać którekolwiek z nich.
Mike Rylander

Odpowiedzi:

66

Krótka odpowiedź: myślę, że te adnotacje są przydatne tylko dla twojego IDE, aby ostrzec cię o potencjalnie zerowych błędach wskaźnika.

Jak powiedziano w książce „Wyczyść kod”, powinieneś sprawdzić parametry swojej metody publicznej, a także unikać sprawdzania niezmienników.

Kolejną dobrą wskazówką jest to, że nigdy nie zwraca wartości null, ale zamiast tego używa się wzorca obiektu Null .

Pedro Boechat
źródło
10
Aby wartości zwracane były prawdopodobnie puste, zdecydowanie sugeruję użycie Optionaltypu zamiast zwykłegonull
Patrick
7
Opcjonalne nie jest lepsze niż „null”. Opcjonalne # get () zgłasza wyjątek NoSuchElementException, podczas gdy użycie wartości null zgłasza wyjątek NullPointerException. Oba są wyjątkami RuntimeException bez sensownego opisu. Wolę zmienne dopuszczające wartość null.
30
4
@ 30thh dlaczego miałbyś użyć Optional.get () bezpośrednio, a nie opcjonalne.isPresent () lub Optional.map?
GauravJ
7
@GauravJ Dlaczego miałbyś używać zmiennej dopuszczającej wartość null bezpośrednio i nie sprawdzać, czy najpierw ma wartość null? ;-)
30
5
Różnica między Optionali dopuszcza wartość null w tym przypadku polega na tym, że Optionallepiej informuje, że ta wartość może celowo być pusta. Z pewnością nie jest to magiczna różdżka i w środowisku wykonawczym może zawieść dokładnie tak samo, jak zmienna dopuszczająca wartość null. Jednak Optionalmoim zdaniem odbiór API przez programistę jest lepszy .
user1053510
31

Poza tym, że IDE daje wskazówki, gdy przechodzisz nulldo metod, które oczekują, że argument nie będzie zerowy, istnieją dalsze zalety:

  • Narzędzia do statycznej analizy kodu mogą testować to samo, co IDE (np. FindBugs)
  • Możesz użyć AOP, aby sprawdzić te potwierdzenia

Dzięki temu Twój kod będzie łatwiejszy w utrzymaniu (ponieważ nie potrzebujesz nullsprawdzania) i mniej podatny na błędy.

Uwe Plonus
źródło
9
Współczuję tutaj OP, ponieważ chociaż przytaczasz te dwie zalety, w obu przypadkach użyłeś słowa „może”. Oznacza to, że nie ma gwarancji, że te kontrole rzeczywiście się pojawią. Teraz ta różnica w zachowaniu może być przydatna w testach wrażliwych na wydajność, których chciałbyś uniknąć w trybie produkcyjnym, dla których mamy assert. Znajduję @Nullablei jestem @Nonnullpożytecznymi pomysłami, ale wolałbym więcej siły za nimi, niż stawiać hipotezę, co można z nimi zrobić, co nadal pozostawia otwartą możliwość nic z nimi nie robić.
seh
2
Pytanie, od czego zacząć. W tej chwili jego anntacje są opcjonalne. Czasami chciałbym, żeby tak nie było, ponieważ w pewnych okolicznościach pomocne byłoby ich wyegzekwowanie ...
Uwe Plonus
Czy mogę zapytać, o co tu chodzi AOP?
Chris.Zou
@ Chris.Zou AOP oznacza programowanie zorientowane aspektowo, np. AspectJ
Uwe Plonus
13

Myślę, że to oryginalne pytanie pośrednio wskazuje na ogólne zalecenie, że sprawdzanie wskaźnika zerowego w czasie wykonywania jest nadal potrzebne, mimo że używane jest @NonNull. Skorzystaj z następującego łącza:

Nowe adnotacje typu Java 8

W powyższym blogu zaleca się:

Opcjonalne adnotacje typu nie zastępują sprawdzania poprawności w czasie wykonywania. Przed adnotacjami typu podstawową lokalizacją do opisywania takich elementów, jak wartość null lub zakresy, był plik javadoc. W przypadku adnotacji typu ta komunikacja jest przekazywana do kodu bajtowego w sposób umożliwiający weryfikację w czasie kompilacji. Twój kod powinien nadal przeprowadzać walidację w czasie wykonywania.

jonathanzh
źródło
1
Zrozumiano, ale domyślne testy lint ostrzegają, że testy zerowe w czasie wykonywania są niepotrzebne, co na pierwszy rzut oka wydaje się zniechęcać do tego zalecenia.
swooby
1
@swooby Zwykle ignoruję ostrzeżenia o kłaczkach, jeśli mam pewność, że mój kod jest poprawny. Te ostrzeżenia nie są błędami.
jonathanzh
12

Kompilując oryginalny przykład w Eclipse przy zgodności 1.8 i z włączoną analizą null opartą na adnotacjach, otrzymujemy następujące ostrzeżenie:

    directPathToA(y);
                  ^
Null type safety (type annotations): The expression of type 'Integer' needs unchecked conversion to conform to '@NonNull Integer'

To ostrzeżenie jest sformułowane analogicznie do tych ostrzeżeń, które pojawiają się podczas mieszania wygenerowanego kodu ze starszym kodem przy użyciu typów surowych („konwersja niesprawdzona”). Tutaj mamy dokładnie taką samą sytuację: metoda indirectPathToA()ma "legacy" podpis, ponieważ nie określa żadnego zerowego kontraktu. Narzędzia mogą z łatwością to zgłosić, więc będą ścigać Cię po wszystkich zaułkach, w których należy propagować adnotacje o wartości NULL, ale jeszcze nie jest.

A kiedy używamy sprytnego @NonNullByDefault, nie musimy nawet tego mówić za każdym razem.

Innymi słowy: to, czy adnotacje o wartości zerowej „rozprzestrzeniają się bardzo daleko”, może zależeć od używanego narzędzia i od tego, jak rygorystycznie przestrzegasz wszystkich ostrzeżeń wysyłanych przez to narzędzie. Dzięki adnotacjom o wartości NULL TYPE_USE możesz wreszcie pozwolić narzędziu ostrzec Cię o każdym możliwym NPE w programie, ponieważ wartość zerowa stała się nieodłączną właściwością systemu typów.

Stephan Herrmann
źródło
8

Zgadzam się, że adnotacje „nie rozprzestrzeniają się zbyt daleko”. Widzę jednak błąd po stronie programisty.

NonnullAdnotację rozumiem jako dokumentację. Poniższa metoda wyraża, że ​​wymaga (jako warunku wstępnego) argumentu innego niż null x.

    public void directPathToA(@Nonnull Integer x){
        x.toString(); // do stuff to x        
    }

Poniższy fragment kodu zawiera błąd. Metoda wywołuje directPathToA()bez wymuszania, że yjest różna od null (to znaczy nie gwarantuje warunku wstępnego wywoływanej metody). Jedną z możliwości jest dodanie Nonnulladnotacji również do indirectPathToA()(propagowanie warunku wstępnego). Możliwość druga polega na sprawdzeniu nieważności yin indirectPathToA()i uniknięciu wywołania directPathToA()kiedy yjest zerowe.

    public void indirectPathToA(Integer y){
        directPathToA(y);
    }
Andres
źródło
1
Propagowanie tego, @Nonnull co ma, indirectPathToA(@Nonnull Integer y)jest złą praktyką IMHO: będziesz musiał zachować propagację na pełnym stosie wywołań (więc jeśli dodasz nullcheck in directPathToA(), będziesz musiał zastąpić @Nonnullprzez @Nullablew pełnym stosie wywołań). W przypadku dużych aplikacji oznaczałoby to ogromne nakłady na konserwację.
Julien Kronegg
Adnotacja @Nonnull po prostu podkreśla, że ​​weryfikacja argumentu jest po Twojej stronie (musisz zagwarantować, że przekażesz wartość niezerową). Metoda nie jest odpowiedzialna.
Alexander Drobyshevsky
@Nonnull jest również rozsądną rzeczą, gdy wartość zerowa nie ma sensu dla tej metody
Alexander Drobyshevsky
5

To, co robię w moich projektach, to aktywowanie następującej opcji podczas inspekcji kodu „Stałe warunki i wyjątki”:
Zaproponuj adnotację @Nullable dla metod, które mogą zwracać wartość null i zgłaszać wartości null przekazywane do parametrów bez adnotacji Inspekcje

Po aktywacji wszystkie parametry bez adnotacji będą traktowane jako niezerowe, a zatem zobaczysz również ostrzeżenie dotyczące połączenia pośredniego:

clazz.indirectPathToA(null); 

Dla jeszcze silniejszych testów dobrym wyborem może być Checker Framework (zobacz ten fajny samouczek .
Uwaga : jeszcze go nie używałem i mogą wystąpić problemy z kompilatorem Jacka: zobacz ten raport o błędzie

TmTron
źródło
4

W Javie użyłbym opcjonalnego typu Guava . Będąc rzeczywistym typem, kompilator gwarantuje jego użycie. Łatwo go ominąć i uzyskać NullPointerException, ale przynajmniej podpis metody jasno komunikuje, czego oczekuje jako argument lub co może zwrócić.

Ionuț G. Stan
źródło
16
Musisz z tym uważać. Opcjonalne powinno być używane tylko wtedy, gdy wartość jest naprawdę opcjonalna, a jej brak jest używany jako bramka decyzyjna dla dalszej logiki. Widziałem to nadużywane, gdy ogólne zastępowanie obiektów opcjami i zerowe sprawdzenia zostały zastąpione sprawdzeniami obecności, co mija się z celem.
Christopher Perry,
jeśli celujesz w JDK 8 lub nowszy, wolisz używać java.util.Optionalzamiast klasy Guava. Zobacz notatki / porównanie Guavy, aby uzyskać szczegółowe informacje na temat różnic.
AndrewF
1
„zerowe czeki zastąpione kontrolami obecności, które mija się z celem” czy możesz zatem wyjaśnić, o co chodzi ? Moim zdaniem nie jest to jedyny powód dla Optionals, ale z pewnością jest to zdecydowanie największy i najlepszy.
scubbo
4

Jeśli używasz Kotlin, obsługuje on te adnotacje o wartości zerowej w swoim kompilatorze i zapobiega przekazywaniu wartości null do metody Java, która wymaga argumentu innego niż null. Chociaż to pytanie było pierwotnie skierowane do Javy, wspominam o tej funkcji Kotlin, ponieważ jest ona specjalnie ukierunkowana na te adnotacje Java, a pytanie brzmiało: „Czy istnieje sposób, aby te adnotacje były bardziej rygorystycznie egzekwowane i / lub dalej rozpowszechniane?” i ta funkcja sprawia, że ​​te adnotacje są bardziej rygorystyczne .

Klasa Java za pomocą @NotNulladnotacji

public class MyJavaClazz {
    public void foo(@NotNull String myString) {
        // will result in an NPE if myString is null
        myString.hashCode();
    }
}

Klasa Kotlin wywołująca klasę Javy i przekazująca wartość null dla argumentu z adnotacją @NotNull

class MyKotlinClazz {
    fun foo() {
        MyJavaClazz().foo(null)
    }
}  

Błąd kompilatora Kotlin wymuszający @NotNulladnotację.

Error:(5, 27) Kotlin: Null can not be a value of a non-null type String

zobacz: http://kotlinlang.org/docs/reference/java-interop.html#nullability-annotations

Mike Rylander
źródło
3
Pytanie dotyczy Javy, według jej pierwszego tagu, a nie Kotlina.
seh
1
@seh zobacz aktualizację, aby dowiedzieć się, dlaczego ta odpowiedź jest odpowiednia dla tego pytania.
Mike Rylander,
2
Słusznie. To fajna cecha Kotlina. Po prostu nie sądzę, że to zadowoli tych, którzy przyjeżdżają tutaj, aby uczyć się o Javie.
seh
ale dostęp myString.hashCode()nadal będzie rzucał NPE, nawet jeśli @NotNullnie jest dodany w parametrze. Więc co jest bardziej konkretnego w dodawaniu go?
kAmol
@kAmol Różnica polega na tym, że podczas korzystania z Kotlin otrzymasz błąd czasu kompilacji zamiast błędu czasu wykonania . Adnotacja ma na celu powiadomienie programisty, że musi upewnić się, że wartość null nie zostanie przekazana. Nie zapobiegnie to przekazaniu wartości null w czasie wykonywania, ale uniemożliwi pisanie kodu, który wywołuje tę metodę z wartością null (lub z funkcją, która może zwrócić wartość null).
Mike Rylander
-4

Ponieważ nowa funkcja Java 8 jest opcjonalna , nie należy już używać @Nullable ani @Notnull we własnym kodzie . Weźmy poniższy przykład:

public void printValue(@Nullable myValue) {
    if (myValue != null) {
        System.out.print(myValue);
    } else {
        System.out.print("I dont have a value");
}

Można go przepisać za pomocą:

public void printValue(Optional<String> myValue) {
    if (myValue.ifPresent) {
        System.out.print(myValue.get());
    } else {
        System.out.print("I dont have a value");
}

Użycie opcjonalnego wymusza sprawdzenie wartości null . W powyższym kodzie dostęp do wartości można uzyskać tylko przez wywołanie getmetody.

Kolejną zaletą jest to, że kod staje się bardziej czytelny . Po dodaniu Java 9 ifPresentOrElse funkcję można by nawet zapisać jako:

public void printValue(Optional<String> myValue) {
    myValue.ifPresentOrElse(
        v -> System.out.print(v),
        () -> System.out.print("I dont have a value"),
    )
}
Mathieu Gemard
źródło
Nawet w Optionalprzypadku wciąż istnieje wiele bibliotek i struktur, które używają tych adnotacji, tak że nie jest możliwe zaktualizowanie / zastąpienie wszystkich zależności wersjami zaktualizowanymi do korzystania z opcji. Optionalmoże jednak pomóc w sytuacjach, w których używasz null we własnym kodzie.
Mike Rylander