Łańcuch sprawdzania zerowego a przechwytywanie wyjątku NullPointerException


118

Usługa sieciowa zwraca ogromny plik XML i muszę uzyskać dostęp do głęboko zagnieżdżonych pól. Na przykład:

return wsObject.getFoo().getBar().getBaz().getInt()

Problemem jest to, że getFoo(), getBar(), getBaz()mogą zwracać null.

Jeśli jednak sprawdzę, czy nullwe wszystkich przypadkach, kod staje się bardzo rozwlekły i trudny do odczytania. Ponadto mogę przegapić kontrole niektórych pól.

if (wsObject.getFoo() == null) return -1;
if (wsObject.getFoo().getBar() == null) return -1;
// maybe also do something with wsObject.getFoo().getBar()
if (wsObject.getFoo().getBar().getBaz() == null) return -1;
return wsObject.getFoo().getBar().getBaz().getInt();

Czy można pisać

try {
    return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
    return -1;
}

czy też byłoby to uważane za anty-wzór?


29
Nie miałbym nic przeciwko nullkontrolom, ponieważ wsObject.getFoo().getBar().getBaz().getInt()jest to już zapach kodu. Przeczytaj, czym jest „Prawo Demeter” i wolisz odpowiednio zmienić kod. Wtedy problem z nullczekami również zniknie. I pomyśl o użyciu Optional.
Tom

9
A co z używaniem XPath i pozostawieniem ich ocenie?
Joop Eggen

15
Ten kod jest prawdopodobnie generowany przez wsdl2java, który nie szanuje prawa Demeter.
Adrian Cox

Odpowiedzi:


143

Łapanie NullPointerExceptionjest naprawdę problematyczne, ponieważ może się zdarzyć prawie wszędzie. Bardzo łatwo jest go złapać przez błąd, złapać go przez przypadek i kontynuować tak, jakby wszystko było normalne, ukrywając w ten sposób prawdziwy problem. Trudno sobie z tym poradzić, więc najlepiej całkowicie tego unikać. (Na przykład pomyśl o automatycznym rozpakowywaniu wartości null Integer).

Proponuję Optionalzamiast tego użyć klasy. Jest to często najlepsze podejście, gdy chcesz pracować z wartościami, które są obecne lub nieobecne.

Używając tego, możesz napisać swój kod w ten sposób:

public Optional<Integer> m(Ws wsObject) {
    return Optional.ofNullable(wsObject.getFoo()) // Here you get Optional.empty() if the Foo is null
        .map(f -> f.getBar()) // Here you transform the optional or get empty if the Bar is null
        .map(b -> b.getBaz())
        .map(b -> b.getInt());
        // Add this if you want to return null instead of an empty optional if any is null
        // .orElse(null);
        // Or this if you want to throw an exception instead
        // .orElseThrow(SomeApplicationException::new);
}

Dlaczego opcjonalne?

Używanie Optionals zamiastnull dla wartości, które mogą być nieobecne, sprawia, że ​​fakt ten jest bardzo widoczny i jasny dla czytelników, a system typów zapewni, że przypadkowo o tym nie zapomnisz.

Otrzymasz również dostęp do metod wygodniejszej pracy z takimi wartościami, takich jak mapi orElse.


Czy nieobecność jest ważna czy błąd?

Ale zastanów się również, czy prawidłowy wynik dla metod pośrednich zwraca wartość null, czy też jest to oznaka błędu. Jeśli zawsze jest to błąd, prawdopodobnie lepiej jest zgłosić wyjątek niż zwrócić specjalną wartość lub aby same metody pośrednie zgłosiły wyjątek.


Może więcej opcji?

Jeśli z drugiej strony nieobecne wartości z metod pośrednich są prawidłowe, być może możesz przełączyć się na Optional je również s?

Następnie możesz ich użyć w ten sposób:

public Optional<Integer> mo(Ws wsObject) {
    return wsObject.getFoo()
        .flatMap(f -> f.getBar())
        .flatMap(b -> b.getBaz())
        .flatMap(b -> b.getInt());        
}

Dlaczego nie opcjonalne?

Jedynym powodem, dla którego mogę wymyślić, aby go nie używać, Optionaljest to, że jest to w naprawdę krytycznej dla wydajności części kodu i jeśli obciążenie związane ze zbieraniem elementów bezużytecznych okazuje się problemem. Dzieje się tak, ponieważ kilka Optionalobiektów jest przydzielanych za każdym razem, gdy kod jest wykonywany, a maszyna wirtualna może nie być w stanie ich zoptymalizować. W takim przypadku oryginalne testy if mogą być lepsze.


Bardzo dobra odpowiedź. Należy wspomnieć, że jeśli istnieją inne możliwe warunki awarii i musisz je rozróżnić, możesz użyć Tryzamiast Optional. Chociaż nie ma Trytakiego interfejsu API języka Java, istnieje wiele bibliotek, które je zapewniają, np. Javaslang.io , github.com/bradleyscollins/try4j , functionjava.org lub github.com/jasongoodwin/better-java-monads
Landei

8
FClass::getBaritp. byłby krótszy.
Boris the Spider

1
@BoristheSpider: Może trochę. Ale zwykle wolę lambdy od referencji metod, ponieważ często nazwy klas są znacznie dłuższe i uważam, że lambdy są trochę łatwiejsze do odczytania.
Lii

6
@Lii w porządku, ale pamiętaj, że odwołanie do metody może być odrobinę szybsze, ponieważ lambda może wymagać bardziej złożonych konstrukcji czasu kompilacji. Lambda będzie wymagała wygenerowania staticmetody, co spowoduje bardzo niewielką karę.
Boris the Spider

1
@Lii Właściwie uważam, że odwołania do metod są bardziej przejrzyste i opisowe, nawet jeśli są nieco dłuższe.
shmosel

14

Proponuję rozważyć Objects.requireNonNull(T obj, String message). Możesz zbudować łańcuchy ze szczegółową wiadomością dla każdego wyjątku, na przykład

requireNonNull(requireNonNull(requireNonNull(
    wsObject, "wsObject is null")
        .getFoo(), "getFoo() is null")
            .getBar(), "getBar() is null");

Sugerowałbym, abyś nie używał specjalnych wartości zwracanych, takich jak -1 . To nie jest styl Java. Java zaprojektowała mechanizm wyjątków, aby uniknąć tego staromodnego sposobu, który pochodzi z języka C.

Rzucanie NullPointerException nie jest najlepszą opcją. Można podać własne wyjątek (co sprawdzone , aby zagwarantować, że będzie on obsługiwany przez użytkownika lub niekontrolowany przetwarzać je w łatwiejszy sposób) lub użyć specyficzny wyjątek od parsera XML, którego używasz.


1
Objects.requireNonNullostatecznie wyrzuca NullPointerException. Więc to nie sprawia, że ​​sytuacja jest inna niżreturn wsObject.getFoo().getBar().getBaz().getInt()
Arka Ghosh

1
@ArkaGhosh, również unika wielu ifs, jak pokazał OP
Andrew Tobilko

4
To jedyne rozsądne rozwiązanie. Wszyscy inni zalecają stosowanie wyjątków do kontroli przepływu, która jest zapachem kodu. Na marginesie: uważam, że metoda łączenia w łańcuchy wykonana przez OP również ma zapach. Gdyby pracował z trzema zmiennymi lokalnymi i odpowiadającym im „jeśli”, sytuacja byłaby znacznie jaśniejsza. Myślę również, że problem jest głębszy niż tylko obejście NPE: OP powinien zadać sobie pytanie, dlaczego gettery mogą zwracać wartość null. Co znaczy null? Może jakiś obiekt zerowy byłby lepszy? Lub rozbić się w jednym pobieraczu z istotnym wyjątkiem? Zasadniczo wszystko jest lepsze niż wyjątki dotyczące kontroli przepływu.
Marius K.

1
Bezwarunkowa rada dotycząca używania wyjątków do sygnalizowania braku prawidłowej wartości zwracanej nie jest zbyt dobra. Wyjątki są przydatne, gdy metoda zawodzi w sposób, który jest trudny do odzyskania przez wywołującego i który jest lepiej obsługiwany w instrukcji try-catch w innej części programu. Aby po prostu zasygnalizować brak wartości zwracanej, lepiej jest użyć Optionalklasy, a może zwrócić wartość nullInteger
Lii

6

Zakładając, że struktura klas jest rzeczywiście poza naszą kontrolą, jak się wydaje, w tym przypadku, myślę, że wychwycenie NPE, jak sugerowano w pytaniu, jest rzeczywiście rozsądnym rozwiązaniem, chyba że wydajność jest głównym problemem. Jednym małym ulepszeniem może być zawinięcie logiki rzutów / łapania, aby uniknąć bałaganu:

static <T> T get(Supplier<T> supplier, T defaultValue) {
    try {
        return supplier.get();
    } catch (NullPointerException e) {
        return defaultValue;
    }
}

Teraz możesz po prostu zrobić:

return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), -1);

return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), "");nie daje błędu w czasie kompilacji, który mógłby być problematyczny.
Philippe Gioseffi

5

Jak już zauważył Tom w komentarzu,

Poniższe stwierdzenie jest niezgodne z prawem Demeter :

wsObject.getFoo().getBar().getBaz().getInt()

To, czego chcesz, to inti możesz to uzyskać Foo. Prawo Demeter mówi, nigdy nie rozmawiaj z nieznajomymi . W swoim przypadku możesz ukryć rzeczywistą realizację pod maską Fooi Bar.

Teraz można utworzyć metodę w Foocelu pobrania intod Baz. Ostatecznie,Foo będziemy mieć Bari Barmożemy uzyskać dostęp Intbez Bazbezpośredniego wystawiania Foo. Tak więc sprawdzanie wartości null jest prawdopodobnie podzielone na różne klasy i tylko wymagane atrybuty będą współdzielone między klasami.


4
Jest to dyskusyjne, jeśli jest niezgodne z prawem Demeter, ponieważ WsObject jest prawdopodobnie tylko strukturą danych. Zobacz tutaj: stackoverflow.com/a/26021695/1528880
DerM

