Catena di controllo null vs cattura di NullPointerException


118

Un servizio web restituisce un enorme XML e ho bisogno di accedere a campi profondamente nidificati. Per esempio:

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

Il problema è che getFoo(), getBar(), getBaz()può tutto il ritorno null.

Tuttavia, se controllo nullin tutti i casi, il codice diventa molto dettagliato e difficile da leggere. Inoltre, potrei perdere i controlli per alcuni campi.

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();

È accettabile scrivere

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

o sarebbe considerato un antipattern?


29
Non mi dispiacerebbe più di nulltanto i controlli, poiché wsObject.getFoo().getBar().getBaz().getInt()è già un odore di codice. Leggi cos'è la "Legge di Demetra" e preferisci rifattorizzare il codice di conseguenza. Quindi anche il problema con i nullcontrolli sparirà. E pensa di usare Optional.
Tom

9
Che ne dici di usare XPath e lasciarlo alla loro valutazione?
Joop Eggen

15
Quel codice è probabilmente generato da wsdl2java, che non ha rispetto per la Legge di Demetra.
Adrian Cox

Risposte:


143

La cattura NullPointerExceptionè una cosa davvero problematica da fare poiché possono accadere quasi ovunque. È molto facile prenderne uno da un bug, catturarlo per sbaglio e continuare come se tutto fosse normale, nascondendo così un vero problema. È così difficile da affrontare, quindi è meglio evitarlo del tutto. (Ad esempio, pensa all'unboxing automatico di un file nullInteger .)

Ti suggerisco di utilizzare l'estensione Optional classe. Questo è spesso l'approccio migliore quando si desidera lavorare con valori che sono presenti o assenti.

Usandolo potresti scrivere il tuo codice in questo modo:

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);
}

Perché opzionale?

Utilizzando Optionals invece dinull per valori che potrebbero essere assenti rende questo fatto molto visibile e chiaro per i lettori, e il sistema di tipi farà in modo che tu non lo dimentichi accidentalmente.

Puoi anche accedere a metodi per lavorare con tali valori in modo più conveniente, come mape orElse.


L'assenza è valida o è un errore?

Ma pensa anche se è un risultato valido per i metodi intermedi restituire null o se questo è un segno di un errore. Se si tratta sempre di un errore, probabilmente è meglio lanciare un'eccezione piuttosto che restituire un valore speciale o che i metodi intermedi stessi lanciano un'eccezione.


Forse più optional?

Se invece sono validi valori assenti dai metodi intermedi, forse puoi passare a Optional s anche per loro?

Quindi potresti usarli in questo modo:

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

Perché non facoltativo?

L'unico motivo che mi viene in mente per non utilizzare Optionalè se questo è in una parte del codice davvero critica per le prestazioni e se il sovraccarico della raccolta dei rifiuti risulta essere un problema. Questo perché alcuni Optionaloggetti vengono allocati ogni volta che il codice viene eseguito e la VM potrebbe non essere in grado di ottimizzarli. In tal caso i tuoi if-test originali potrebbero essere migliori.


Risposta molto buona. Va ricordato che se ci sono altre possibili condizioni di guasto e devi distinguerle, puoi usare al Tryposto di Optional. Sebbene non sia presente Trynell'API Java, ci sono molte librerie che ne forniscono una, ad esempio javaslang.io , github.com/bradleyscollins/try4j , functionaljava.org o github.com/jasongoodwin/better-java-monads
Landei

8
FClass::getBarecc sarebbe più breve.
Boris the Spider

1
@ BoristheSpider: Forse un po '. Ma di solito preferisco i lambda ai metodi refs perché spesso i nomi delle classi sono molto più lunghi e trovo i lambda un po 'più facili da leggere.
Lii

6
@Lii è abbastanza giusto, ma nota che un riferimento a un metodo potrebbe essere leggermente più veloce, poiché un lambda potrebbe richiedere costrutti di compilazione più complessi. Il lambda richiederà la generazione di un staticmetodo, che incorrerà in una penalità molto minore.
Boris the Spider

1
@Lii In realtà trovo che i riferimenti ai metodi siano più puliti e più descrittivi, anche quando sono leggermente più lunghi.
shmosel

14

Suggerisco di considerare Objects.requireNonNull(T obj, String message). Potresti costruire catene con un messaggio dettagliato per ogni eccezione, come

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

Ti suggerirei di non utilizzare valori di ritorno speciali, come -1. Non è uno stile Java. Java ha progettato il meccanismo delle eccezioni per evitare questo modo antiquato che proveniva dal linguaggio C.

Anche il lancio NullPointerExceptionnon è l'opzione migliore. È possibile fornire la propria eccezione ( verificandola per garantire che verrà gestita da un utente o deselezionata per elaborarla in modo più semplice) o utilizzare un'eccezione specifica dal parser XML che si sta utilizzando.


1
Objects.requireNonNullalla fine lancia NullPointerException. Quindi questo non rende la situazione diversa dareturn wsObject.getFoo().getBar().getBaz().getInt()
Arka Ghosh

1
@ArkaGhosh, evita anche un sacco di ifs come ha mostrato OP
Andrew Tobilko,

4
Questa è l'unica soluzione sensata. Tutti gli altri consigliano di utilizzare eccezioni per il controllo del flusso che è un odore di codice. Una nota a margine: considero anche un odore il metodo di concatenamento eseguito dall'OP. Se lavorasse con tre variabili locali e il corrispondente se la situazione sarebbe molto più chiara. Inoltre penso che il problema sia più profondo del semplice aggirare un NPE: OP dovrebbe chiedersi perché i getter possono restituire null. Cosa significa null? Forse qualche oggetto nullo sarebbe meglio? O schiantarsi in un getter con un'eccezione significativa? Fondamentalmente tutto è meglio delle eccezioni per il controllo del flusso.
Marius K.

1
Il consiglio incondizionato di utilizzare le eccezioni per segnalare l'assenza di un valore di ritorno valido non è molto buono. Le eccezioni sono utili quando un metodo fallisce in un modo da cui è difficile per il chiamante recuperare e che è meglio gestita in un'istruzione try-catch in qualche altra parte del programma. Per segnalare semplicemente l'assenza di un valore di ritorno è meglio usare la Optionalclasse, o magari restituire un nullableInteger
Lii

6

Supponendo che la struttura della classe sia effettivamente fuori dal nostro controllo, come sembra essere il caso, penso che catturare l'NPE come suggerito nella domanda sia davvero una soluzione ragionevole, a meno che la performance non sia una delle principali preoccupazioni. Un piccolo miglioramento potrebbe essere quello di avvolgere la logica di lancio / ripresa per evitare il disordine:

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

Ora puoi semplicemente fare:

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

return get(() -> wsObject.getFoo().getBar().getBaz().getInt(), "");non dà un errore in fase di compilazione che potrebbe essere problematico.
Philippe Gioseffi

5

Come già sottolineato da Tom nel commento,

La seguente dichiarazione disobbedisce alla legge di Demetra ,

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

Quello che vuoi è inte puoi ottenerlo Foo. Legge di Demetra dice: non parlare mai con gli estranei . Nel tuo caso puoi nascondere l'effettiva implementazione sotto il cofano di Fooe Bar.

Ora puoi creare un metodo Fooper recuperare intda Baz. In definitiva, Fooavremo Bare in Barcui possiamo accedere Intsenza esporre Bazdirettamente a Foo. Quindi, i controlli nulli sono probabilmente divisi in classi diverse e solo gli attributi richiesti saranno condivisi tra le classi.


4
È discutibile se disobbedisce alla Legge di Demetra poiché il WsObject è probabilmente solo una struttura di dati. Vedi qui: stackoverflow.com/a/26021695/1528880
Derm

2
@DerM Sì, è possibile, ma poiché OP ha già qualcosa che analizza il suo file XML, può anche pensare di creare classi di modello adatte per i tag richiesti, in modo che la libreria di analisi possa mapparli. Quindi queste classi di modelli contengono la logica per un nullcontrollo dei propri tag secondari.
Tom

4

La mia risposta va quasi sulla stessa riga di @janki, ma vorrei modificare leggermente lo snippet di codice come di seguito:

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;

Puoi anche aggiungere un controllo nullo wsObject, se c'è qualche possibilità che quell'oggetto sia nullo.


4

Dici che alcuni metodi "possono tornare null" ma non dici in quali circostanze tornano null. Dici di prendere il fileNullPointerException ma non dici perché lo prendi. Questa mancanza di informazioni suggerisce che non si ha una chiara comprensione di cosa siano le eccezioni e perché siano superiori all'alternativa.

Considera un metodo di classe che ha lo scopo di eseguire un'azione, ma il metodo non può garantire che eseguirà l'azione, a causa di circostanze al di fuori del suo controllo (che in effetti è il caso di tutti i metodi in Java ). Chiamiamo quel metodo e ritorna. Il codice che chiama quel metodo deve sapere se ha avuto successo. Come può saperlo? Come può essere strutturato per far fronte alle due possibilità, di successo o di fallimento?

Utilizzando le eccezioni, possiamo scrivere metodi che hanno successo come condizione di post . Se il metodo ritorna, ha avuto successo. Se genera un'eccezione, ha fallito. Questa è una grande vittoria per la chiarezza. Possiamo scrivere codice che elabori chiaramente il normale caso di successo e spostare tutto il codice di gestione degli errori in catchclausole. Capita spesso che i dettagli di come o perché un metodo non abbia avuto successo non sono importanti per il chiamante, quindi la stessa catchclausola può essere utilizzata per gestire diversi tipi di errori. E spesso accade che un metodo non ha bisogno di eccezioni di cattura a tutti , ma può semplicemente permettere loro di propagare a suo chiamante. Le eccezioni dovute a bug del programma si trovano in quest'ultima classe; pochi metodi possono reagire in modo appropriato quando c'è un bug.

Quindi, quei metodi che restituiscono null.

  • Un nullvalore indica un bug nel codice? In tal caso, non dovresti assolutamente rilevare l'eccezione. E il tuo codice non dovrebbe cercare di indovinare se stesso. Basta scrivere ciò che è chiaro e conciso partendo dal presupposto che funzionerà. Una catena di chiamate di metodo è chiara e concisa? Quindi usali.
  • Un nullvalore indica un input non valido per il tuo programma? Se lo fa, a NullPointerExceptionnon è un'eccezione appropriata da lanciare, perché convenzionalmente è riservata per indicare bug. Probabilmente vuoi lanciare un'eccezione personalizzata derivata da IllegalArgumentException(se vuoi un'eccezione non controllata ) o IOException(se vuoi un'eccezione controllata). Il tuo programma è tenuto a fornire messaggi di errore di sintassi dettagliati in caso di input non valido? In tal caso, nulll'unica cosa che puoi fare è controllare ogni metodo per un valore restituito e quindi lanciare un'eccezione diagnostica appropriata. Se il tuo programma non ha bisogno di fornire una diagnostica dettagliata, concatenare le chiamate di metodo insieme, catturarne qualsiasi NullPointerExceptione quindi lanciare la tua eccezione personalizzata è più chiaro e conciso.

Una delle risposte afferma che le chiamate al metodo concatenato violano la Legge di Demetra e quindi sono cattive. Questa affermazione è sbagliata.

  • Quando si tratta di progettazione di programmi, non esistono regole assolute su ciò che è buono e ciò che è cattivo. Ci sono solo euristiche: regole che sono giuste per la maggior parte (anche quasi tutte) le volte. Parte dell'abilità della programmazione sta nel sapere quando va bene infrangere questo tipo di regole. Quindi un'affermazione concisa che "questo è contro la regola X " non è affatto una risposta. È questa una delle situazioni in cui dovrebbe essere la regola essere infranta?
  • La Legge di Demetra è davvero una regola sulla progettazione di API o interfacce di classe. Quando si progettano classi, è utile disporre di una gerarchia di astrazioni . Esistono classi di basso livello che utilizzano le primitive del linguaggio per eseguire direttamente operazioni e rappresentare oggetti in un'astrazione di livello superiore rispetto alle primitive del linguaggio. Hai classi di livello medio che delegano alle classi di basso livello e implementano operazioni e rappresentazioni a un livello superiore rispetto alle classi di basso livello. Hai classi di alto livello che delegano alle classi di livello medio e implementano operazioni e astrazioni di livello ancora più alto. (Ho parlato solo di tre livelli di astrazione qui, ma sono possibili di più). Ciò consente al codice di esprimersi in termini di astrazioni appropriate a ciascun livello, nascondendo così la complessità. La logica per il Legge di Demetraè che se hai una catena di chiamate di metodo, ciò suggerisce che hai una classe di alto livello che arriva attraverso una classe di livello medio per trattare direttamente i dettagli di basso livello, e quindi che la tua classe di livello medio non ha fornito un'operazione astratta di livello medio di cui la classe di alto livello ha bisogno. Ma sembra che non sia la situazione che hai qui: non hai progettato le classi nella catena di chiamate di metodo, sono il risultato di un codice di serializzazione XML generato automaticamente (giusto?) E la catena di chiamate non è discendente attraverso una gerarchia di astrazione perché l'XML des serializzato è tutto allo stesso livello della gerarchia di astrazione (giusto?)?

3

Per migliorare la leggibilità, potresti voler utilizzare più variabili, come

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();

Questo è molto meno leggibile secondo me. Copre il metodo con tutta una serie di logica di controllo nullo che è completamente irrilevante dalla logica effettiva del metodo.
Sviluppatore

2

Non prendere NullPointerException. Non sai da dove proviene (so che non è probabile nel tuo caso ma forse qualcos'altro l'ha lanciato) ed è lento. Si desidera accedere al campo specificato e per questo ogni altro campo non deve essere nullo. Questo è un motivo perfetto e valido per controllare ogni campo. Probabilmente lo controllerei in uno se e poi creerei un metodo per la leggibilità. Come altri hanno sottolineato, il ritorno di -1 è molto vecchio ma non so se ne hai una ragione o meno (ad esempio parlando con un altro sistema).

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;
}

Modifica: è discutibile se disobbedisce alla Legge di Demetra poiché WsObject è probabilmente solo una struttura di dati (controlla https://stackoverflow.com/a/26021695/1528880 ).


2

Se non si desidera effettuare il refactoring del codice e si può utilizzare Java 8, è possibile utilizzare riferimenti a metodi.

Prima una semplice demo (scusa le classi interne statiche)

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;
            }
}

Produzione

241
4

nullo
2

L'interfaccia Getterè solo un'interfaccia funzionale, puoi usare qualsiasi equivalente.
GetterResultclass, le funzioni di accesso eliminate per chiarezza, mantengono il risultato della catena getter, se presente, o l'indice dell'ultimo getter chiamato.

Il metodo getterChainè un semplice pezzo di codice standard, che può essere generato automaticamente (o manualmente quando necessario).
Ho strutturato il codice in modo che il blocco ripetuto sia evidente.


Questa non è una soluzione perfetta in quanto è ancora necessario definire un sovraccarico di getterChain per numero di getter.

Vorrei invece rifattorizzare il codice, ma se non è possibile e ti ritrovi a usare lunghe catene di getter spesso potresti prendere in considerazione la creazione di una classe con i sovraccarichi che richiedono da 2 a, diciamo, 10, getter.


2

Come altri hanno detto, il rispetto della Legge di Demetra fa sicuramente parte della soluzione. Un'altra parte, ove possibile, consiste nel modificare quei metodi concatenati in modo che non possano tornare null. Puoi evitare di tornare nullrestituendo invece un oggetto vuoto String, vuoto Collectiono qualche altro oggetto fittizio che significa o fa qualunque cosa il chiamante farebbe con null.


2

Vorrei aggiungere una risposta che si concentri sul significato dell'errore . L'eccezione nulla di per sé non fornisce alcun significato completo di errore. Quindi consiglierei di evitare di trattarli direttamente.

Ci sono migliaia di casi in cui il tuo codice può andare storto: impossibile connettersi al database, eccezione IO, errore di rete ... Se li gestisci uno per uno (come il controllo nullo qui), sarebbe una seccatura.

Nel codice:

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

Anche quando sai quale campo è nullo, non hai idea di cosa va storto. Forse Bar è nullo, ma è previsto? O è un errore di dati? Pensa alle persone che leggono il tuo codice

Come nella risposta di xenteros, proporrei di utilizzare un'eccezione personalizzata non selezionata . Ad esempio, in questa situazione: Foo può essere nullo (dati validi), ma Bar e Baz non dovrebbero mai essere nulli (dati non validi)

Il codice può essere riscritto:

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"
    }
}

Le eccezioni non selezionate indicano che il programmatore sta utilizzando in modo improprio l'API. Problemi esterni come "impossibile connettersi al database, eccezione IO, errore di rete" dovrebbero essere indicati dalle eccezioni selezionate.
Kevin Krumwiede

Dipende davvero dalle necessità di chi chiama. Aiuto per le eccezioni verificate perché ti obbliga a elaborare l'errore. Tuttavia, in altri casi, non è necessario e potrebbe inquinare il codice. Ad esempio, hai un'IOException nel tuo livello dati, la lanci al livello Presentazione? Ciò significherebbe che devi intercettare l'eccezione e lanciarla nuovamente a ogni chiamante. Preferirei racchiudere l'IOException con una BusinessException personalizzata, con un messaggio pertinente, e lasciarla scorrere attraverso lo stacktrace, fino a quando un filtro globale non la cattura e mostra il messaggio all'utente.
Hoàng Long,

I chiamanti non devono catturare e rilanciare le eccezioni verificate, ma solo dichiararle da lanciare.
Kevin Krumwiede

@KevinKrumwiede: hai ragione, dobbiamo solo dichiarare l'eccezione da lanciare. Tuttavia, dobbiamo ancora dichiarare. Modifica: Dando una seconda occhiata a questo, ci sono molti dibattiti sull'uso delle eccezioni controllate e non controllate (ad esempio: programmers.stackexchange.com/questions/121328/… ).
Hoàng Long,

2

NullPointerException è un'eccezione in fase di esecuzione, quindi in generale non è consigliabile prenderla, ma evitarla.

Dovrai catturare l'eccezione ovunque tu voglia chiamare il metodo (o si propagherà nello stack). Tuttavia, se nel tuo caso puoi continuare a lavorare con quel risultato con valore -1 e sei sicuro che non si propagherà perché non stai usando nessuno dei "pezzi" che potrebbero essere nulli, allora mi sembra giusto prenderla

Modificare:

Sono d'accordo con la risposta successiva di @xenteros, sarebbe meglio lanciare la tua eccezione invece di restituire -1 puoi chiamarlo InvalidXMLExceptionad esempio.


3
Cosa intendi con "non importa se lo prendi, può propagarsi ad altre parti del codice"?
Hulk

Se il null è in questa frase wsObject.getFoo () E nelle parti successive del codice esegui di nuovo quella query o usi wsObject.getFoo (). GetBar () (per esempio) solleverà di nuovo una NullPointerException.
SCouto

Questa è una formulazione insolita per "Dovrai catturare l'eccezione ovunque tu voglia chiamare il metodo (o si propagherà nello stack)". se ho capito bene. Sono d'accordo con questo (e questo potrebbe essere un problema), trovo solo che la formulazione sia confusa.
Hulk

Lo aggiusterò, mi dispiace, l'inglese non è la mia prima lingua quindi questo può accadere a volte :) Grazie
SCouto

2

Seguo questo post da ieri.

Ho commentato / votato i commenti che dicono, catturare NPE è negativo. Ecco perché l'ho fatto.

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;
    }
}

Produzione

3.216

0.002

Vedo un chiaro vincitore qui. Avere se i controlli è troppo meno costoso che prendere un'eccezione. Ho visto quel modo di fare Java-8. Considerando che il 70% delle attuali applicazioni funziona ancora su Java-7, aggiungo questa risposta.

Conclusione Per qualsiasi applicazione mission-critical, la gestione degli NPE è costosa.


Tre secondi extra su un milione di richieste nel peggiore dei casi sono misurabili, ma raramente sarebbe un problema, anche in "applicazioni mission critical". Ci sono sistemi in cui aggiungere 3,2 microsecondi a una richiesta è un grosso problema, e se hai un sistema del genere, pensa con attenzione alle eccezioni. Ma chiamare un servizio web e deserializzare il suo output, secondo la domanda originale, probabilmente richiede molto più tempo, e preoccuparsi delle prestazioni della gestione delle eccezioni non è importante.
Jeroen Mostert

@JeroenMostert: 3 secondi per assegno / milione. Quindi, il numero di controlli aumenterà il costo
NewUser

Vero. Anche con questo lo considererei comunque un caso di "profilo prima", però. Avresti bisogno di oltre 300 controlli in una richiesta prima che la richiesta richieda un millisecondo intero in più. Le considerazioni sul design peserebbero sulla mia anima molto prima.
Jeroen Mostert

@JeroenMostert: :) D'accordo! Vorrei lasciare al programmatore il risultato e fargli prendere una chiamata!
Nuovo utente

1

Se l'efficienza è un problema, si dovrebbe prendere in considerazione l'opzione "cattura". Se 'cattura' non può essere utilizzato perché si propaga (come detto da 'SCouto') quindi utilizzare le variabili locali per evitare più chiamate a metodi getFoo(), getBar()e getBaz().


1

Vale la pena considerare di creare la tua eccezione. Chiamiamolo MyOperationFailedException. Puoi lanciarlo invece restituendo un valore. Il risultato sarà lo stesso: abbandonerai la funzione, ma non restituirai il valore hardcoded -1 che è Java anti-pattern. In Java usiamo le eccezioni.

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

MODIFICARE:

Secondo la discussione nei commenti, lasciami aggiungere qualcosa ai miei pensieri precedenti. In questo codice ci sono due possibilità. Uno è che accetti null e l'altro è che si tratta di un errore.

Se si tratta di un errore e si verifica, puoi eseguire il debug del codice utilizzando altre strutture a scopo di debug quando i punti di interruzione non sono sufficienti.

Se è accettabile, non ti interessa dove è apparso questo null. Se lo fai, non dovresti assolutamente concatenare quelle richieste.


2
Non pensi che sia una cattiva idea sopprimere l'eccezione? In tempo reale, se perdiamo la traccia di un'eccezione, è veramente doloroso scoprire cosa diavolo sta succedendo! Suggerirei sempre di non utilizzare il concatenamento. Il secondo problema che vedo è: questo codice non può garantire al momento, quale risultato era nullo.
Nuovo utente

No, la tua eccezione può contenere un messaggio che indicherebbe sicuramente il punto in cui è stata lanciata. Sono d'accordo che il concatenamento non è la soluzione migliore :)
xenteros

3
No, direbbe solo il numero di riga. Quindi, qualsiasi chiamata nella catena può portare a un'eccezione.
NewUser

"Se si tratta di un errore e si verifica, è possibile eseguire il debug del codice" - non in produzione. Preferirei di gran lunga sapere COSA ha fallito quando tutto quello che ho è un registro piuttosto che cercare di indovinare cosa è successo per farlo fallire. Con quel consiglio (e quel codice), tutto ciò che sai veramente è che una delle 4 cose era nulla, ma non quale o perché.
VLAZ

1

Il metodo che hai è lungo, ma molto leggibile. Se fossi un nuovo sviluppatore che viene nella tua base di codice, potrei vedere cosa stavi facendo abbastanza rapidamente. La maggior parte delle altre risposte (inclusa la cattura dell'eccezione) non sembrano rendere le cose più leggibili e alcune lo rendono meno leggibile secondo me.

Dato che probabilmente non hai il controllo sulla sorgente generata e supponendo che tu abbia davvero solo bisogno di accedere ad alcuni campi profondamente nidificati qua e là, allora consiglierei di avvolgere ogni accesso profondamente nidificato con un metodo.

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();
}

Se ti ritrovi a scrivere molti di questi metodi o se sei tentato di rendere questi metodi statici pubblici, creerei un modello di oggetti separato, annidato come vorresti, con solo i campi che ti interessano e convertirò dal web modello a oggetti dei servizi al modello a oggetti.

Quando si comunica con un servizio Web remoto è molto tipico avere un "dominio remoto" e un "dominio applicazione" e passare da uno all'altro. Il dominio remoto è spesso limitato dal protocollo web (ad esempio, non è possibile inviare metodi di supporto avanti e indietro in un puro servizio RESTful e modelli a oggetti profondamente nidificati sono comuni per evitare più chiamate API) e quindi non è l'ideale per l'uso diretto in il tuo cliente.

Per esempio:

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();

applicando la Legge di Demetra,

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

Dare una risposta che sembra diversa da tutte le altre.

Ti consiglio di controllare NULLin ifs.

Motivo :

Non dobbiamo lasciare una sola possibilità che il nostro programma vada in crash. NullPointer viene generato dal sistema. Il comportamento delle eccezioni generate dal sistema non può essere previsto . Non dovresti lasciare il tuo programma nelle mani del sistema quando hai già un modo per gestirlo da solo. E metti il ​​meccanismo di gestione delle eccezioni per una maggiore sicurezza. !!

Per rendere il tuo codice facile da leggere, prova questo per verificare le condizioni:

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

MODIFICARE :

Qui è necessario memorizzare questi valori wsObject.getFoo(), wsObject.getFoo().getBar(), wsObject.getFoo().getBar().getBaz()in alcune variabili. Non lo sto facendo perché non conosco i tipi di ritorno di quelle funzioni.

Ogni suggerimento sarà apprezzato..!!


Hai considerato getFoo () un'operazione che richiede molto tempo? È necessario memorizzare i valori restituiti nelle variabili, tuttavia è uno spreco di memoria. Il tuo metodo è perfetto per la programmazione C.
xenteros

ma a volte è meglio essere in ritardo di 1 millisecondo, quindi il programma si blocca @xenteros .. !!
Janki Gadhiya

getFoo () potrebbe ricevere un valore da un server situato in un altro continente. Può durare in qualsiasi momento: minuti / ore ...
xenteros

wsObjectconterrà il valore restituito dal Webservice .. !! Il Servizio sarà già chiamato e wsObjectriceverà un lungo XMLdato come risposta del webservice .. !! Quindi non c'è niente come un server situato in un altro continente perché getFoo()è solo un elemento che ottiene un metodo getter non una chiamata al servizio Web .. !! @xenteros
Janki Gadhiya

1
Ebbene, dai nomi dei getter presumo che restituiscano oggetti Foo, Bar e Baz: P Considera anche la rimozione della doppia sicurezza menzionata dalla tua risposta. Non credo che fornisca alcun valore reale a parte l'inquinamento del codice. Con le variabili locali sane e il controllo null abbiamo fatto più che sufficiente per garantire la correttezza del codice. Se può verificarsi un'eccezione, deve essere trattata come tale.
Marius K.

0

Ho scritto una classe chiamata Snagche ti permette di definire un percorso per navigare attraverso un albero di oggetti. Ecco un esempio del suo utilizzo:

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

Significa che l'istanza ENGINE_NAMEchiamerebbe effettivamente sull'istanza Car?.getEngine()?.getName()passatale e restituirebbe nullse fosse restituito un riferimento null:

final String name =  ENGINE_NAME.get(firstCar);

Non è pubblicato su Maven ma se qualcuno lo trova utile è qui (senza garanzia ovviamente!)

È un po 'semplice ma sembra fare il lavoro. Ovviamente è più obsoleto con le versioni più recenti di Java e altri linguaggi JVM che supportano la navigazione sicura o Optional.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.