Szukaj…


Uwagi

Wartość null jest wartością domyślną niezainicjowanej wartości pola, którego typ jest typem referencyjnym.

NullPointerException (lub NPE) to wyjątek NullPointerException podczas próby wykonania niewłaściwej operacji na odwołaniu do obiektu o null . Takie operacje obejmują:

  • wywołanie metody instancji na null obiekcie docelowym,
  • dostęp do pola null obiektu docelowego,
  • próba indeksowania null obiektu tablicowego lub uzyskania dostępu do jego długości,
  • używając odwołania do obiektu null jako muteksu w synchronized bloku,
  • rzutowanie odwołania do obiektu null ,
  • rozpakowanie odwołania do null obiektu oraz
  • rzucanie odwołania do obiektu null .

Najczęstsze przyczyny pierwotne NPE:

  • zapomnienie zainicjować pole z typem referencyjnym,
  • zapomnienie zainicjować elementy tablicy typu referencyjnego, lub
  • nie testowanie wyników niektórych metod API, które w określonych okolicznościach są zwracane jako null .

Przykłady często używanych metod, które zwracają null obejmują:

  • Metoda get(key) w interfejsie API Map zwróci null jeśli wywołasz ją za pomocą klucza, który nie ma mapowania.
  • Metody getResource(path) i getResourceAsStream(path) w interfejsach API ClassLoader i Class zwracają null jeśli nie można znaleźć zasobu.
  • Metoda get() w Reference interfejsie API zwróci null jeśli moduł czyszczący usunie odwołanie.
  • Różne metody getXxxx w interfejsach API serwletów Java EE zwracają null jeśli spróbujesz pobrać nieistniejący parametr żądania, atrybut sesji lub sesji i tak dalej.

Istnieją strategie unikania niechcianych NPE, takie jak jawne testowanie na null lub używanie „Notacji Yoda”, ale strategie te często powodują niepożądany efekt ukrywania problemów w kodzie, który naprawdę powinien zostać naprawiony.

Pitfall - Niepotrzebne użycie pierwotnych opakowań może prowadzić do wyjątków NullPointerExceptions

Czasami programiści, którzy są nowymi Javami, będą używać zamiennie typów pierwotnych i opakowań. Może to prowadzić do problemów. Rozważ ten przykład:

public class MyRecord {
    public int a, b;
    public Integer c, d;
}

...
MyRecord record = new MyRecord();
record.a = 1;               // OK
record.b = record.b + 1;    // OK
record.c = 1;               // OK
record.d = record.d + 1;    // throws a NullPointerException

Nasza klasa 1 MyRecord polega na domyślnej inicjalizacji w celu inicjalizacji wartości w swoich polach. Tak więc, gdy mamy new rekord, gdy a i b pola zostaną wyzerowane, a c i d pola zostanie ustawiona na null .

Kiedy próbujemy użyć domyślnych zainicjowanych pól, widzimy, że pola int działają cały czas, ale pola Integer działają w niektórych przypadkach, a nie w innych. W szczególności w przypadku, gdy się nie powiedzie (za pomocą d ), zdarza się, że wyrażenie po prawej stronie próbuje rozpakować odwołanie null , i to powoduje NullPointerException .

Jest na to kilka sposobów:

  • Jeśli pola c i d konieczności być prymitywne owijarki, wówczas nie powinniśmy polegać na domyślnym inicjalizacji, albo powinny być badania dla null . Dla pierwszego jest to poprawne podejście, chyba że istnieje określone znaczenie dla pól w stanie null .

  • Jeśli pola nie muszą być prymitywnymi opakowaniami, błędem jest uczynienie ich prymitywnymi opakowaniami. Oprócz tego problemu pierwotne opakowania mają dodatkowe koszty ogólne w stosunku do typów pierwotnych.

Lekcja polega na tym, aby nie używać prymitywnych typów opakowań, chyba że naprawdę jest to konieczne.


1 - Ta klasa nie jest przykładem dobrej praktyki kodowania. Na przykład dobrze zaprojektowana klasa nie miałaby pól publicznych. Jednak nie o to chodzi w tym przykładzie.

Pitfall - użycie wartości null do przedstawienia pustej tablicy lub kolekcji

Niektórzy programiści uważają, że dobrym pomysłem jest oszczędzanie miejsca za pomocą null do przedstawienia pustej tablicy lub kolekcji. Chociaż prawdą jest, że można zaoszczędzić niewielką ilość miejsca, drugą stroną jest to, że sprawia, że kod jest bardziej skomplikowany i delikatny. Porównaj te dwie wersje metody sumowania tablicy:

Pierwsza wersja to sposób, w jaki normalnie kodujesz metodę:

/**
 * Sum the values in an array of integers.
 * @arg values the array to be summed
 * @return the sum
 **/
public int sum(int[] values) {
    int sum = 0;
    for (int value : values) {
        sum += value;
    }
    return sum;
}