2
@DerM Tak, jest to możliwe, ale ponieważ OP ma już coś, co analizuje jego plik XML, może również pomyśleć o stworzeniu odpowiednich klas modelu dla wymaganych tagów, aby biblioteka analizująca mogła je odwzorować. Następnie te klasy modeli zawierają logikę do nullsprawdzania własnych tagów podrzędnych.
Tom

4

Moja odpowiedź jest prawie w tym samym wierszu co @janki, ale chciałbym nieco zmodyfikować fragment kodu, jak poniżej:

if (wsObject.getFoo() != null && wsObject.getFoo().getBar() != null && wsObject.getFoo().getBar().getBaz() != null) 
   return wsObject.getFoo().getBar().getBaz().getInt();
else
   return something or throw exception;

Możesz również dodać sprawdzenie wartości null wsObject, jeśli istnieje jakakolwiek szansa, że ​​ten obiekt będzie pusty.


4

Mówisz, że niektóre metody „mogą powrócić null”, ale nie mówisz, w jakich okolicznościach powracają null. Mówisz, że łapieszNullPointerException ale nie mówisz, dlaczego to łapiesz. Ten brak informacji sugeruje, że nie masz jasnego zrozumienia, do czego służą wyjątki i dlaczego są one lepsze od alternatywy.

Rozważ metodę klasy, która jest przeznaczona do wykonywania akcji, ale metoda nie może zagwarantować, że wykona akcję, z powodu okoliczności niezależnych od niej (co w rzeczywistości dotyczy wszystkich metod w Javie ). Nazywamy tę metodę i zwraca. Kod, który wywołuje tę metodę, musi wiedzieć, czy się powiodła. Skąd to może wiedzieć? Jak można zorganizować radzenie sobie z dwiema możliwościami, sukcesu lub porażki?

Korzystając z wyjątków, możemy pisać metody, które mają powodzenie jako warunek wiadomości . Jeśli metoda zwraca, powiodła się. Jeśli zgłosi wyjątek, nie powiodło się. To duża wygrana dla jasności. Możemy napisać kod, który w jasny sposób przetwarza normalny przypadek sukcesu i przenieść cały kod obsługi błędów do catchklauzul. Często okazuje się, że szczegóły tego, jak lub dlaczego metoda zakończyła się niepowodzeniem, nie są ważne dla wywołującego, więc ta sama catchklauzula może być używana do obsługi kilku typów niepowodzeń. A często zdarza się, że metoda nie wymaga wyjątków dotyczących połowów w ogóle , ale można po prostu pozwolić im propagować do swojego rozmówcy. Wyjątki spowodowane błędami programu znajdują się w tej ostatniej klasie; kilka metod może odpowiednio zareagować, gdy pojawi się błąd.

Więc te metody, które powracają null.

  • Czy nullwartość wskazuje na błąd w kodzie? Jeśli tak, nie powinieneś w ogóle łapać wyjątku. Twój kod nie powinien próbować odgadnąć samego siebie. Po prostu napisz to, co jest jasne i zwięzłe przy założeniu, że zadziała. Czy łańcuch wywołań metod jest jasny i zwięzły? Następnie po prostu ich użyj.
  • Czy nullwartość wskazuje na nieprawidłowe dane wejściowe w programie? Jeśli tak, a NullPointerExceptionnie jest odpowiednim wyjątkiem do zgłoszenia, ponieważ zwykle jest zarezerwowany do wskazywania błędów. Prawdopodobnie chcesz zgłosić wyjątek niestandardowy pochodzący z IllegalArgumentException(jeśli chcesz niezaznaczony wyjątek ) lub IOException(jeśli chcesz sprawdzić wyjątek). Czy Twój program jest zobowiązany do dostarczania szczegółowych komunikatów o błędach składni w przypadku nieprawidłowych danych wejściowych? Jeśli tak, nullto jedyne, co możesz zrobić , to sprawdzenie każdej metody pod kątem wartości zwracanej, a następnie zgłoszenie odpowiedniego wyjątku diagnostycznego. Jeśli program nie wymaga szczegółowej diagnostyki, łączenie wywołań metod w łańcuchy, wychwytywanie dowolnego, NullPointerExceptiona następnie wyrzucanie niestandardowego wyjątku jest najwyraźniejsze i najbardziej zwięzłe.

