Dlaczego warto korzystać z Objects.requireNonNull ()?


214

Zauważyłem, że wiele metod Java 8 w Oracle JDK używa Objects.requireNonNull(), które wewnętrznie wyrzuca, NullPointerExceptionjeśli dany obiekt (argument) jest null.

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

Ale i NullPointerExceptiontak zostanie wyrzucony, jeśli nullobiekt zostanie zaniedbany. Dlaczego więc należy wykonać tę dodatkową kontrolę zerową i rzucić NullPointerException?

Jedną oczywistą odpowiedzią (lub korzyścią) jest to, że czyni kod bardziej czytelnym i zgadzam się. Chciałbym poznać wszelkie inne powody używania Objects.requireNonNull()na początku metody.




1
Takie podejście do sprawdzania argumentów pozwala oszukiwać podczas pisania testów jednostkowych. Spring ma również takie narzędzia (patrz docs.spring.io/spring/docs/current/javadoc-api/org/… ). Jeśli masz „if” i chcesz mieć wysoki zasięg testu jednostkowego, musisz objąć obie gałęzie: kiedy warunek jest spełniony, a kiedy warunek nie jest spełniony. Jeśli używasz Objects.requireNonNull, twój kod nie ma rozgałęzień, więc pojedynczy test jednostkowy zapewni 100% pokrycia :-)
Xorcus

Odpowiedzi:


214

Ponieważ robiąc to, możesz wyrazić to jasno . Lubić:

public class Foo {
  private final Bar bar;

  public Foo(Bar bar) {
    Objects.requireNonNull(bar, "bar must not be null");
    this.bar = bar;
  }

Lub krócej:

  this.bar = Objects.requireNonNull(bar, "bar must not be null");

Teraz wiesz :

  • kiedy obiekt Foo został pomyślnie utworzony przy użyciunew()
  • następnie jego bar pole jest gwarantowana nie być null.

Porównaj to z: dzisiaj tworzysz obiekt Foo, a jutro wywołujesz metodę, która używa tego pola i rzuca. Najprawdopodobniej jutro nie dowiesz się, dlaczego to odniesienie było wczoraj puste, kiedy zostało przekazane konstruktorowi!

Innymi słowy: używając jawnie tej metody do sprawdzania przychodzących referencji, możesz kontrolować moment, w którym wyjątek zostanie zgłoszony. I przez większość czasu chcesz zawieść tak szybko, jak to możliwe !

Główne zalety to:

  • jak powiedziane, kontrolowane zachowanie
  • łatwiejsze debugowanie - ponieważ wymiotujesz w kontekście tworzenia obiektu. W momencie, gdy masz pewną szansę, że twoje logi / ślady mówią ci, co poszło nie tak!
  • i jak pokazano powyżej: prawdziwa siła tego pomysłu ujawnia się w połączeniu z ostatecznymi polami. Ponieważ teraz każdy inny kod w twojej klasie może bezpiecznie założyć, że barnie ma wartości zerowej - a zatem nie potrzebujesz żadnych if (bar == null)kontroli w innych miejscach!

23
Możesz uczynić swój kod bardziej zwartym, piszącthis.bar = Objects.requireNonNull(bar, "bar must not be null");
Kirill Rakhman

3
@KirillRakhman Nauczanie GhostCat czegoś fajnego, o czym nie wiedziałem -> wygrania biletu w loterii uprzywilejowanej GhostCat.
GhostCat

1
Myślę, że komentarz nr 1 tutaj jest faktyczną zaletą Objects.requireNonNull(bar, "bar must not be null");. Dzięki za to.
Kawu

1
Który był pierwszy i dlaczego ludzie „języka” powinni podążać za autorami jakiejś biblioteki ?! Dlaczego guawa nie podąża za zachowaniem języka Java ?!
GhostCat

1
Jest this.bar = Objects.requireNonNull(bar, "bar must not be null");to poprawne w konstruktorach, ale potencjalnie niebezpieczne w innych metodach, jeśli dwie lub więcej zmiennych jest ustawionych w tej samej metodzie. Przykład: talkwards.com/2018/11/03/…
Erk

100

Niezawodny

Kod powinien ulec awarii tak szybko, jak to możliwe. Nie powinno to wykonywać połowy pracy, a następnie oderwać się od wartości zerowej i powodować awarię tylko wtedy, gdy połowa wykonanej pracy powoduje, że system jest w nieprawidłowym stanie.

Jest to powszechnie nazywane „wczesnym niepowodzeniem” lub „szybkim działaniem awaryjnym” .


40

Ale wyjątek NullPointerException i tak zostanie zgłoszony, jeśli obiekt zerowy zostanie odwołany. Dlaczego więc należy zrobić to dodatkowe sprawdzanie wartości zerowej i zgłosić wyjątek NullPointerException?

Oznacza to, że natychmiast i niezawodnie wykryjesz problem .

Rozważać:

  • Odniesienia można użyć dopiero później w metodzie, po tym, jak kod wykonał już jakieś skutki uboczne
  • Odniesienie może w ogóle nie być dereferencyjne w tej metodzie
    • Może być przekazany do zupełnie innego kodu (tzn. Przyczyna i błąd są daleko od siebie oddzielone w przestrzeni kodu)
    • Można go użyć znacznie później (tj. Przyczyna i błąd są daleko od siebie w czasie)
  • Można go użyć gdzieś, gdzie odwołanie zerowe jest prawidłowe, ale ma niezamierzony efekt

.NET poprawia to poprzez oddzielenie NullReferenceException(„wyrejestrowałeś wartość zerową”) od ArgumentNullException(„nie powinieneś podawać wartości null jako argumentu - i tak było w przypadku tego parametru). Chciałbym, aby Java zrobiła to samo, ale nawet z tylko NullPointerException, że to nadal dużo łatwiejsze do kodu poprawek, jeżeli zostanie zgłoszony błąd w najbliższym punkcie, w którym może zostać wykryte.


12

Użycie requireNonNull()jako pierwszych instrukcji w metodzie pozwala na natychmiastowe zidentyfikowanie przyczyny wyjątku.
Stacktrace wyraźnie wskazuje, że wyjątek został zgłoszony natychmiast po wprowadzeniu metody, ponieważ program wywołujący nie przestrzegał wymagań / kontraktu. Przekazanie nullobiektu do innej metody może rzeczywiście wywołać wyjątek na raz, ale przyczyna problemu może być bardziej skomplikowana do zrozumienia, ponieważ wyjątek zostanie zgłoszony w konkretnym wywołaniu nullobiektu, które może być znacznie dalej.


Oto konkretny i prawdziwy przykład, który pokazuje, dlaczego ogólnie musimy sprzyjać szybkiemu niepowodzeniu, a w szczególności za pomocą Object.requireNonNull()lub w jakikolwiek sposób wykonać kontrolę zerową parametrów zaprojektowanych tak, by nie były null.

Załóżmy, że Dictionaryklasa, która komponuje LookupServicei Listod Stringreprezentujący słów zawartych w. Pola te są zaprojektowane, aby nie być null, a jednym z nich jest przekazywana w Dictionary konstruktorze.

Załóżmy teraz, że „zła” implementacja Dictionarybez nullsprawdzania we wpisie metody (tutaj jest to konstruktor):

public class Dictionary {

    private final List<String> words;
    private final LookupService lookupService;

    public Dictionary(List<String> words) {
        this.words = this.words;
        this.lookupService = new LookupService(words);
    }

    public boolean isFirstElement(String userData) {
        return lookupService.isFirstElement(userData);
    }        
}


public class LookupService {

    List<String> words;

    public LookupService(List<String> words) {
        this.words = words;
    }