Druga wersja to sposób kodowania metody, jeśli masz zwyczaj używania null do reprezentowania pustej tablicy.

/**
 * Sum the values in an array of integers.
 * @arg values the array to be summed, or null.
 * @return the sum, or zero if the array is null.
 **/
public int sum(int[] values) {
    int sum = 0;
    if (values != null) {
        for (int value : values) {
            sum += value;
        }
    }
    return sum;
}

Jak widać, kod jest nieco bardziej skomplikowany. Jest to bezpośrednio związane z decyzją o zastosowaniu null w ten sposób.

Teraz zastanów się, czy ta tablica, która może być null jest używana w wielu miejscach. W każdym miejscu, w którym go używasz, musisz rozważyć, czy musisz przetestować na null . Jeśli przegapisz test null który musi tam być, ryzykujesz NullPointerException . Dlatego strategia używania null w ten sposób powoduje, że twoja aplikacja jest bardziej delikatna; tj. bardziej podatny na konsekwencje błędów programisty.


Lekcja polega na tym, aby używać pustych tablic i pustych list, gdy o to ci chodzi.

int[] values = new int[0];                     // always empty
List<Integer> list = new ArrayList();          // initially empty
List<Integer> list = Collections.emptyList();  // always empty

Przestrzeń nad głową jest niewielka i istnieją inne sposoby jej zminimalizowania, jeśli jest to warte zachodu.

Pitfall - „Making good” nieoczekiwane wartości zerowe

Na StackOverflow często widzimy taki kod w odpowiedziach:

public String joinStrings(String a, String b) {
    if (a == null) {
        a = "";
    }
    if (b == null) {
        b = "";
    }
    return a + ": " + b;
}

Często towarzyszy temu twierdzenie, które jest „najlepszą praktyką” do testowania null ten sposób, aby uniknąć NullPointerException .

Czy to najlepsza praktyka? W skrócie: Nie.

Istnieją pewne podstawowe założenia, które należy zakwestionować, zanim będziemy mogli stwierdzić, czy dobrym pomysłem jest to zrobić w naszym joinStrings :

Co to znaczy, że „a” lub „b” ma wartość zerową?

String wartość może być zero lub więcej znaków, więc mamy już sposób reprezentowania pusty ciąg. Czy null oznacza coś innego niż "" ? Jeśli nie, problematyczne są dwa sposoby reprezentowania pustego ciągu.

Czy wartość null pochodzi od niezainicjowanej zmiennej?

null może pochodzić z niezainicjowanego pola lub niezainicjowanego elementu tablicy. Wartość może być niezainicjowana przez projekt lub przez przypadek. Jeśli był to przypadek, to jest to błąd.

Czy wartość null oznacza „nie wiem” lub „brakującą wartość”?

Czasami null może mieć prawdziwe znaczenie; np. że rzeczywista wartość zmiennej jest nieznana lub niedostępna lub „opcjonalna”. W Javie 8 klasa Optional zapewnia lepszy sposób na wyrażenie tego.

Jeśli to błąd (lub błąd projektowy), czy powinniśmy „naprawić”?

Jedną z interpretacji kodu jest to, że „naprawiamy” nieoczekiwany null , używając pustego ciągu zamiast niego. Czy właściwa strategia? Czy lepiej byłoby pozwolić na NullPointerException , a następnie uchwycić wyjątek na górze stosu i zarejestrować go jako błąd?

Problem z „naprawianiem” polega na tym, że może albo ukryć problem, albo utrudnić diagnozę.

Czy to jest wydajne / dobre dla jakości kodu?

Jeśli podejście „make good” będzie stosowane konsekwentnie, twój kod będzie zawierał wiele „defensywnych” testów zerowych. Sprawi to, że będzie dłuższy i trudniejszy do odczytania. Co więcej, wszystkie te testy i „poprawianie” mogą mieć wpływ na wydajność aplikacji.

W podsumowaniu

Jeśli null jest znaczącą wartością, testowanie dla przypadku null jest poprawnym podejściem. Następstwem jest to, że jeśli wartość null ma znaczenie, to powinno to być wyraźnie udokumentowane w javadocs każdej metody, która akceptuje lub zwraca wartość null .

W przeciwnym razie lepszym pomysłem jest potraktowanie nieoczekiwanego null jako błędu programowego i pozwolenie na NullPointerException , aby deweloper dowiedział się o problemie w kodzie.

Pitfall - zwracanie wartości null zamiast zgłaszania wyjątku

Niektórzy programiści Java mają ogólną niechęć do zgłaszania lub propagowania wyjątków. Prowadzi to do następującego kodu:

public Reader getReader(String pathname) {
    try {
        return new BufferedReader(FileReader(pathname));
    } catch (IOException ex) {
        System.out.println("Open failed: " + ex.getMessage());
        return null;
    }

}

Jaki jest z tym problem?

