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 null
che è il modo Java di dire "nessun oggetto".
A NullPointerException
viene lanciato in fase di esecuzione ogni volta che il programma tenta di utilizzare a null
come 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 null
riferimento e questo genererà un file NullPointerException
.
Esistono molti modi per utilizzare un null
valore che risulterà in un file NullPointerException
. In effetti, le uniche cose che puoi fare con un null
senza 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 javac
compilatore 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
main
metodo della Test
classe.
- "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 foo
ha il valore null
. Proviamo quindi a eseguire il length()
metodo null
e ... 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.String
classe e la riga 4 di Test.java
sarebbe la seconda riga "at".
Allora da dove null
viene? 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
è null
quindi bar[pos]
lancerà un NPE.
- Se il valore di
bar[pos]
è null
quindi invitante length()
, genererà un NPE.
Successivamente, dobbiamo capire quale di questi scenari spiega cosa sta realmente accadendo. Inizieremo esplorando il primo:
Da dove bar
viene? È un parametro per la test
chiamata al metodo e se guardiamo come è test
stato chiamato, possiamo vedere che proviene dalla foo
variabile statica. Inoltre, possiamo vedere chiaramente che abbiamo inizializzato foo
su 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
.