Domanda: cosa causa un NullPointerException(NPE)?
Come dovreste sapere, tipi Java sono divisi in tipi primitivi ( boolean, int, ecc) e tipi di riferimento . I tipi di riferimento in Java consentono di utilizzare il valore speciale nullche è il modo Java di dire "nessun oggetto".
A NullPointerExceptionviene lanciato in fase di esecuzione ogni volta che il programma tenta di utilizzare a nullcome se fosse un riferimento reale. Ad esempio, se scrivi questo:
public class Test {
public static void main(String[] args) {
String foo = null;
int length = foo.length();
}
}
l'istruzione etichettata "HERE" tenterà di eseguire il length()metodo su un nullriferimento e questo genererà un file NullPointerException.
Esistono molti modi per utilizzare un nullvalore che risulterà in un file NullPointerException. In effetti, le uniche cose che puoi fare con un nullsenza causare un NPE sono:
- assegnarlo a una variabile di riferimento o leggerlo da una variabile di riferimento,
- assegnarlo a un elemento dell'array o leggerlo da un elemento dell'array (a condizione che il riferimento all'array stesso non sia nullo!),
- passarlo come parametro o restituirlo come risultato, o
- testarlo utilizzando gli
==o !=operatori, o instanceof.
Domanda: come leggo lo stacktrace NPE?
Supponiamo che io compili ed esegua il programma sopra:
$ javac Test.java
$ java Test
Exception in thread "main" java.lang.NullPointerException
at Test.main(Test.java:4)
$
Prima osservazione: la compilazione riesce! Il problema nel programma NON è un errore di compilazione. È un errore di runtime . (Alcuni IDE potrebbero avvertire che il tuo programma genererà sempre un'eccezione ... ma il javaccompilatore standard no.)
Seconda osservazione: quando eseguo il programma, emette due righe di "gobbledy-gook". SBAGLIATO!! Non è gobbledy-gook. È uno stacktrace ... e fornisce informazioni vitali che ti aiuteranno a rintracciare l'errore nel tuo codice se ti prendi il tempo di leggerlo attentamente.
Quindi diamo un'occhiata a cosa dice:
Exception in thread "main" java.lang.NullPointerException
La prima riga della traccia dello stack ti dice una serie di cose:
- Ti dice il nome del thread Java in cui è stata generata l'eccezione. Per un programma semplice con un thread (come questo), sarà "main". Andiamo avanti ...
- Ti dice il nome completo dell'eccezione che è stata lanciata; cioè
java.lang.NullPointerException.
- Se l'eccezione ha un messaggio di errore associato, verrà emesso dopo il nome dell'eccezione.
NullPointerExceptionè insolito in questo senso, perché raramente ha un messaggio di errore.
La seconda riga è la più importante nella diagnosi di un NPE.
at Test.main(Test.java:4)
Questo ci dice una serie di cose:
- "at Test.main" dice che eravamo nel
mainmetodo della Testclasse.
- "Test.java:4" fornisce il nome del file sorgente della classe, E ci dice che l'istruzione in cui si è verificato è nella riga 4 del file.
Se conti le righe nel file sopra, la riga 4 è quella che ho etichettato con il commento "QUI".
Nota che in un esempio più complicato, ci saranno molte righe nella traccia dello stack NPE. Ma puoi essere certo che la seconda riga (la prima riga "at") ti dirà dove è stato lanciato l'NPE 1 .
In breve, la traccia dello stack ci dirà in modo inequivocabile quale affermazione del programma ha generato l'NPE.
1 - Non proprio vero. Ci sono cose chiamate eccezioni annidate ...
Domanda: come faccio a rintracciare la causa dell'eccezione NPE nel mio codice?
Questa è la parte difficile. La risposta breve è applicare l'inferenza logica all'evidenza fornita dalla traccia dello stack, dal codice sorgente e dalla documentazione API pertinente.
Illustriamo prima con il semplice esempio (sopra). Iniziamo osservando la riga che la traccia dello stack ci ha detto è dove si è verificato l'NPE:
int length = foo.length();
Come può generare un NPE?
In effetti, c'è un solo modo: può accadere solo se fooha il valore null. Proviamo quindi a eseguire il length()metodo nulle ... BANG!
Ma (ti sento dire) cosa succederebbe se l'NPE fosse stato lanciato all'interno della length()chiamata al metodo?
Bene, se ciò accadesse, la traccia dello stack apparirebbe diversa. La prima riga "at" direbbe che l'eccezione è stata lanciata in una riga della java.lang.Stringclasse e la riga 4 di Test.javasarebbe la seconda riga "at".
Allora da dove nullviene? In questo caso, è ovvio ed è ovvio cosa dobbiamo fare per risolverlo. (Assegna un valore non nullo a foo.)
OK, quindi proviamo un esempio leggermente più complicato. Ciò richiederà una deduzione logica .
public class Test {
private static String[] foo = new String[2];
private static int test(String[] bar, int pos) {
return bar[pos].length();
}
public static void main(String[] args) {
int length = test(foo, 1);
}
}
$ javac Test.java
$ java Test
Exception in thread "main" java.lang.NullPointerException
at Test.test(Test.java:6)
at Test.main(Test.java:10)
$
Quindi ora abbiamo due righe "at". Il primo è per questa linea:
return args[pos].length();
e il secondo è per questa linea:
int length = test(foo, 1);
Guardando la prima riga, come potrebbe generare un NPE? Ci sono due modi:
- Se il valore di
barè nullquindi bar[pos]lancerà un NPE.
- Se il valore di
bar[pos]è nullquindi invitante length(), genererà un NPE.
Successivamente, dobbiamo capire quale di questi scenari spiega cosa sta realmente accadendo. Inizieremo esplorando il primo:
Da dove barviene? È un parametro per la testchiamata al metodo e se guardiamo come è teststato chiamato, possiamo vedere che proviene dalla foovariabile statica. Inoltre, possiamo vedere chiaramente che abbiamo inizializzato foosu un valore non nullo. Ciò è sufficiente per respingere provvisoriamente questa spiegazione. (In teoria, qualcos'altro potrebbe cambiare foo in null... ma questo non sta accadendo qui.)
E il nostro secondo scenario? Bene, possiamo vedere che posè 1, quindi significa che foo[1]deve essere null. È possibile?
Certo che lo è! E questo è il problema. Quando inizializziamo in questo modo:
private static String[] foo = new String[2];
assegniamo a String[]con due elementi che vengono inizializzatinull . Dopodiché, non abbiamo cambiato il contenuto di foo... così foo[1]sarà ancora null.