Problem polega na tym, że getReader zwraca null jako specjalną wartość wskazującą, że nie można otworzyć programu Reader . Teraz zwrócona wartość musi zostać przetestowana, aby sprawdzić, czy jest null przed użyciem. Jeśli test zostanie pominięty, wynikiem będzie NullPointerException .

W rzeczywistości istnieją tutaj trzy problemy:

  1. IOException został IOException zbyt wcześnie.
  2. Struktura tego kodu oznacza, że istnieje ryzyko wycieku zasobu.
  3. Zwrócono null ponieważ żaden „prawdziwy” program Reader był dostępny do zwrócenia.

W rzeczywistości, zakładając, że wyjątek musiał zostać wychwycony tak wcześnie, istnieje kilka alternatyw dla zwrócenia null :

  1. Byłoby możliwe zaimplementowanie klasy NullReader ; np. taki, w którym operacje API zachowują się tak, jakby czytnik był już w pozycji „końca pliku”.
  2. W Javie 8 możliwe jest zadeklarowanie getReader jako zwracającego Optional<Reader> .

Pitfall - Nie sprawdza, czy strumień I / O nie jest nawet inicjowany podczas zamykania

Aby zapobiec wyciekom pamięci, nie należy zapominać o zamknięciu strumienia wejściowego lub wyjściowego, którego zadanie zostało wykonane. Zwykle odbywa się to za pomocą instrukcji try - catch - finally bez części catch :

void writeNullBytesToAFile(int count, String filename) throws IOException {
    FileOutputStream out = null;
    try {
        out = new FileOutputStream(filename);
        for(; count > 0; count--)
            out.write(0);
    } finally {
        out.close();
    }
}

Chociaż powyższy kod może wyglądać niewinnie, ma wadę, która może uniemożliwić debugowanie. Jeśli linia, w której inicjowane jest out ( out = new FileOutputStream(filename) ), zgłasza wyjątek, wówczas out będzie null po wykonaniu out.close() , co NullPointerException nieprzyjemny NullPointerException !

Aby temu zapobiec, po prostu upewnij się, że strumień nie ma null zanim spróbujesz go zamknąć.

void writeNullBytesToAFile(int count, String filename) throws IOException {
    FileOutputStream out = null;
    try {
        out = new FileOutputStream(filename);
        for(; count > 0; count--)
            out.write(0);
    } finally {
        if (out != null)
            out.close();
    }
}

Jeszcze lepszym podejściem jest try -with-resources, ponieważ automatycznie zamknie strumień z prawdopodobieństwem 0, aby wyrzucić NPE bez konieczności blokowania w finally .

void writeNullBytesToAFile(int count, String filename) throws IOException {
    try (FileOutputStream out = new FileOutputStream(filename)) {
        for(; count > 0; count--)
            out.write(0);
    }
}

Pitfall - Używanie „notacji Yoda”, aby uniknąć wyjątku NullPointerException

Wiele przykładowego kodu opublikowanego na StackOverflow zawiera takie fragmenty:

if ("A".equals(someString)) {
    // do something
}

To znaczy „zapobiegać” lub „unikania” ewentualna NullPointerException w przypadku, someString jest null . Ponadto można to stwierdzić

    "A".equals(someString)

jest lepszy niż:

    someString != null && someString.equals("A")

(Jest bardziej zwięzły, aw niektórych okolicznościach może być bardziej wydajny. Jednak, jak argumentujemy poniżej, zwięzłość może być negatywna).

Jednak prawdziwą pułapką jest użycie testu Yoda, aby uniknąć NullPointerExceptions jako przyzwyczajenia.

Kiedy piszesz "A".equals(someString) , tak naprawdę "A".equals(someString) ” przypadek, w którym someString jest null . Ale jako kolejny przykład ( Pitfall - „Making good” nieoczekiwane wartości zerowe ) wyjaśnia, że „tworzenie dobrych” wartości null może być szkodliwe z różnych powodów.

Oznacza to, że warunki Yoda nie są „najlepszą praktyką” 1 . O ile nie spodziewany jest null , lepiej pozwolić na NullPointerException , aby uzyskać błąd testu jednostkowego (lub raportu o błędzie). To pozwala ci znaleźć i naprawić błąd, który spowodował pojawienie się nieoczekiwanego / niepożądanego null .

Warunki Yoda powinny być stosowane tylko w przypadkach, w których oczekiwany jest null ponieważ testowany obiekt pochodzi z interfejsu API udokumentowanego jako zwracanie null . I prawdopodobnie lepiej byłoby użyć jednego z mniej ładnych sposobów wyrażania testu, ponieważ pomaga to podświetlić test null komuś, kto przegląda twój kod.


1 - Według Wikipedii : „Najlepsze praktyki kodowania to zbiór nieformalnych zasad, których społeczność programistów nauczyła się z czasem, co może pomóc w poprawie jakości oprogramowania”. . Używanie notacji Yoda nie osiąga tego. W wielu sytuacjach kod jest gorszy.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow