Perché esplicitamente lanciare una NullPointerException piuttosto che lasciarla accadere naturalmente?


182

Quando leggo il codice sorgente JDK, trovo comune che l'autore controllerà i parametri se sono nulli e quindi lancia manualmente il nuovo NullPointerException (). Perché lo fanno? Penso che non sia necessario farlo poiché genererà nuovo NullPointerException () quando chiama un metodo. (Ecco un codice sorgente di HashMap, ad esempio :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}

32
un punto chiave della programmazione è l'intento
Scary Wombat,

19
Questa è un'ottima domanda per la tua prima domanda! Ho apportato alcune modifiche minori; Spero non ti dispiaccia. Ho anche rimosso il ringraziamento e la nota sul fatto che questa è la tua prima domanda poiché normalmente questo genere di cose non fa parte delle domande SO.
David Conrad,

11
Sono C # la convenzione è di sollevare ArgumentNullExceptionin casi del genere (piuttosto che NullReferenceException) - in realtà è davvero una buona domanda sul perché dovresti sollevare NullPointerExceptionesplicitamente qui (piuttosto che uno diverso).
EJoshuaS - Ripristina Monica

21
@EJoshuaS È un vecchio dibattito se lanciare IllegalArgumentExceptiono NullPointerExceptionper un argomento nullo. La convenzione JDK è quest'ultima.
shmosel,

33
Il vero problema è che generano un errore e eliminano tutte le informazioni che hanno portato a questo errore . Sembra che questo sia il vero codice sorgente. Nemmeno un semplice messaggio di stringa insanguinata. Triste.
Martin Ba,

Risposte:


254

Ci sono una serie di ragioni che vengono in mente, molte delle quali strettamente correlate:

Fail-fast: se fallirà, meglio fallire prima piuttosto che dopo. Ciò consente di individuare i problemi più vicini alla loro fonte, semplificandone l'identificazione e il recupero. Evita inoltre di sprecare cicli della CPU su codice destinato a fallire.

Intento: il lancio esplicito dell'eccezione chiarisce ai manutentori che l'errore è presente intenzionalmente e che l'autore era a conoscenza delle conseguenze.

Coerenza: se l'errore dovesse verificarsi in modo naturale, potrebbe non verificarsi in tutti gli scenari. Se non viene trovato alcun mapping, ad esempio, remappingFunctionnon verrebbe mai utilizzato e l'eccezione non verrebbe generata. La convalida anticipata degli input consente un comportamento più deterministico e una documentazione più chiara .

Stabilità: il codice si evolve nel tempo. Il codice che incontra un'eccezione naturalmente potrebbe, dopo un po 'di refactoring, cessare di farlo o farlo in circostanze diverse. Lanciarlo in modo esplicito rende meno probabile che il comportamento cambi inavvertitamente.


14
Inoltre: in questo modo la posizione da cui viene generata l'eccezione è legata esattamente a una variabile che viene controllata. Senza di essa, l'eccezione potrebbe essere dovuta al fatto che una delle variabili multiple è nulla.
Jens Schauder,

45
Un altro: se aspetti che NPE avvenga in modo naturale, un codice intermedio potrebbe aver già cambiato lo stato del tuo programma attraverso effetti collaterali.
Thomas,

6
Mentre questo frammento non lo fa, puoi usare il new NullPointerException(message)costruttore per chiarire cosa è nullo. Buono per le persone che non hanno accesso al tuo codice sorgente. Hanno persino reso questo un one-liner in JDK 8 con il Objects.requireNonNull(object, message)metodo dell'utilità.
Robin,

3
Il GUASTO dovrebbe essere vicino al GUASTO. "Fail Fast" è più di una regola empirica. Quando non vorresti questo comportamento? Qualsiasi altro comportamento significa che stai nascondendo errori. Ci sono "GUASTI" e "GUASTI". Il GUASTO è quando questo programma digerisce un puntatore NULL e si arresta in modo anomalo. Ma quella riga di codice non è dove sta il GUASTO. Il NULL veniva da qualche parte - un argomento di metodo. Chi ha superato tale argomento? Da una riga di codice che fa riferimento a una variabile locale. Dov'è che ... Vedi? Che schifo Di chi avrebbe dovuto essere la responsabilità di vedere un valore negativo memorizzato? Il tuo programma dovrebbe essere andato in crash allora.
Noah Spurrier,

4
@Thomas Un buon punto. Shmosel: il punto di Thomas potrebbe essere implicito nel punto di errore, ma è un po 'sepolto. È un concetto abbastanza importante che ha il suo nome: fallimento atomicità . Vedi Bloch, Effective Java , Item 46. Ha una semantica più forte di fail-fast. Suggerirei di chiamarlo in un punto separato. Ottima risposta nel complesso, BTW. +1
Stuart segna

40

È per chiarezza, coerenza e per impedire l'esecuzione di lavori extra e non necessari.

Considera cosa accadrebbe se non ci fosse una clausola di guardia nella parte superiore del metodo. Chiamava sempre hash(key)e getNode(hash, key)anche quando nullera stato passato remappingFunctionprima che l'NPE venisse lanciato.

Ancora peggio, se la ifcondizione è falseallora prendiamo il elseramo, che non usa remappingFunctionaffatto, il che significa che il metodo non lancia sempre NPE quando nullviene passato un; se lo fa dipende dallo stato della mappa.

Entrambi gli scenari sono cattivi. Se nullnon è un valore valido per remappingFunctionil metodo, deve sempre essere generata un'eccezione, indipendentemente dallo stato interno dell'oggetto al momento della chiamata, e dovrebbe farlo senza fare un lavoro inutile che è inutile dato che sta per essere lanciato. Infine, è un buon principio di codice pulito e chiaro avere la guardia in primo piano in modo che chiunque riesca a rivedere il codice sorgente possa facilmente vedere che lo farà.

Anche se l'eccezione fosse attualmente generata da ogni ramo di codice, è possibile che una futura revisione del codice cambierebbe tale. L'esecuzione del controllo all'inizio garantisce che verrà sicuramente eseguita.


25

Oltre ai motivi elencati dall'ottima risposta di @ shmosel ...

Prestazioni: potrebbero esserci / sono stati dei vantaggi in termini di prestazioni (su alcune JVM) nel lanciare l'NPE in modo esplicito piuttosto che lasciarlo fare alla JVM.

Dipende dalla strategia che l'interprete Java e il compilatore JIT adottano per rilevare la dereferenziazione dei puntatori null. Una strategia è quella di non testare null, ma di intercettare il SIGSEGV che si verifica quando un'istruzione tenta di accedere all'indirizzo 0. Questo è l'approccio più veloce nel caso in cui il riferimento sia sempre valido, ma è costoso nel caso NPE.

Un test esplicito nullnel codice eviterebbe il colpo alle prestazioni di SIGSEGV in uno scenario in cui gli NPE erano frequenti.

(Dubito che questa sarebbe una micro-ottimizzazione utile in una JVM moderna, ma avrebbe potuto essere in passato.)


Compatibilità: il probabile motivo per cui non vi è alcun messaggio nell'eccezione è per la compatibilità con gli NPE generati dalla JVM stessa. In un'implementazione Java conforme, un NPE generato dalla JVM ha un nullmessaggio. (Android Java è diverso.)


20

A parte ciò che altre persone hanno sottolineato, vale la pena notare il ruolo della convenzione qui. In C #, ad esempio, hai anche la stessa convenzione di sollevare esplicitamente un'eccezione in casi come questo, ma è specificamente un ArgumentNullException, che è in qualche modo più specifico. (La convenzione C # è che rappresenta NullReferenceException sempre un bug di qualche tipo - abbastanza semplicemente, non dovrebbe mai accadere nel codice di produzione; scontato, di ArgumentNullExceptionsolito lo fa anche, ma potrebbe essere un bug più lungo la linea di "tu non capire come utilizzare correttamente la libreria "tipo di bug).

Quindi, fondamentalmente, in C # NullReferenceExceptionsignifica che il tuo programma ha effettivamente provato a usarlo, mentre ArgumentNullExceptionsignifica che ha riconosciuto che il valore era sbagliato e non si è nemmeno preso la briga di provare a usarlo. Le implicazioni possono effettivamente essere diverse (a seconda delle circostanze) perché ArgumentNullExceptionsignifica che il metodo in questione non ha ancora avuto effetti collaterali (poiché ha fallito le condizioni preliminari del metodo).

Per inciso, se stai sollevando qualcosa di simile ArgumentNullExceptiono IllegalArgumentException, fa parte del punto di fare il controllo: vuoi un'eccezione diversa da quella che "normalmente" otterrai.

In ogni caso, sollevare esplicitamente l'eccezione rafforza la buona pratica di essere espliciti sulle pre-condizioni del metodo e sugli argomenti previsti, il che semplifica la lettura, l'utilizzo e la manutenzione del codice. Se non hai verificato esplicitamente null, non so se è perché pensavi che nessuno avrebbe mai passato un nullargomento, lo stai contando per lanciare comunque l'eccezione o hai semplicemente dimenticato di verificarlo.


4
+1 per il paragrafo centrale. Direi che il codice in questione dovrebbe 'lanciare nuovo IllegalArgumentException ("remappingFunction non può essere nullo");' In questo modo è immediatamente evidente cosa non andava. L'NPE mostrato è un po 'ambiguo.
Chris Parker,

1
@ChrisParker In passato ero della stessa opinione, ma risulta che NullPointerException ha lo scopo di indicare un argomento null passato a un metodo in attesa di un argomento non null, oltre a essere una risposta di runtime a un tentativo di dereference null. Da javadoc: "Le applicazioni dovrebbero lanciare istanze di questa classe per indicare altri usi illegali nulldell'oggetto." Non ne vado matto, ma sembra essere il design previsto.
VGR,

1
Sono d'accordo, @ChrisParker - Penso che quell'eccezione sia più specifica (poiché il codice non ha mai nemmeno provato a fare qualcosa con il valore, ha immediatamente riconosciuto che non avrebbe dovuto usarlo). Mi piace la convenzione C # in questo caso. La convenzione C # è che NullReferenceException(equivalente a NullPointerException) significa che il tuo codice ha effettivamente cercato di usarlo (che è sempre un bug - non dovrebbe mai accadere nel codice di produzione), rispetto a "So che l'argomento è sbagliato, quindi non ho nemmeno prova ad usarlo. " C'è anche ArgumentException(il che significa che l'argomento era sbagliato per qualche altro motivo).
EJoshuaS - Ripristina Monica

2
Lo dico molto, lancio SEMPRE una IllegalArgumentException come descritto. Mi sento sempre a mio agio con le convenzioni contrarie quando sento che la convenzione è stupida.
Chris Parker,

1
@PieterGeerkens - sì, perché la riga 35 di NullPointerException è molto migliore della riga 35 di IllegalArgumentException ("La funzione non può essere nulla"). Davvero?
Chris Parker,

12

È così che otterrai l'eccezione non appena commetti l'errore, piuttosto che in seguito quando stai usando la mappa e non capirai perché è successo.


9

Trasforma una condizione di errore apparentemente irregolare in una chiara violazione del contratto: la funzione ha alcuni presupposti per funzionare correttamente, quindi li controlla in anticipo, obbligandoli a soddisfarli.

L'effetto è che non sarà necessario eseguire il debug computeIfPresent()quando si ottiene l'eccezione. Quando vedi che l'eccezione proviene dal controllo dei presupposti, sai che hai chiamato la funzione con un argomento illegale. Se il controllo non fosse presente, sarebbe necessario escludere la possibilità che vi sia qualche bug all'interno di computeIfPresent()se stesso che porta all'eccezione generata.

Ovviamente, lanciare il generico NullPointerExceptionè una scelta davvero sbagliata, in quanto non segnala una violazione del contratto in sé e per sé. IllegalArgumentExceptionsarebbe una scelta migliore.


Sidenote:
Non so se Java lo permetta (ne dubito), ma i programmatori C / C ++ usano un assert()in questo caso, che è significativamente migliore per il debug: dice al programma di arrestarsi immediatamente e il più duro possibile se la condizione viene valutata come falsa. Quindi, se hai corso

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

sotto un debugger, e qualcosa è passato NULLin entrambi gli argomenti, il programma si fermerebbe proprio sulla linea dicendo quale argomento era NULL, e si sarebbe in grado di esaminare tutte le variabili locali dell'intero stack di chiamate a piacere.


1
assert something != null;ma ciò richiede il -assertionsflag quando si esegue l'app. Se la -assertionsbandiera non è presente, la parola chiave assert non genererà un'eccezione Assertion
Zoe,

Sono d'accordo, ecco perché preferisco qui la convenzione C #: riferimento null, argomento non valido e argomento null generalmente implicano un bug di qualche tipo, ma implicano diversi tipi di bug. "Stai cercando di utilizzare un riferimento null" è spesso molto diverso da "stai abusando della libreria".
EJoshuaS - Ripristina Monica

7

È perché è possibile che ciò non accada naturalmente. Vediamo un pezzo di codice come questo:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(ovviamente ci sono numerosi problemi in questo esempio sulla qualità del codice. Mi dispiace pigro immaginare un campione perfetto)

Situazione in cui cnon verrà effettivamente utilizzato e NullPointerExceptionnon verrà generato anche se c == nullpossibile.

In situazioni più complicate diventa molto facile cacciare questi casi. Ecco perché il controllo generale come if (c == null) throw new NullPointerException()è meglio.


Probabilmente, un pezzo di codice che funziona senza una connessione al database quando non è realmente necessario è una buona cosa, e il codice che si collega a un database solo per vedere se non riesce a farlo è di solito abbastanza fastidioso.
Dmitry Grigoryev,

5

È intenzionale proteggere ulteriori danni o entrare in uno stato incoerente.


1

Oltre a tutte le altre eccellenti risposte qui, vorrei anche aggiungere alcuni casi.

Puoi aggiungere un messaggio se crei la tua eccezione

Se lo fai NullPointerExceptiontu puoi aggiungere un messaggio (che sicuramente dovresti!)

Il messaggio predefinito è un nullda new NullPointerException()e tutti i metodi che lo utilizzano, ad esempio Objects.requireNonNull. Se lo stampi, puoi anche tradurlo in una stringa vuota ...

Un po 'corto e poco informativo ...

La traccia dello stack fornirà molte informazioni, ma affinché l'utente sappia cosa è null deve scavare il codice e guardare la riga esatta.

Ora immagina che l'NPE venga incartato e inviato in rete, ad esempio come un messaggio in un errore del servizio Web, forse tra dipartimenti diversi o persino organizzazioni. Nel peggiore dei casi, nessuno può capire cosa nullrappresenta ...

Le chiamate a metodi concatenati ti terranno indovinare

Un'eccezione ti dirà solo su quale riga si è verificata l'eccezione. Considera la seguente riga:

repository.getService(someObject.someMethod());

Se ottieni un NPE e punta a questa riga, quale di repositorye someObjectera nullo?

Al contrario, il controllo di queste variabili quando le ottieni indicherà almeno una riga in cui si spera siano l'unica variabile gestita. E, come detto prima, ancora meglio se il tuo messaggio di errore contiene il nome della variabile o simile.

Gli errori durante l'elaborazione di molti input dovrebbero fornire informazioni di identificazione

Immagina che il tuo programma stia elaborando un file di input con migliaia di righe e improvvisamente c'è una NullPointerException. Guardi il posto e ti rendi conto che alcuni input non sono corretti ... quali input? Avrai bisogno di maggiori informazioni sul numero di riga, forse sulla colonna o addirittura sull'intero testo della riga per capire quale riga in quel file deve essere riparata.

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.