Najlepszy sposób obsługi wartości zerowych w Javie? [Zamknięte]

21

Mam kod, który zawodzi z powodu NullPointerException. Wywoływana jest metoda na obiekcie, na którym obiekt nie istnieje.

Doprowadziło mnie to jednak do zastanowienia się, jak to naprawić. Czy zawsze koduję defensywnie dla wartości zerowych, aby w przyszłości sprawdzać kod wyjątków wskaźnika zerowego, czy też powinienem naprawić przyczynę wartości zerowej, aby nie wystąpiła ona w dalszym ciągu?

Jakie są Twoje myśli?

Shaun F.
źródło
5
Dlaczego nie miałbyś „naprawić przyczyny zerowej”? Czy możesz podać przykład, dlaczego nie jest to jedyny rozsądny wybór? Najwyraźniej coś musi odciągać cię od oczywistości. Co to jest? Dlaczego naprawienie przyczyny nie jest przyczyną pierwszego wyboru?
S.Lott,
Poprawienie „głównej przyczyny” problemu może pomóc w krótkim okresie, ale jeśli kod nadal nie sprawdza obronnie wartości zerowych i jest ponownie wykorzystywany przez inny proces, który może wprowadzić wartość zerową, musiałbym to naprawić ponownie.
Shaun F
2
@Shaun F: Jeśli kod jest uszkodzony, jest uszkodzony. Nie rozumiem, w jaki sposób kod może generować nieoczekiwane wartości zerowe i nie może zostać naprawiony. Najwyraźniej dzieje się coś „głupiego” lub „politycznego”. Lub coś, co pozwala w jakiś sposób zaakceptować błędny kod. Czy możesz wyjaśnić, w jaki sposób błędny kod nie jest naprawiany? Jakie jest tło polityczne, które skłania Cię do zadania tego pytania?
S.Lott,
1
„podatny na późniejsze tworzenie danych, które zawierały wartości zerowe” Co to w ogóle oznacza? Jeśli „mogę naprawić procedurę tworzenia danych”, nie będzie już żadnej luki w zabezpieczeniach. Nie jestem w stanie zrozumieć, co może oznaczać to „późniejsze tworzenie danych z zerami”? Błędne oprogramowanie?
S.Lott,
1
@ S.Lott, enkapsulacja i pojedyncza odpowiedzialność. Cały Twój kod nie „wie”, co robi Twój drugi kod. Jeśli klasa akceptuje Froobinatorów, przyjmuje jak najmniej założeń o tym, kto stworzył Froobinator i jak. Może pochodzić z kodu „zewnętrznego” lub po prostu pochodzić z innej części podstawy kodu. Nie twierdzę, że nie powinieneś naprawiać błędnego kodu; ale wyszukaj „programowanie obronne”, a zobaczysz, o czym mówi PO.
Paul Draper,

Odpowiedzi:

45

Jeśli null jest rozsądnym parametrem wejściowym dla metody, popraw metodę. Jeśli nie, napraw dzwoniącego. „Rozsądny” to elastyczny termin, dlatego proponuję następujący test: Jak metoda powinna podawać wartość zerową? Jeśli znajdziesz więcej niż jedną możliwą odpowiedź, wartość null nie jest rozsądnym wejściem.

użytkownik 281377
źródło
1
To naprawdę tak proste.
biziclop
3
Guava ma kilka bardzo fajnych metod pomocniczych, dzięki którym zerowanie jest tak proste jak Precondition.checkNotNull(...). Zobacz stackoverflow.com/questions/3022319/…
Moją regułą jest, jeśli to możliwe, inicjowanie wszystkiego do rozsądnych wartości domyślnych i później martwienie się o zerowe wyjątki.
davidk01
Thorbjørn: Więc zastępuje przypadkowy wyjątek NullPointerException celowym NullPointerException? W niektórych przypadkach wcześniejsze sprawdzenie może być przydatne lub nawet konieczne; ale obawiam się, że wiele bezsensownych sprawdzeń zerowych, które dodają niewielką wartość i po prostu powiększają program.
user281377,
6
Jeśli wartość null nie jest rozsądnym parametrem wejściowym dla metody, ale wywołania tej metody mogą przypadkowo przekazać wartość NULL (szczególnie jeśli metoda jest publiczna), możesz również chcieć, aby twoja metoda wyrzuciła IllegalArgumentExceptionwartość NULL. Sygnalizuje to dzwoniącym metody, że błąd znajduje się w ich kodzie (a nie w samej metodzie).
Brian
20