    public boolean isFirstElement(String userData) {
        return words.get(0).contains(userData);
    }
}

Teraz wywołajmy Dictionarykonstruktor z nullreferencją do wordsparametru:

Dictionary dictionary = new Dictionary(null); 

// exception thrown lately : only in the next statement
boolean isFirstElement = dictionary.isFirstElement("anyThing");

JVM rzuca NPE na tę instrukcję:

return words.get(0).contains(userData); 
Wyjątek w wątku „main” java.lang.NullPointerException
    w LookupService.isFirstElement (LookupService.java:5)
    at Dictionary.isFirstElement (Dictionary.java:15)
    at Dictionary.main (Dictionary.java:22)

Wyjątek jest wyzwalany w LookupServiceklasie, podczas gdy jego pochodzenie jest znacznie wcześniej ( Dictionarykonstruktor). To sprawia, że ​​ogólna analiza problemu jest znacznie mniej oczywista.
Jest words null? Jest words.get(0) null? Obie ? Dlaczego jedno, drugie, a może oba są null? Czy to błąd kodowania w Dictionary(wywoływana metoda konstruktora?)? Czy to błąd kodowania LookupService? (konstruktor? wywołana metoda?)?
Wreszcie będziemy musieli sprawdzić więcej kodu, aby znaleźć źródło błędu, aw bardziej złożonej klasie może nawet użyć debuggera, aby łatwiej zrozumieć, co się stało.
Ale dlaczego prosta rzecz (brak kontroli zerowej) staje się złożonym problemem?
Ponieważ pozwoliliśmy na identyfikację początkowego błędu / braku określonego przecieku na niższych komponentach.
Wyobraź sobie, że LookupServicenie była to usługa lokalna, ale usługa zdalna lub biblioteka strony trzeciej z kilkoma informacjami debugowania lub wyobraź sobie, że nie masz 2 warstw, ale 4 lub 5 warstw wywołań obiektów przed nullwykryciem? Problem byłby jeszcze bardziej złożony do analizy.

Sposobem na faworyzowanie jest:

public Dictionary(List<String> words) {
    this.words = Objects.requireNonNull(words);
    this.lookupService = new LookupService(words);
}

W ten sposób nie ma bólu głowy: wyjątek zostaje zgłoszony natychmiast po otrzymaniu:

// exception thrown early : in the constructor 
Dictionary dictionary = new Dictionary(null);

// we never arrive here
boolean isFirstElement = dictionary.isFirstElement("anyThing");
Wyjątek w wątku „main” java.lang.NullPointerException
    at java.util.Objects.requireNonNull (Objects.java:203)
    w com.Dictionary. (Dictionary.java:15)
    at com.Dictionary.main (Dictionary.java:24)

Zauważ, że tutaj zilustrowałem problem z konstruktorem, ale wywołanie metody może mieć takie samo ograniczenie kontroli innej niż null.


Łał! Świetne wyjaśnienie.
Jonathas Nascimento

2

Na marginesie, to szybko się nie udawało, zanim Object#requireNotNullzostało wdrożone nieco inaczej przed java-9 w niektórych klasach jre. Załóżmy, że przypadek:

 Consumer<String> consumer = System.out::println;

W java-8 kompiluje się jako (tylko odpowiednie części)

getstatic Field java/lang/System.out
invokevirtual java/lang/Object.getClass

Zasadniczo operacja yourReference.getClasstaka jak: - która zakończy się niepowodzeniem, jeśli twoja wartość referencyjna to null.

W jdk-9 wszystko się zmieniło, gdzie kompiluje się ten sam kod, co

getstatic Field java/lang/System.out
invokestatic java/util/Objects.requireNonNull

Lub w zasadzie Objects.requireNotNull (yourReference)


1

Podstawowym zastosowaniem jest NullPointerExceptionnatychmiastowe sprawdzanie i rzucanie .

Lepszą alternatywą (skrótem) do spełnienia tego samego wymagania jest adnotacja @NonNull autorstwa lombok.


1
Adnotacja nie zastępuje metody Objects, działają one razem. Nie możesz powiedzieć @ NonNull x = możeBeNull, powiedziałbyś @ NonNull x = Objects.requireNonNull (mayBeNull, „niepojęty!”);
Bill K

@BillK przepraszam, nie dostaję
Supun Wijerathne

Właśnie mówiłem, że adnotacja Nonnull działa z wymagaNonNull, nie jest to alternatywa, ale działają całkiem dobrze.
Bill K

wdrożyć naturę szybkiego działania, czy jest to alternatywa, prawda? Tak, zgadzam się, że nie jest to alternatywa dla zadania.
Supun Wijerathne,

Myślę, że nazwałbym „replaceNull ()„ konwersją ”z @ Nullable na @ NonNull. Jeśli nie użyjesz adnotacji, metoda nie jest tak naprawdę bardzo interesująca (ponieważ wystarczy rzucić NPE dokładnie tak, jak chroniłby to kod, który chroni) - choć dość wyraźnie pokazuje twoją intencję.
Bill K

1

Wyjątek wskaźnika zerowego jest zgłaszany, gdy uzyskujesz dostęp do elementu obiektu, który znajduje się nullw późniejszym momencie. Objects.requireNonNull()natychmiast sprawdza wartość i natychmiast rzuca wyjątek, nie poruszając się do przodu.


0

Myślę, że powinien być używany w konstruktorach kopiowania i niektórych innych przypadkach, takich jak DI, których parametr wejściowy jest obiektem, powinieneś sprawdzić, czy parametr jest pusty. W takich okolicznościach można wygodnie korzystać z tej metody statycznej.


0

W kontekście rozszerzeń kompilatora, które implementują sprawdzanie Nullability (np .: uber / NullAway ), Objects.requireNonNullnależy używać go nieco oszczędnie w sytuacjach, gdy masz pole zerowe, o którym wiesz, że w pewnym momencie kodu nie ma wartości null.

W ten sposób istnieją dwa główne zastosowania:

  • Uprawomocnienie

    • już ujęte w innych odpowiedziach tutaj
    • kontrola czasu działania z narzutem i potencjalnym NPE
  • Oznaczenie wartości zerowej (zmiana z @Nullable na @Nonnull)

    • minimalne użycie sprawdzania czasu wykonywania na rzecz sprawdzania czasu kompilacji
    • działa tylko wtedy, gdy adnotacje są poprawne (wymuszone przez kompilator)

Przykładowe użycie oznakowania dopuszczającego wartość zerową:

@Nullable
Foo getFoo(boolean getNull) { return getNull ? null : new Foo(); }

// Changes contract from Nullable to Nonnull without compiler error
@Nonnull Foo myFoo = Objects.requireNonNull(getFoo(false));

0

Oprócz wszystkich poprawnych odpowiedzi:

Używamy go w strumieniach reaktywnych. Zazwyczaj powstałe NullpointerExceptions są zawijane w inne wyjątki w zależności od ich wystąpienia w strumieniu. Dlatego możemy później łatwo zdecydować, jak obsłużyć błąd.

Tylko przykład: Wyobraź sobie, że masz

<T> T parseAndValidate(String payload) throws ParsingException { ... };
<T> T save(T t) throws DBAccessException { ... };

gdzie parseAndValidateWrapps NullPointerExceptionod requireNonNullw sposób ParsingException.

Teraz możesz zdecydować, np. Kiedy spróbować ponownie, czy nie:

...
.map(this::parseAndValidate)
.map(this::save)
.retry(Retry.<T>allBut(ParsingException.class))

Bez tej kontroli w metodzie wystąpi wyjątek NullPointerException save, co spowoduje niekończące się próby. Warto nawet wyobrazić sobie długą subskrypcję, dodając

.onErrorContinue(
    throwable -> throwable.getClass().equals(ParsingException.class),
    parsingExceptionConsumer()
)

Teraz RetryExhaustExceptionzabije twoją subskrypcję.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.