Jedna z odpowiedzi głosi, że wywołania metod łańcuchowych naruszają prawo Demeter, a zatem są złe. To twierdzenie jest błędne.

  • Jeśli chodzi o projektowanie programów, tak naprawdę nie ma żadnych absolutnych reguł określających, co jest dobre, a co złe. Istnieją tylko heurystyki: reguły, które są poprawne przez większość (nawet prawie przez cały czas). Częścią umiejętności programowania jest wiedza, kiedy można złamać tego rodzaju zasady. Tak więc zwięzłe stwierdzenie, że „to jest sprzeczne z regułą X ” nie jest w rzeczywistości odpowiedzią. Czy to jedna z sytuacji, w których reguła powinna złamać ?
  • Prawo Demeter jest naprawdę zasada o projektowaniu interfejsu API lub klasa. Projektując klasy, warto mieć hierarchię abstrakcji . Masz klasy niskiego poziomu, które używają prymitywów języka do bezpośredniego wykonywania operacji i reprezentowania obiektów w abstrakcji, która jest wyższa niż prymitywy języka. Masz klasy średniego poziomu, które delegują do klas niskiego poziomu i implementują operacje i reprezentacje na wyższym poziomie niż klasy niskiego poziomu. Masz klasy wysokiego poziomu, które delegują do klas średniego poziomu i implementują operacje i abstrakcje jeszcze wyższego poziomu. (Mówiłem tutaj o zaledwie trzech poziomach abstrakcji, ale możliwych jest więcej). Dzięki temu Twój kod może wyrazić się w postaci odpowiednich abstrakcji na każdym poziomie, ukrywając w ten sposób złożoność. Uzasadnienie dla prawa Demeterjest to, że jeśli masz łańcuch wywołań metod, sugeruje to, że masz klasę wysokiego poziomu sięgającą przez klasę średniego poziomu, aby bezpośrednio zajmować się szczegółami niskiego poziomu, a zatem twoja klasa średniego poziomu nie zapewniła operacji abstrakcyjnej na średnim poziomie czego potrzebuje klasa na wysokim poziomie. Ale wygląda na to, że nie jest to sytuacja, którą masz tutaj: nie zaprojektowałeś klas w łańcuchu wywołań metod, są one wynikiem jakiegoś automatycznie generowanego kodu serializacji XML (prawda?), A łańcuch wywołań nie jest malejący przez hierarchię abstrakcji, ponieważ des-serializowany plik XML znajduje się na tym samym poziomie hierarchii abstrakcji (prawda?)?

3

Aby poprawić czytelność, możesz chcieć użyć wielu zmiennych, takich jak

Foo theFoo;
Bar theBar;
Baz theBaz;

theFoo = wsObject.getFoo();

if ( theFoo == null ) {
  // Exit.
}

theBar = theFoo.getBar();

if ( theBar == null ) {
  // Exit.
}

theBaz = theBar.getBaz();

if ( theBaz == null ) {
  // Exit.
}

return theBaz.getInt();

Moim zdaniem jest to znacznie mniej czytelne. Zaśmieca metodę całą masą logiki sprawdzającej wartość null, która jest całkowicie nieistotna w odniesieniu do rzeczywistej logiki metody.
Deweloper102938

2

Nie łap NullPointerException . Nie wiesz, skąd się bierze (wiem, że to nie jest prawdopodobne w twoim przypadku, ale może coś innego rzuciło) i jest powolne. Chcesz uzyskać dostęp do określonego pola i w tym przypadku każde inne pole nie może być puste. To doskonały powód, aby sprawdzić każde pole. Prawdopodobnie sprawdziłbym to w jednym jeśli, a następnie stworzyłbym metodę na czytelność. Jak wskazywali inni, już zwracanie -1 jest bardzo oldschoolowe, ale nie wiem, czy masz ku temu powód, czy nie (np. Rozmowa z innym systemem).

public int callService() {
    ...
    if(isValid(wsObject)){
        return wsObject.getFoo().getBar().getBaz().getInt();
    }
    return -1;
}


public boolean isValid(WsObject wsObject) {
    if(wsObject.getFoo() != null &&
        wsObject.getFoo().getBar() != null &&
        wsObject.getFoo().getBar().getBaz() != null) {
        return true;
    }
    return false;
}

Edycja: Jest to dyskusyjne, jeśli jest niezgodne z prawem Demeter, ponieważ WsObject jest prawdopodobnie tylko strukturą danych (sprawdź https://stackoverflow.com/a/26021695/1528880 ).


2

Jeśli nie chcesz refaktoryzować kodu i możesz użyć języka Java 8, możesz użyć odwołań do metod.

Najpierw proste demo (przepraszam za statyczne klasy wewnętrzne)

public class JavaApplication14 
{
    static class Baz
    {
        private final int _int;
        public Baz(int value){ _int = value; }
        public int getInt(){ return _int; }
    }
    static class Bar
    {
        private final Baz _baz;
        public Bar(Baz baz){ _baz = baz; }
        public Baz getBar(){ return _baz; }   
    }
    static class Foo
    {
        private final Bar _bar;
        public Foo(Bar bar){ _bar = bar; }
        public Bar getBar(){ return _bar; }   
    }
    static class WSObject
    {
        private final Foo _foo;
        public WSObject(Foo foo){ _foo = foo; }
        public Foo getFoo(){ return _foo; }
    }
    interface Getter<T, R>
    {
        R get(T value);
    }

    static class GetterResult<R>
    {
        public R result;
        public int lastIndex;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) 
    {
        WSObject wsObject = new WSObject(new Foo(new Bar(new Baz(241))));
        WSObject wsObjectNull = new WSObject(new Foo(null));

        GetterResult<Integer> intResult
                = getterChain(wsObject, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);

        GetterResult<Integer> intResult2
                = getterChain(wsObjectNull, WSObject::getFoo, Foo::getBar, Bar::getBar, Baz::getInt);


        System.out.println(intResult.result);
        System.out.println(intResult.lastIndex);

        System.out.println();
        System.out.println(intResult2.result);
        System.out.println(intResult2.lastIndex);

        // TODO code application logic here
    }

    public static <R, V1, V2, V3, V4> GetterResult<R>
            getterChain(V1 value, Getter<V1, V2> g1, Getter<V2, V3> g2, Getter<V3, V4> g3, Getter<V4, R> g4)
            {
                GetterResult result = new GetterResult<>();

                Object tmp = value;


                if (tmp == null)
                    return result;
                tmp = g1.get((V1)tmp);
                result.lastIndex++;


                if (tmp == null)
                    return result;
                tmp = g2.get((V2)tmp);
                result.lastIndex++;

                if (tmp == null)
                    return result;
                tmp = g3.get((V3)tmp);
                result.lastIndex++;

                if (tmp == null)
                    return result;
                tmp = g4.get((V4)tmp);
                result.lastIndex++;


                result.result = (R)tmp;

                return result;
            }
}

Wynik

241
4

null
2

Interfejs Getterjest tylko interfejsem funkcjonalnym, możesz użyć dowolnego odpowiednika.
GetterResultklasa, metody dostępu zostały usunięte dla przejrzystości, przechowują wynik łańcucha pobierania, jeśli taki istnieje, lub indeks ostatniego wywołanego gettera.

Metoda getterChainto prosty, szablonowy fragment kodu, który można wygenerować automatycznie (lub ręcznie w razie potrzeby).
Skonstruowałem kod tak, aby powtarzający się blok był oczywisty.


Nie jest to idealne rozwiązanie, ponieważ nadal musisz zdefiniować jedno przeciążenie getterChain na liczbę pobierających.

Zamiast tego dokonałbym refaktoryzacji kodu, ale jeśli nie możesz i często używasz długich łańcuchów getterów, możesz rozważyć zbudowanie klasy z przeciążeniami, które zajmują od 2 do, powiedzmy, 10, getterów.


2

Jak powiedzieli inni, poszanowanie prawa Demeter jest zdecydowanie częścią rozwiązania. Inną częścią, jeśli to możliwe, jest zmiana tych połączonych metod, aby nie mogły wrócić null. Możesz uniknąć powrotu, zwracając nullzamiast tego pusty String, pusty Collectionlub inny fikcyjny obiekt, który oznacza lub robi wszystko, z czym wywołujący zrobiłby null.


2

Chciałbym dodać odpowiedź, która skupia się na znaczeniu błędu . Wyjątek zerowy sam w sobie nie zapewnia żadnego pełnego znaczenia błędu. Dlatego radziłbym unikać bezpośredniego kontaktu z nimi.

Istnieją tysiące przypadków, w których twój kod może się nie udać: nie można połączyć się z bazą danych, wyjątek IO, błąd sieci ... Jeśli zajmiesz się nimi jeden po drugim (tak jak tutaj sprawdzanie zerowe), byłoby to zbyt kłopotliwe.

W kodzie:

wsObject.getFoo().getBar().getBaz().getInt();

Nawet jeśli wiesz, które pole jest puste, nie masz pojęcia, co poszło nie tak. Może Bar jest pusty, ale czy jest oczekiwany? A może to błąd danych? Pomyśl o ludziach, którzy czytają Twój kod

Podobnie jak w przypadku odpowiedzi Xenterosa, proponuję użycie niestandardowego niezaznaczonego wyjątku . Na przykład w tej sytuacji: Foo może mieć wartość null (prawidłowe dane), ale Bar i Baz nigdy nie powinny być puste (nieprawidłowe dane)

Kod można przepisać:

void myFunction()
{
    try 
    {
        if (wsObject.getFoo() == null)
        {
          throw new FooNotExistException();
        }

        return wsObject.getFoo().getBar().getBaz().getInt();
    }
    catch (Exception ex)
    {
        log.error(ex.Message, ex); // Write log to track whatever exception happening
        throw new OperationFailedException("The requested operation failed")
    }
}


void Main()
{
    try
    {
        myFunction();
    }
    catch(FooNotExistException)
    {
        // Show error: "Your foo does not exist, please check"
    }
    catch(OperationFailedException)
    {
        // Show error: "Operation failed, please contact our support"
    }
}

Niezaznaczone wyjątki wskazują, że programista nadużywa API. Zewnętrzne problemy, takie jak „nie można połączyć się z bazą danych, wyjątek we / wy, błąd sieci”, powinny być wskazywane przez zaznaczone wyjątki.
Kevin Krumwiede

To naprawdę zależy od potrzeby dzwoniącego. Zaznaczono pomoc dotyczącą wyjątków, ponieważ wymusza to przetworzenie błędu. Jednak w innych przypadkach nie jest to konieczne i może zanieczyścić kod. Na przykład masz wyjątek IOException w warstwie danych, czy wrzucisz go do warstwy prezentacji? Oznaczałoby to, że musisz złapać wyjątek i ponownie rzucić na każdego dzwoniącego. Wolałbym opakować wyjątek IOException w niestandardowy wyjątek BusinessException z odpowiednim komunikatem i pozwolić mu przejść przez ślad stosu, aż globalny filtr przechwyci go i wyświetli komunikat użytkownikowi.
Hoàng Long

Wzywający nie muszą łapać i ponownie rzucać sprawdzonych wyjątków, po prostu deklarują je jako wyrzucone.
Kevin Krumwiede

@KevinKrumwiede: masz rację, musimy tylko zadeklarować wyjątek do wyrzucenia. Nadal jednak musimy zadeklarować. Edycja: Patrząc na to po raz drugi, istnieje wiele debat na temat użycia sprawdzonych i niezaznaczonych wyjątków (np .: programmers.stackexchange.com/questions/121328/ ... ).
Hoàng Long

2

NullPointerException jest wyjątkiem w czasie wykonywania, więc generalnie nie zaleca się jego przechwytywania, ale unikania go.

Będziesz musiał złapać wyjątek wszędzie tam, gdzie chcesz wywołać metodę (lub będzie propagować stos). Niemniej jednak, jeśli w twoim przypadku możesz kontynuować pracę z tym wynikiem z wartością -1 i masz pewność, że nie zostanie on propagowany, ponieważ nie używasz żadnego z "elementów", które mogą być zerowe, to wydaje mi się słuszne, aby Złap to

Edytować:

Zgadzam się z późniejszą odpowiedzią od @xenteros, lepiej będzie uruchomić własny wyjątek zamiast zwracać -1, możesz to nazwać InvalidXMLExceptionna przykład.


3
Co masz na myśli mówiąc „bez względu na to, czy go złapiesz, może rozprzestrzenić się na inne części kodu”?
Hulk

Jeśli wartość null znajduje się w tym zdaniu wsObject.getFoo () A w późniejszych częściach kodu ponownie uruchamiasz to zapytanie lub używasz na przykład wsObject.getFoo (). GetBar (), ponownie podniesie NullPointerException.
SCouto

To jest niezwykłe sformułowanie „Będziesz musiał wyłapać wyjątek wszędzie tam, gdzie chcesz wywołać metodę (w przeciwnym razie rozprzestrzeni się w górę stosu)”. Jeśli poprawnie rozumiem. Zgadzam się z tym (i to może być problem), po prostu uważam, że sformułowanie jest mylące.
Hulk

Naprawię to, przepraszam, angielski nie jest moim pierwszym językiem, więc czasami może się to zdarzyć :) Dzięki
SCouto

2

Śledzę ten post od wczoraj.

Komentowałem / głosowałem na komentarze, które mówią, że łapanie NPE jest złe. Oto dlaczego to robię.

package com.todelete;

public class Test {
    public static void main(String[] args) {
        Address address = new Address();
        address.setSomeCrap(null);
        Person person = new Person();
        person.setAddress(address);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            try {
                System.out.println(person.getAddress().getSomeCrap().getCrap());
            } catch (NullPointerException npe) {

            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println((endTime - startTime) / 1000F);
        long startTime1 = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            if (person != null) {
                Address address1 = person.getAddress();
                if (address1 != null) {
                    SomeCrap someCrap2 = address1.getSomeCrap();
                    if (someCrap2 != null) {
                        System.out.println(someCrap2.getCrap());
                    }
                }
            }
        }
        long endTime1 = System.currentTimeMillis();
        System.out.println((endTime1 - startTime1) / 1000F);
    }
}

  public class Person {
    private Address address;

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }
}

package com.todelete;

public class Address {
    private SomeCrap someCrap;

    public SomeCrap getSomeCrap() {
        return someCrap;
    }

    public void setSomeCrap(SomeCrap someCrap) {
        this.someCrap = someCrap;
    }
}

package com.todelete;

public class SomeCrap {
    private String crap;

    public String getCrap() {
        return crap;
    }

    public void setCrap(String crap) {
        this.crap = crap;
    }
}

Wynik

3.216

0,002

Widzę tutaj wyraźnego zwycięzcę. Posiadanie czeków jest o wiele tańsze niż złapanie wyjątku. Widziałem, jak działa Java-8. Biorąc pod uwagę, że 70% obecnych aplikacji nadal działa na Javie-7, dodaję tę odpowiedź.

Podsumowanie W przypadku aplikacji o znaczeniu krytycznym obsługa NPE jest kosztowna.


Trzy dodatkowe sekundy na milion żądań w najgorszym przypadku są wymierne, ale rzadko byłoby to przełomem, nawet w „aplikacjach o znaczeniu krytycznym”. Istnieją systemy, w których dodanie 3,2 mikrosekundy do żądania to wielka sprawa, a jeśli masz taki system, koniecznie zastanów się dokładnie nad wyjątkami. Ale wywołanie usługi sieciowej i deserializacja jej danych wyjściowych, zgodnie z pierwotnym pytaniem, prawdopodobnie zajmuje znacznie więcej czasu, a martwienie się o wydajność obsługi wyjątków nie ma tu znaczenia.
Jeroen Mostert