Nie używaj null, użyj Opcjonalnie

Jak wskazałeś, jednym z największych problemów nullw Javie jest to, że można jej używać wszędzie , a przynajmniej we wszystkich typach referencyjnych.

Nie można powiedzieć, że to może być, nulla co nie może być.

Java 8 wprowadza wiele lepiej kierunkowa: Optional.

I przykład z Oracle:

String version = "UNKNOWN";
if(computer != null) {
  Soundcard soundcard = computer.getSoundcard();
  if(soundcard != null) {
    USB usb = soundcard.getUSB();
    if(usb != null) {
      version = usb.getVersion();
    }
  }
}

Jeśli każdy z nich może, ale nie musi, zwrócić wartość udaną, możesz zmienić interfejsy API na Optionals:

String name = computer.flatMap(Computer::getSoundcard)
    .flatMap(Soundcard::getUSB)
    .map(USB::getVersion)
    .orElse("UNKNOWN");

Dzięki jawnemu kodowaniu opcjonalności w typie interfejsy będą znacznie lepsze, a kod będzie czystszy.

Jeśli nie używasz Java 8, możesz zajrzeć na com.google.common.base.OptionalGoogle Guava.

Dobre wyjaśnienie zespołu Guava: https://github.com/google/guava/wiki/UsingAndAvoidingNullExplained

Bardziej ogólne objaśnienie wad do zera, z przykładami z kilku języków: https://www.lucidchart.com/techblog/2015/08/31/the-worst-mistake-of-computer-science/


@Nonnull, @Nullable

Java 8 dodaje te adnotacje, aby pomóc narzędziom do sprawdzania kodu, takim jak IDE, wykrywać problemy. Ich skuteczność jest dość ograniczona.


Sprawdź, kiedy ma to sens

Nie pisz 50% kodu sprawdzającego wartość null, szczególnie jeśli nie ma nic sensownego, by twój kod mógł zrobić z nullwartością.

Z drugiej strony, jeśli nullmożna go użyć i coś znaczyć, należy go użyć.


Ostatecznie nie można oczywiście usunąć nullz Javy. Zdecydowanie zalecam zastępowanie Optionalabstrakcji, gdy tylko jest to możliwe, i sprawdzanie, nullkiedy można zrobić z tym coś rozsądnego.

Paul Draper
źródło
2
Zwykle odpowiedzi na czteroletnie pytania są usuwane z powodu niskiej jakości. Dobra robota, mówiąc o tym, jak Java 8 (która wtedy nie istniała) może rozwiązać problem.
ale nie ma znaczenia, oczywiście, pierwotne pytanie. A co powiedzieć, argument jest rzeczywiście opcjonalny?
jwenting
5
@jwenting Jest to całkowicie związane z pierwotnym pytaniem. Odpowiedź na „Jaki jest najlepszy sposób na wbicie gwoździa śrubokrętem” brzmi: „Użyj młotka zamiast śrubokręta”.
Daenyth,
@Jwenting, myślę, że to omówiłem, ale dla jasności: czy argument nie jest opcjonalny, zwykle nie sprawdzałbym wartości null. Będziesz miał 100 linii zerowania i 40 linii treści. Sprawdź wartość null na bardziej publicznych interfejsach lub gdy wartość null ma sens (tzn. Argument jest opcjonalny).
Paul Draper,
4
@ J-Boss, jak w Javie, byś nie mieć zaznaczoneNullPointerException ? NullPointerExceptionMoże się zdarzyć dosłownie każdym wywołaniu metody instancji w Javie. Miałbyś throws NullPointerExceptionprawie każdą metodę.
Paul Draper,
8

Jest wiele sposobów na poradzenie sobie z tym, pieprzenie kodu if (obj != null) {}nie jest idealne, jest bałaganiarskie, powoduje hałas podczas czytania kodu później podczas cyklu konserwacji i jest podatne na błędy, ponieważ łatwo jest zapomnieć o owijaniu płyty kotła.

Zależy to od tego, czy chcesz, aby kod działał cicho, czy nie. To nullbłąd lub oczekiwany warunek.

Co jest zerowe?

W każdej definicji i przypadku Null oznacza absolutny brak danych. Wartości null w bazach danych oznaczają brak wartości dla tej kolumny. zerowe Stringto nie to samo co puste String, zerowe intto nie to samo, co ZERO w teorii. W praktyce „to zależy”. Puste Stringmoże stanowić dobrą Null Objectimplementację dla klasy String, Integerponieważ zależy to od logiki biznesowej.

Alternatywy to:

  1. Null ObjectWzór. Utwórz instancję swojego obiektu, która reprezentuje nullstan, i zainicjuj wszystkie odwołania do tego typu za pomocą odwołania do Nullimplementacji. Jest to przydatne w przypadku prostych obiektów typu wartości, które nie mają wielu odniesień do innych obiektów, które również mogą być nulli powinny być nullw stanie poprawnym.

  2. Użyj narzędzi zorientowanych na aspekt, aby utkać metody o Null Checkeraspekcie, który zapobiega zerowaniu parametrów. Dotyczy to przypadków, w których nullwystępuje błąd.

  3. Używaj assert()niewiele lepiej niż if (obj != null){}mniej hałasu.

  4. Użyj narzędzia do egzekwowania umów, takiego jak Kontrakty dla Java . Taki sam przypadek użycia jak w przypadku AspectJ, ale nowszy i wykorzystuje Adnotacje zamiast zewnętrznych plików konfiguracyjnych. Najlepsze z obu dzieł Aspektów i Asertów.

1 jest idealnym rozwiązaniem, gdy wiadomo, że przychodzą dane nulli należy je zastąpić pewną wartością domyślną, aby odbiorcy nie musieli zajmować się całym kodem kontrolnym zerowania. Sprawdzanie ze znanymi wartościami domyślnymi również będzie bardziej wyraziste.

2, 3 i 4 są po prostu wygodnymi alternatywnymi generatorami wyjątków do zastąpienia NullPointerException czymś bardziej pouczającym, co zawsze stanowi ulepszenie.

Na końcu

nullw Javie prawie we wszystkich przypadkach występuje błąd logiczny. Zawsze powinieneś dążyć do wyeliminowania pierwotnej przyczyny NullPointerExceptions. Powinieneś starać się nie używać nullwarunków jako logiki biznesowej. if (x == null) { i = someDefault; }po prostu dokonaj wstępnego przypisania do tej domyślnej instancji obiektu.


źródło
Jeśli to możliwe, sposób na wzór zerowy jest najkorzystniejszym sposobem obsługi wielkości zerowej. Rzadko zdarza się, że chcesz, aby wartość zerowa spowodowała wysadzenie rzeczy w produkcji!
Richard Miskin
@ Richard: jeśli nie nulljest to nieoczekiwane, to jest to prawdziwy błąd, wtedy wszystko powinno się całkowicie zatrzymać.
Null w bazach danych i kodzie programowym jest traktowany inaczej w jednym ważnym aspekcie: w programowaniu null == null(nawet w PHP), ale w bazach danych, null != nullponieważ w bazach danych reprezentuje nieznaną wartość, a nie „nic”. Dwie niewiadome niekoniecznie są równe, podczas gdy dwa nic nie są równe.
Simon Forsberg
8

Dodanie kontroli zerowej może sprawić, że testowanie będzie problematyczne. Zobacz ten wspaniały wykład ...

Sprawdź wykład Google Tech: „Czysty kod mówi - nie szukaj rzeczy!” mówi o tym około 24 minuty

http://www.youtube.com/watch?v=RlfLCWKxHJ0&list=PL693EFD059797C21E

