Che cos'è una NullPointerException e come si risolve?
Cosa sono le eccezioni puntatore nullo ( java.lang.NullPointerException
) e cosa le causa?
Quali metodi / strumenti possono essere utilizzati per determinare la causa in modo da impedire all'eccezione di causare la chiusura anticipata del programma?
Risposte
Quando dichiari una variabile di riferimento (cioè un oggetto) stai realmente creando un puntatore a un oggetto. Considera il codice seguente in cui dichiari una variabile di tipo primitivo int
:
int x;
x = 10;
In questo esempio, la variabile x
è an int
e Java la inizializzerà 0
per te. Quando gli si assegna il valore di 10
sulla seconda riga, il valore di 10
viene scritto nella posizione di memoria a cui fa riferimento x
.
Ma, quando provi a dichiarare un tipo di riferimento , accade qualcosa di diverso. Prendi il codice seguente:
Integer num;
num = new Integer(10);
La prima riga dichiara una variabile denominata num
, ma in realtà non contiene ancora un valore primitivo. Invece, contiene un puntatore (perché il tipo è Integer
che è un tipo di riferimento). Dal momento che non hai ancora detto a cosa puntare, Java lo imposta null
, il che significa " Non sto indicando nulla ".
Nella seconda riga, la new
parola chiave viene utilizzata per istanziare (o creare) un oggetto di tipo Integer
e la variabile del puntatore num
viene assegnata a Integer
quell'oggetto.
La NullPointerException
verifica quando si dichiara una variabile, ma non ha creato un oggetto e assegnare alla variabile prima di tentare di utilizzare i contenuti della variabile (denominata dereferenziazione ). Quindi stai indicando qualcosa che in realtà non esiste.
La dereferenziazione di solito si verifica quando si utilizza .
per accedere a un metodo o campo, oppure si utilizza [
per indicizzare un array.
Se si tenta di dereferenziare num
PRIMA di creare l'oggetto, si ottiene un file NullPointerException
. Nei casi più banali, il compilatore rileverà il problema e ti informerà che " num may not have been initialized
," ma a volte potresti scrivere codice che non crea direttamente l'oggetto.
Ad esempio, potresti avere un metodo come segue:
public void doSomething(SomeObject obj) {
//do something to obj, assumes obj is not null
obj.myMethod();
}
In tal caso, non si crea l'oggetto obj
, ma si presume che sia stato creato prima della doSomething()
chiamata del metodo. Nota, è possibile chiamare il metodo in questo modo:
doSomething(null);
In tal caso, obj
is null
, e l'istruzione obj.myMethod()
genererà un file NullPointerException
.
Se il metodo ha lo scopo di fare qualcosa all'oggetto passato come fa il metodo precedente, è appropriato lanciare il NullPointerException
perché è un errore del programmatore e il programmatore avrà bisogno di queste informazioni per scopi di debug.
Oltre ai messaggi NullPointerException
generati come risultato della logica del metodo, puoi anche controllare gli argomenti del metodo per i null
valori e lanciare esplicitamente NPE aggiungendo qualcosa di simile all'inizio di un metodo:
//Throws an NPE with a custom error message if obj is null
Objects.requireNonNull(obj, "obj must not be null");
Nota che è utile dire chiaramente nel tuo messaggio di errore quale oggetto non può essere null
. Il vantaggio di eseguire una convalida in questo modo è che 1) puoi restituire i tuoi messaggi di errore più chiari e 2) per il resto del metodo sai che a meno che non obj
venga riassegnato, non è nullo e può essere dereferenziato in modo sicuro.
In alternativa, potrebbero esserci casi in cui lo scopo del metodo non è esclusivamente quello di operare sull'oggetto passato e quindi un parametro nullo può essere accettabile. In questo caso, dovresti verificare la presenza di un parametro nullo e comportarti in modo diverso. Dovresti anche spiegarlo nella documentazione. Ad esempio, doSomething()
potrebbe essere scritto come:
/**
* @param obj An optional foo for ____. May be null, in which case
* the result will be ____.
*/
public void doSomething(SomeObject obj) {
if(obj == null) {
//do something
} else {
//do something else
}
}
Infine, come individuare l'eccezione e la causa utilizzando Stack Trace
Quali metodi / strumenti possono essere utilizzati per determinare la causa in modo da impedire all'eccezione di causare la chiusura anticipata del programma?
Il sonar con bug di ricerca può rilevare NPE. Il sonar può rilevare eccezioni di puntatore nullo causate da JVM dinamicamente
Ora Java 14 ha aggiunto una nuova funzionalità del linguaggio per mostrare la causa principale di NullPointerException. Questa funzionalità del linguaggio fa parte della JVM commerciale di SAP dal 2006. Quanto segue è una lettura di 2 minuti per comprendere questa straordinaria funzionalità del linguaggio.
https://jfeatures.com/blog/NullPointerException
In java 14, di seguito è riportato un messaggio di eccezione NullPointerException di esempio:
nel thread "main" java.lang.NullPointerException: Impossibile richiamare "java.util.List.size ()" perché "list" è nullo
NullPointerException
Le eccezioni che si verificano quando si tenta di utilizzare un riferimento che non punta a nessuna posizione in memoria (null) come se stesse facendo riferimento a un oggetto. La chiamata a un metodo su un riferimento null o il tentativo di accedere a un campo di un riferimento null attiverà un NullPointerException
. Questi sono i più comuni, ma altri modi sono elencati nella NullPointerExceptionpagina javadoc.
Probabilmente il codice di esempio più veloce che potrei inventare per illustrare un NullPointerException
sarebbe:
public class Example {
public static void main(String[] args) {
Object obj = null;
obj.hashCode();
}
}
Nella prima riga interna main
, imposto esplicitamente il Object
riferimento obj
uguale a null
. Ciò significa che ho un riferimento, ma non punta a nessun oggetto. Dopodiché, provo a trattare il riferimento come se puntasse a un oggetto chiamando un metodo su di esso. Ciò si traduce in un NullPointerException
perché non è presente alcun codice da eseguire nella posizione a cui punta il riferimento.
(Questo è un tecnicismo, ma penso che valga la pena menzionarlo: un riferimento che punta a null non è lo stesso di un puntatore C che punta a una posizione di memoria non valida. Un puntatore null non punta letteralmente da nessuna parte , il che è leggermente diverso da che punta a una posizione che sembra non essere valida.)
Cos'è una NullPointerException?
Un buon punto di partenza è JavaDocs . Hanno questo coperto:
Generato quando un'applicazione tenta di utilizzare null in un caso in cui è richiesto un oggetto. Questi includono:
- Chiamata al metodo di istanza di un oggetto null.
- Accesso o modifica del campo di un oggetto nullo.
- Prendendo la lunghezza di null come se fosse un array.
- Accedere o modificare gli slot di null come se fosse un array.
- Lanciare null come se fosse un valore Throwable.
Le applicazioni dovrebbero lanciare istanze di questa classe per indicare altri usi illegali dell'oggetto null.
È anche il caso che se si tenta di utilizzare un riferimento nullo con synchronized
, verrà generata anche questa eccezione, per JLS :
SynchronizedStatement: synchronized ( Expression ) Block
- In caso contrario, se il valore di Expression è null,
NullPointerException
viene generato a.
Come lo aggiusto?
Quindi hai un file NullPointerException
. Come lo aggiusti? Facciamo un semplice esempio che genera un NullPointerException
:
public class Printer {
private String name;
public void setName(String name) {
this.name = name;
}
public void print() {
printString(name);
}
private void printString(String s) {
System.out.println(s + " (" + s.length() + ")");
}
public static void main(String[] args) {
Printer printer = new Printer();
printer.print();
}
}
Identifica i valori nulli
Il primo passo è identificare esattamente quali valori stanno causando l'eccezione . Per questo, dobbiamo fare un po 'di debug. È importante imparare a leggere uno stacktrace . Questo ti mostrerà dove è stata generata l'eccezione:
Exception in thread "main" java.lang.NullPointerException
at Printer.printString(Printer.java:13)
at Printer.print(Printer.java:9)
at Printer.main(Printer.java:19)
Qui, vediamo che l'eccezione viene lanciata sulla riga 13 (nel printString
metodo). Guarda la riga e controlla quali valori sono nulli aggiungendo istruzioni di registrazione o utilizzando un debugger . Scopriamo che s
è nullo e la chiamata al length
metodo su di esso genera l'eccezione. Possiamo vedere che il programma smette di lanciare l'eccezione quando s.length()
viene rimosso dal metodo.
Traccia da dove provengono questi valori
Quindi controlla da dove proviene questo valore. Seguendo i chiamanti del metodo, vediamo che s
viene passato con printString(name)
nel print()
metodo ed this.name
è nullo.
Traccia dove dovrebbero essere impostati questi valori
Dov'è this.name
ambientato? Nel setName(String)
metodo. Con un po 'più di debug, possiamo vedere che questo metodo non viene chiamato affatto. Se il metodo è stato chiamato, assicurati di controllare l' ordine in cui vengono chiamati questi metodi e il metodo set non viene chiamato dopo il metodo print.
Questo è sufficiente per darci una soluzione: aggiungi una chiamata a printer.setName()
prima di chiamare printer.print()
.
Altre correzioni
La variabile può avere un valore predefinito (e setName
può impedire che venga impostata su null):
private String name = "";
Il metodo print
o printString
può verificare la presenza di null , ad esempio:
printString((name == null) ? "" : name);
Oppure puoi progettare la classe in modo che name
abbia sempre un valore non nullo :
public class Printer {
private final String name;
public Printer(String name) {
this.name = Objects.requireNonNull(name);
}
public void print() {
printString(name);
}
private void printString(String s) {
System.out.println(s + " (" + s.length() + ")");
}
public static void main(String[] args) {
Printer printer = new Printer("123");
printer.print();
}
}
Guarda anche:
- Evitare le istruzioni "! = Null" in Java?
Non riesco ancora a trovare il problema
Se hai provato a eseguire il debug del problema e non hai ancora una soluzione, puoi pubblicare una domanda per ulteriore assistenza, ma assicurati di includere ciò che hai provato finora. Come minimo, includi lo stacktrace nella domanda e contrassegna i numeri di riga importanti nel codice. Inoltre, prova prima a semplificare il codice (vedi SSCCE ).
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(); // HERE
}
}
l'istruzione etichettata "HERE" tenterà di eseguire il length()
metodo su un null
riferimento e questo genererà un file NullPointerException
.
Ci sono molti modi in cui puoi usare 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, oinstanceof
.
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 sotto questo aspetto, 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 dellaTest
classe. - "Test.java:4" fornisce il nome del file sorgente della classe, E ci dice che l'istruzione in cui si è verificato si trova nella riga 4 del file.
Se conti le righe nel file sopra, la riga 4 è quella che ho etichettato con il commento "QUI".
Si noti 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(); // HERE
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
quindibar[pos]
lancerà un NPE. - Se il valore di
bar[pos]
ènull
quindi invitantelength()
, 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
.
È come se stessi tentando di accedere a un oggetto che è null
. Considera l'esempio seguente:
TypeA objA;
A questo punto hai appena dichiarato questo oggetto ma non è stato inizializzato o istanziato . E ogni volta che provi ad accedere a qualsiasi proprietà o metodo al suo interno, verrà visualizzato il messaggio NullPointerException
che ha senso.
Vedi anche questo esempio di seguito:
String a = null;
System.out.println(a.toString()); // NullPointerException will be thrown
Un'eccezione del puntatore null viene generata quando un'applicazione tenta di utilizzare null in un caso in cui è richiesto un oggetto. Questi includono:
- Chiamata al metodo di istanza di un
null
oggetto. - Accesso o modifica del campo di un
null
oggetto. - Prendendo la lunghezza di
null
come se fosse un array. - Accedere o modificare gli slot di
null
come se fosse un array. - Lanciare
null
come se fosse un valore Lanciabile.
Le applicazioni dovrebbero generare istanze di questa classe per indicare altri usi illegali null
dell'oggetto.
Riferimento: http://docs.oracle.com/javase/8/docs/api/java/lang/NullPointerException.html
Un null
puntatore è quello che punta a nulla. Quando si dereferenzia un puntatore p
, si dice "dammi i dati nella posizione memorizzata in" p ". Quando p
è un null
puntatore, la posizione memorizzata in p
è nowhere
, stai dicendo" dammi i dati nella posizione 'da nessuna parte' ". Ovviamente, non può farlo, quindi genera un file null pointer exception
.
In generale, è perché qualcosa non è stato inizializzato correttamente.
Sono già presenti molte spiegazioni per spiegare come accade e come risolverlo, ma dovresti anche seguire le migliori pratiche per evitare NullPointerExceptiondel tutto.
Vedi anche: Un buon elenco di best practice
Aggiungerei, molto importante, fare un buon uso del final
modificatore. Utilizzo del modificatore "finale" ogniqualvolta applicabile in Java
Sommario:
- Usa il
final
modificatore per imporre una buona inizializzazione. - Evita di restituire null nei metodi, ad esempio restituendo raccolte vuote quando applicabile.
- Usa annotazioni @NotNulle@Nullable
- Fallisci velocemente e usa asserzioni per evitare la propagazione di oggetti nulli attraverso l'intera applicazione quando non dovrebbero essere nulli.
- Usa prima uguale con un oggetto noto:
if("knownObject".equals(unknownObject)
- Preferiscono
valueOf()
sopratoString()
. - Utilizza StringUtilsmetodi null safe
StringUtils.isEmpty(null)
. - Usa Java 8 Optional come valore di ritorno nei metodi, la classe Optional fornisce una soluzione per rappresentare valori facoltativi invece di riferimenti nulli.
In Java, tutto (esclusi i tipi primitivi) ha la forma di una classe.
Se vuoi usare qualsiasi oggetto, hai due fasi:
- Dichiarare
- Inizializzazione
Esempio:
- Dichiarazione:
Object object;
- Inizializzazione:
object = new Object();
Lo stesso per il concetto di array:
- Dichiarazione:
Item item[] = new Item[5];
- Inizializzazione:
item[0] = new Item();
Se non stai dando la sezione di inizializzazione, allora il NullPointerException
sorgere.
Un'eccezione del puntatore nullo è un indicatore che stai utilizzando un oggetto senza inizializzarlo.
Ad esempio, di seguito è riportata una classe di studenti che la utilizzerà nel nostro codice.
public class Student {
private int id;
public int getId() {
return this.id;
}
public setId(int newId) {
this.id = newId;
}
}
Il codice seguente ti dà un'eccezione del puntatore nullo.
public class School {
Student student;
public School() {
try {
student.getId();
}
catch(Exception e) {
System.out.println("Null pointer exception");
}
}
}
Perché stai usando student
, ma hai dimenticato di inizializzarlo come nel codice corretto mostrato di seguito:
public class School {
Student student;
public School() {
try {
student = new Student();
student.setId(12);
student.getId();
}
catch(Exception e) {
System.out.println("Null pointer exception");
}
}
}
In Java tutte le variabili che dichiari sono in realtà "riferimenti" agli oggetti (o primitive) e non agli oggetti stessi.
Quando si tenta di eseguire un metodo oggetto, il riferimento chiede all'oggetto vivente di eseguire quel metodo. Ma se il riferimento fa riferimento a NULL (niente, zero, void, nada), non è possibile che il metodo venga eseguito. Quindi il runtime ti fa sapere questo lanciando una NullPointerException.
Il tuo riferimento "punta" a null, quindi "Null -> Pointer".
L'oggetto risiede nello spazio di memoria della VM e l'unico modo per accedervi è utilizzare i this
riferimenti. Prendi questo esempio:
public class Some {
private int id;
public int getId(){
return this.id;
}
public setId( int newId ) {
this.id = newId;
}
}
E in un altro punto del codice:
Some reference = new Some(); // Point to a new object of type Some()
Some otherReference = null; // Initiallly this points to NULL
reference.setId( 1 ); // Execute setId method, now private var id is 1
System.out.println( reference.getId() ); // Prints 1 to the console
otherReference = reference // Now they both point to the only object.
reference = null; // "reference" now point to null.
// But "otherReference" still point to the "real" object so this print 1 too...
System.out.println( otherReference.getId() );
// Guess what will happen
System.out.println( reference.getId() ); // :S Throws NullPointerException because "reference" is pointing to NULL remember...
Questa è una cosa importante da sapere - quando non ci sono più riferimenti a un oggetto (nell'esempio sopra quando reference
ed otherReference
entrambi puntano a null) allora l'oggetto è "irraggiungibile". Non è possibile lavorarci sopra, quindi questo oggetto è pronto per essere sottoposto a garbage collection e, a un certo punto, la VM libererà la memoria utilizzata da questo oggetto e ne assegnerà un altro.
Un'altra occorrenza di a si NullPointerException
verifica quando si dichiara un array di oggetti, quindi si cerca immediatamente di dereferenziare gli elementi al suo interno.
String[] phrases = new String[10];
String keyPhrase = "Bird";
for(String phrase : phrases) {
System.out.println(phrase.equals(keyPhrase));
}
Questo particolare NPE può essere evitato invertendo l'ordine di confronto; vale a dire, l'uso .equals
su un oggetto non nullo garantito.
Tutti gli elementi all'interno di un array vengono inizializzati al loro valore iniziale comune ; per qualsiasi tipo di array di oggetti, ciò significa che tutti gli elementi lo sono null
.
È necessario inizializzare gli elementi nell'array prima di accedervi o dereferenziarli.
String[] phrases = new String[] {"The bird", "A bird", "My bird", "Bird"};
String keyPhrase = "Bird";
for(String phrase : phrases) {
System.out.println(phrase.equals(keyPhrase));
}