@JeroenMostert: 3 sekundy na czek / milion. Tak więc liczba sprawdzeń zwiększy koszt
NewUser

Prawdziwe. Mimo to nadal uważam to za przypadek „najpierw profilu”. Potrzebowałbyś ponad 300 czeków w jednym żądaniu, zanim żądanie zajmie dodatkowe milisekundy. Względy projektowe ciążyłyby na mojej duszy znacznie wcześniej.
Jeroen Mostert

@JeroenMostert: :) Zgoda! Chciałbym zostawić to programiście z wynikiem i pozwolić mu odebrać telefon!
NewUser

1

Jeśli problemem jest wydajność, należy rozważyć opcję „połowu”. Jeśli nie można użyć słowa „catch”, ponieważ byłoby to propagowane (o czym wspomina „SCouto”), należy użyć zmiennych lokalnych, aby uniknąć wielokrotnych wywołań metod getFoo(), getBar()i getBaz().


1

Warto zastanowić się nad stworzeniem własnego Wyjątku. Nazwijmy to MyOperationFailedException. Możesz go rzucić zamiast zwracać wartość. Rezultat będzie taki sam - wyjdziesz z funkcji, ale nie zwrócisz zakodowanej na stałe wartości -1, która jest anty-wzorcem Java. W Javie używamy wyjątków.

try {
    return wsObject.getFoo().getBar().getBaz().getInt();
} catch (NullPointerException ignored) {
    throw new MyOperationFailedException();
}

EDYTOWAĆ:

Zgodnie z dyskusją w komentarzach pozwolę sobie dodać coś do moich wcześniejszych przemyśleń. W tym kodzie są dwie możliwości. Po pierwsze, akceptujesz wartość null, a po drugie, że jest to błąd.

Jeśli jest to błąd i występuje, możesz debugować kod przy użyciu innych struktur do celów debugowania, gdy punkty przerwania nie są wystarczające.

Jeśli jest to dopuszczalne, nie obchodzi Cię, gdzie pojawił się ten null. Jeśli to zrobisz, zdecydowanie nie powinieneś łączyć tych żądań.


2
Czy nie uważasz, że zniesienie wyjątku to zły pomysł? W czasie rzeczywistym, jeśli zgubimy ślad wyjątku, to prawdziwy ból na dole, aby dowiedzieć się, co się do cholery dzieje! Zawsze radziłbym nie używać łańcuchów. Drugi problem, jaki widzę, to: ten kod nie może w danym momencie przyznać, który z wyników był pusty.
NewUser

Nie, Twój wyjątek może zawierać wiadomość, która z pewnością wskaże miejsce, w którym został rzucony. Zgadzam się, że
łańcuch

3
Nie, wystarczyłoby powiedzieć o numerze linii. Zatem każde wywołanie w łańcuchu może prowadzić do wyjątku.
NewUser

„Jeśli jest to błąd i występuje, możesz debugować swój kod” - nie w produkcji. Wolałbym raczej wiedzieć, CO zawiodło, kiedy wszystko, co mam, to dziennik, niż próbować odgadnąć, co się stało, co spowodowało niepowodzenie. Mając tę ​​radę (i ten kod), wszystko, co naprawdę wiesz, to to, że jedna z 4 rzeczy była zerowa, ale nie która z nich lub dlaczego.
VLAZ

1

Metoda, którą masz, jest długa, ale bardzo czytelna. Gdybym był nowym programistą przychodzącym do twojej bazy kodu, mógłbym dość szybko zobaczyć, co robisz. Większość innych odpowiedzi (w tym wychwytywanie wyjątku) nie wydaje się czynić rzeczy bardziej czytelnymi, a niektóre sprawiają, że moim zdaniem jest mniej czytelny.

Biorąc pod uwagę, że prawdopodobnie nie masz kontroli nad wygenerowanym źródłem i zakładając, że naprawdę potrzebujesz dostępu do kilku głęboko zagnieżdżonych pól tu i tam, zalecałbym zawijanie każdego głęboko zagnieżdżonego dostępu metodą.

private int getFooBarBazInt() {
    if (wsObject.getFoo() == null) return -1;
    if (wsObject.getFoo().getBar() == null) return -1;
    if (wsObject.getFoo().getBar().getBaz() == null) return -1;
    return wsObject.getFoo().getBar().getBaz().getInt();
}

Jeśli zauważysz, że piszesz wiele z tych metod lub jeśli kusi Cię, aby utworzyć te publiczne metody statyczne, utworzę oddzielny model obiektowy, zagnieżdżony tak, jak chcesz, z tylko polami, na których Ci zależy, i przekonwertuj go z sieci Services do modelu obiektów.

Podczas komunikacji ze zdalną usługą internetową bardzo typowe jest posiadanie „domeny zdalnej” i „domeny aplikacji” i przełączanie się między nimi. Domena zdalna jest często ograniczona protokołem sieciowym (na przykład nie można wysyłać metod pomocniczych tam iz powrotem w czystej usłudze RESTful, a głęboko zagnieżdżone modele obiektów są powszechne, aby uniknąć wielu wywołań API), a więc nie są idealne do bezpośredniego użycia w Twój klient.

Na przykład:

public static class MyFoo {

    private int barBazInt;

    public MyFoo(Foo foo) {
        this.barBazInt = parseBarBazInt();
    }

    public int getBarBazInt() {
        return barBazInt;
    }

    private int parseFooBarBazInt(Foo foo) {
        if (foo() == null) return -1;
        if (foo().getBar() == null) return -1;
        if (foo().getBar().getBaz() == null) return -1;
        return foo().getBar().getBaz().getInt();
    }

}

1
return wsObject.getFooBarBazInt();

stosując prawo Demeter,

class WsObject
{
    FooObject foo;
    ..
    Integer getFooBarBazInt()
    {
        if(foo != null) return foo.getBarBazInt();
        else return null;
    }
}

class FooObject
{
    BarObject bar;
    ..
    Integer getBarBazInt()
    {
        if(bar != null) return bar.getBazInt();
        else return null;
    }
}

class BarObject
{
    BazObject baz;
    ..
    Integer getBazInt()
    {
        if(baz != null) return baz.getInt();
        else return null;
    }
}

class BazObject
{
    Integer myInt;
    ..
    Integer getInt()
    {
        return myInt;
    }
}

0

Dając odpowiedź, która wydaje się inna niż wszystkie inne.

Polecam sprawdzić NULLw ifpkt.

Powód:

Nie powinniśmy zostawiać ani jednej szansy na awarię naszego programu. NullPointer jest generowany przez system. Nie można przewidzieć zachowania wyjątków wygenerowanych przez system . Nie powinieneś zostawiać programu w rękach Systemu, jeśli masz już sposób na jego samodzielną obsługę. I umieść mechanizm obsługi wyjątków dla dodatkowego bezpieczeństwa. !!

Aby uczynić swój kod łatwym do odczytania, spróbuj tego, aby sprawdzić warunki:

if (wsObject.getFoo() == null || wsObject.getFoo().getBar() == null || wsObject.getFoo().getBar().getBaz() == null) 
   return -1;
else 
   return wsObject.getFoo().getBar().getBaz().getInt();

EDYTOWAĆ :

Tutaj trzeba przechowywać te wartości wsObject.getFoo(), wsObject.getFoo().getBar(), wsObject.getFoo().getBar().getBaz()w niektórych zmiennych. Nie robię tego, ponieważ nie znam typów zwrotów tych funkcji.

Wszelkie sugestie będą mile widziane .. !!


Czy uważałeś getFoo () za bardzo czasochłonną operację? Powinieneś przechowywać zwracane wartości w zmiennych, jednak jest to strata pamięci. Twoja metoda jest idealna do programowania w C.
xenteros

ale czasami lepiej jest spóźnić się o 1 milisekundę, a wtedy program się zawiesza @xenteros .. !!
Janki Gadhiya

getFoo () może pobrać wartość z serwera znajdującego się na innym kontynencie. Może trwać w dowolnym momencie: min / godziny ...
xenteros

wsObjectbędzie zawierać wartość zwróconą przez usługę sieciową .. !! Usługa zostanie już wywołana i wsObjectotrzyma długie XMLdane jako odpowiedź usługi sieciowej .. !! Nie ma więc nic podobnego do serwera znajdującego się na innym kontynencie, ponieważ getFoo()jest to tylko element pobierający metodę pobierania, a nie wywołanie usługi Webservice .. !! @xenteros
Janki Gadhiya

1
Cóż, z nazw getterów zakładam, że zwracają obiekty Foo, Bar i Baz: P Rozważ także usunięcie wspomnianego podwójnego bezpieczeństwa z twojej odpowiedzi. Nie sądzę, aby zapewniało to jakąkolwiek prawdziwą wartość poza zanieczyszczeniem kodu. Dzięki rozsądnym zmiennym lokalnym i sprawdzaniu wartości null zrobiliśmy więcej niż wystarczająco, aby zapewnić poprawność kodu. Jeśli może wystąpić wyjątek, należy go traktować jako jeden.
Marius K.

0

Napisałem klasę o nazwie, Snagktóra pozwala zdefiniować ścieżkę poruszania się po drzewie obiektów. Oto przykład jego zastosowania:

Snag<Car, String> ENGINE_NAME = Snag.createForAndReturn(Car.class, String.class).toGet("engine.name").andReturnNullIfMissing();

Oznacza to, że instancja ENGINE_NAMEskutecznie wywoła Car?.getEngine()?.getName()przekazaną do niej instancję i zwróci, nulljeśli zwróci jakiekolwiek odwołanie null:

final String name =  ENGINE_NAME.get(firstCar);

Nie jest opublikowany w Maven, ale jeśli ktoś uzna to za przydatne, jest tutaj (oczywiście bez gwarancji!)

To trochę proste, ale wydaje się, że spełnia swoje zadanie. Oczywiście jest bardziej przestarzały w nowszych wersjach języka Java i innych języków JVM, które obsługują bezpieczną nawigację lub Optional.

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.