Programowanie paranoiczne polega na dodawaniu wszędzie zerowych kontroli. Na początku wydaje się to dobrym pomysłem, ale z perspektywy testowania sprawia, że ​​testowanie typu zerowania jest trudne.

Ponadto po utworzeniu warunku wstępnego istnienia jakiegoś obiektu, takiego jak

class House(Door door){

    .. null check here and throw exception if Door is NULL
    this.door = door
}

uniemożliwia ci tworzenie Domu, ponieważ będziesz rzucał wyjątek. Załóżmy, że twoje przypadki testowe tworzą fałszywe obiekty do testowania czegoś innego niż drzwi, cóż, nie możesz tego zrobić, ponieważ drzwi są wymagane

Ci, którzy cierpieli z powodu fałszywego stworzenia, są świadomi tego rodzaju irytacji.

Podsumowując, zestaw testowy powinien być wystarczająco solidny, aby testować drzwi, domy, dachy itp., Bez konieczności paranoikowania. Serio, jak trudno jest dodać test zerowy dla określonych obiektów w testach :)

Zawsze powinieneś preferować aplikacje, które działają, ponieważ masz kilka testów, które POTWIERDZAJĄ, że to działa, zamiast NADZIEI, że działa po prostu dlatego, że masz całą masę wstępnych kontroli zerowych w całym miejscu

Constantin
źródło
4

tl; dr - DOBRE jest sprawdzanie nieoczekiwanych nulls, ale ZŁE dla aplikacji, która próbuje je naprawić.

Detale

Oczywiste jest, że istnieją sytuacje, w których nullpoprawne dane wejściowe lub wyjściowe są przekazywane do metody, i inne, w których tak nie jest.

Zasada nr 1:

Jawadoc dla metody, która zezwala na nullparametr lub zwraca nullwartość, musi to wyraźnie udokumentować i wyjaśnić, co to nullznaczy.

Reguła nr 2:

Metodę API nie należy określać jako przyjmującą lub zwracającą, nullchyba że istnieje ku temu dobry powód.

Biorąc pod uwagę wyraźne określenie „umowy” metoda vis-a-vis nulls, jest to błąd programowania przejść lub zwróci nullgdzie nie powinny.

Zasada nr 3:

Aplikacja nie powinna próbować „robić dobrych” błędów programistycznych.

Jeśli metoda wykryje, nullże nie powinno jej tam być, nie powinna próbować rozwiązać problemu, zamieniając ją w coś innego. To po prostu ukrywa problem przed programistą. Zamiast tego powinno pozwolić na wystąpienie NPE i spowodować awarię, aby programiści mogli dowiedzieć się, co jest główną przyczyną i naprawić go. Mam nadzieję, że awaria zostanie zauważona podczas testów. Jeśli nie, to mówi coś o twojej metodyce testowania.

Reguła # 4:

Tam, gdzie to możliwe, napisz kod, aby wcześnie wykryć błędy programistyczne.

Jeśli masz w kodzie błędy, które powodują wiele NPE, najtrudniejsze może być ustalenie, skąd pochodzą te nullwartości. Jednym ze sposobów ułatwienia diagnozy jest napisanie kodu, aby został nullon wykryty jak najszybciej. Często możesz to zrobić w połączeniu z innymi czekami; na przykład

public setName(String name) {
    // This also detects `null` as an (intended) side-effect
    if (name.length() == 0) {
        throw new IllegalArgumentException("empty name");
    }
}

(Są oczywiście przypadki, w których reguły 3 i 4 powinny zostać zahartowane w rzeczywistości. Na przykład (reguła 3) niektóre rodzaje aplikacji muszą próbować kontynuować po wykryciu prawdopodobnie błędów programowania. I (reguła 4) zbyt wiele sprawdzania złych parametrów może mieć wpływ na wydajność).

Stephen C.
źródło
3

Poleciłbym ustalenie metody na defensywną. Na przykład:

String go(String s){  
    return s.toString();  
}

Powinny być bardziej zgodne z tym:

String go(String s){  
    if(s == null){  
       return "";  
    }     
    return s.toString();  
}

Zdaję sobie sprawę, że jest to absolutnie trywialne, ale jeśli wywołujący oczekuje, że obiekt da mu domyślny obiekt, który nie spowoduje przekazania wartości null.

Woot4Moo
źródło
10
Ma to nieprzyjemny efekt uboczny, ponieważ program wywołujący (programista, który koduje ten interfejs API) może rozwinąć nawyk przekazywania wartości null jako parametru, aby uzyskać zachowanie „domyślne”.
Goran Jovic,
2
Jeśli jest to Java, nie powinieneś w ogóle używać new String().
biziclop
1
@biziclop w rzeczywistości jest to o wiele ważniejsze niż większość zdaje sobie sprawę.
Woot4Moo,
1
Moim zdaniem bardziej sensowne jest umieszczenie twierdzenia i wyrzucenie wyjątku, jeśli argument jest zerowy, ponieważ jest to oczywiście błąd osoby dzwoniącej. Nie dyskretnie „naprawiaj” błędów dzwoniącego; zamiast tego, uświadom je im.
Andres F.,
1
@ Woot4Moo Napisz więc swój własny kod, który zgłasza wyjątek. Ważne jest, aby poinformować osobę dzwoniącą, że jej przekazane argumenty są błędne, i poinformować ją jak najszybciej. Ciche korygowanie nulls jest najgorszą możliwą opcją, gorszą niż rzucanie NPE.
Andres F.,
2

Do tej pory bardzo pomogły mi następujące ogólne zasady dotyczące wartości null:

  1. Jeśli dane pochodzą spoza twojej kontroli, systematycznie sprawdzaj wartości zerowe i działaj odpowiednio. Oznacza to albo zgłoszenie wyjątku, który ma sens dla funkcji (zaznaczone lub niezaznaczone, po prostu upewnij się, że nazwa wyjątku mówi ci dokładnie, co się dzieje). Ale NIGDY nie pozwól, aby w twoim systemie spadła jakaś wartość, która mogłaby potencjalnie przynieść niespodzianki.

  2. Jeśli wartość Null znajduje się w domenie odpowiednich wartości dla twojego modelu danych, postępuj z nią odpowiednio.

  3. Zwracając wartości, staraj się nie zwracać wartości null, gdy tylko jest to możliwe. Zawsze preferuj Puste listy, puste ciągi znaków, wzorce zerowych obiektów. Zachowaj wartości null jako zwracane wartości, gdy jest to najlepsza możliwa reprezentacja danych dla danego przypadku użycia.

  4. Prawdopodobnie najważniejszy ze wszystkich ... Testy, testy i testy ponownie. Podczas testowania kodu nie testuj go jako kodera, testuj go jako nazistowską dominację psychopaty i spróbuj wyobrazić sobie różne sposoby torturowania tego kodu.

Ma to nieco paranoiczną stronę w odniesieniu do zer, które często prowadzą do elewacji i serwerów pośredniczących, które łączą systemy ze światem zewnętrznym i ściśle kontrolowanych wartości wewnątrz z dużą obfitością redundancji. Świat zewnętrzny tutaj oznacza prawie wszystko, czego sam nie kodowałem. Ma czas działania, ale jak dotąd rzadko musiałem to optymalizować, tworząc „bezpieczne sekcje” o zerowym kodzie. Muszę jednak powiedzieć, że głównie tworzę systemy działające długo dla służby zdrowia, a ostatnią rzeczą, jakiej chcę, to podsystem interfejsu przenoszący twoje alergie na jod do skanera CT ulega awarii z powodu nieoczekiwanego wskaźnika zerowego, ponieważ ktoś w innym systemie nigdy nie zdawał sobie sprawy, że nazwy mogą zawierają apostrofy lub znaki takie jak 但 耒耨。

w każdym razie .... moje 2 centy

Newtopian
źródło
1

Proponuję użycie wzorca Option / Some / None z języków funkcjonalnych. Nie jestem specjalistą od Java, ale intensywnie wykorzystuję własną implementację tego wzorca w moim projekcie C # i jestem pewien, że można go przekonwertować na świat Java.

Ideą tego wzorca jest: jeśli logicznie jest sytuacja, w której istnieje możliwość nieobecności wartości (na przykład podczas odzyskiwania z bazy danych według identyfikatora), podajesz obiekt typu Opcja [T], gdzie T jest możliwą wartością . I przypadek nieobecności obiektu wartości klasy None [T] zwrócił, w przypadku gdy istnienie wartości - obiekt Some [T] zwrócił, zawierający wartość.

W takim przypadku musisz poradzić sobie z możliwością braku wartości, a jeśli zrobisz przegląd kodu, łatwo znajdziesz miejsce niepoprawnej obsługi. Aby uzyskać inspirację z implementacji języka C #, zapoznaj się z moim repozytorium bitbucket https://bitbucket.org/mikegirkin/optionsomenone

Jeśli zwrócisz wartość zerową, która jest logicznie równoważna z błędem (na przykład żaden plik nie istnieje lub nie można się połączyć), powinieneś zgłosić wyjątek lub użyć innego wzorca obsługi błędów. Pomysł, który za tym stoi, kończy się rozwiązaniem, gdy trzeba poradzić sobie z sytuacją braku wartości i łatwo można znaleźć w kodzie miejsca nieprawidłowej obsługi.

Hedin
źródło
0

Cóż, jeśli jednym z możliwych wyników twojej metody jest wartość null, powinieneś dla tego kodować defensywnie, ale jeśli metoda ma zwracać wartość inną niż null, ale nie jest, na pewno to naprawię.

Jak zwykle zależy to od przypadku, jak w większości rzeczy w życiu :)

jonezy
źródło
0

Biblioteki języka Apache Commons zapewniają obsługę wartości zerowych

metoda defaultIfNull w klasie ObjectUtils umożliwia zwrócenie wartości domyślnej, jeśli przekazywany obiekt ma wartość null

Mahmoud Hossam
źródło
0
  1. Nie używaj typu Opcjonalny, chyba że naprawdę jest opcjonalny, często wyjście jest lepiej traktowane jako wyjątek, chyba że naprawdę spodziewałeś się wartości null jako opcji, a nie dlatego, że regularnie piszesz błędny kod.

  2. Jak wskazuje artykuł Google, problem nie jest zerowy jako typ, który ma swoje zastosowania. Problem polega na tym, że wartości zerowe powinny być sprawdzane i obsługiwane, często można je z wdziękiem wychodzić.

  3. Istnieje wiele przypadków zerowych, które reprezentują nieprawidłowe warunki w obszarach poza rutynowym działaniem programu (nieprawidłowe wprowadzanie danych przez użytkownika, problemy z bazą danych, awarie sieci, brakujące pliki, uszkodzone dane), i do tego służą sprawdzone wyjątki, obsługa ich, a nawet jeśli to tylko się zalogować.

  4. Obsługa wyjątków jest dozwolona dla różnych priorytetów operacyjnych i uprawnień w JVM w przeciwieństwie do typu, takiego jak Opcjonalny, który ma sens ze względu na wyjątkowy charakter ich wystąpienia, w tym wczesne wsparcie dla priorytetowego leniwego ładowania procedury obsługi do pamięci, ponieważ nie trzeba go cały czas kręcić.

  5. Nie musisz pisać wszędzie zerowych modułów obsługi, tylko tam, gdzie są one prawdopodobne, wszędzie tam, gdzie potencjalnie uzyskujesz dostęp do niewiarygodnej usługi danych, a ponieważ większość z nich dzieli się na kilka ogólnych wzorców, które można łatwo wyodrębnić, naprawdę potrzebujesz połączenie z przewodnikiem, z wyjątkiem rzadkich przypadków.

Sądzę więc, że moją odpowiedzią byłoby zawinięcie jej w sprawdzony wyjątek i obsłużenie go lub naprawienie niewiarygodnego kodu, jeśli jest on w twoich możliwościach.

J-Boss
źródło