Java Language
Insidie di Java - Nulls e NullPointerException
Ricerca…
Osservazioni
Il valore null
è il valore predefinito per un valore non inizializzato di un campo il cui tipo è un tipo di riferimento.
NullPointerException
(o NPE) è l'eccezione che viene generata quando si tenta di eseguire un'operazione inappropriata sul riferimento all'oggetto null
. Tali operazioni includono:
- chiamando un metodo di istanza su un oggetto di destinazione
null
, - accedere a un campo di un oggetto obiettivo
null
, - tentando di indicizzare un oggetto array
null
o accedere alla sua lunghezza, - usando un riferimento oggetto
null
come mutex in un bloccosynchronized
, - casting di un oggetto
null
riferimento, - unboxing di un riferimento a oggetti
null
e - lancio di un riferimento a oggetto
null
.
Le cause principali più comuni per gli NPE:
- dimenticando di inizializzare un campo con un tipo di riferimento,
- dimenticando di inizializzare elementi di una matrice di un tipo di riferimento, o
- non testare i risultati di determinati metodi API che vengono specificati come restituire
null
in determinate circostanze.
Esempi di metodi comunemente usati che restituiscono null
includono:
- Il metodo
get(key)
nell'API diMap
restituirà un valorenull
se lo chiami con una chiave che non ha una mappatura. - I
getResource(path)
egetResourceAsStream(path)
nelle APIClassLoader
eClass
restituirannonull
se non è possibile trovare la risorsa. - Il metodo
get()
nell'API diReference
restituirànull
se il garbage collector ha cancellato il riferimento. - Vari metodi
getXxxx
nelle API servlet Java EE restituiscononull
se si tenta di recuperare un parametro di richiesta inesistente, un attributo di sessione o sessione e così via.
Esistono strategie per evitare NPE indesiderati, come test espliciti per null
o usando "Yoda Notation", ma queste strategie spesso hanno il risultato indesiderato di nascondere i problemi nel codice che dovrebbero essere corretti.
Pitfall - L'uso non necessario di Primitive Wrapper può portare a NullPointerExceptions
A volte, i programmatori che sono nuovi Java useranno i tipi primitivi e i wrapper in modo intercambiabile. Questo può portare a problemi. Considera questo esempio:
public class MyRecord {
public int a, b;
public Integer c, d;
}
...
MyRecord record = new MyRecord();
record.a = 1; // OK
record.b = record.b + 1; // OK
record.c = 1; // OK
record.d = record.d + 1; // throws a NullPointerException
La MyRecord
classe MyRecord
1 si basa MyRecord
predefinita per inizializzare i valori sui suoi campi. Quindi, quando creiamo un new
record, i campi a
e b
saranno impostati a zero ei campi c
e d
saranno impostati su null
.
Quando proviamo a utilizzare i campi inizializzati predefiniti, vediamo che i campi int
funzionano sempre, ma i campi Integer
funzionano in alcuni casi e non in altri. Nello specifico, nel caso in cui non riesca (con d
), ciò che accade è che l'espressione sul lato destro tenta di annullare la selezione di un riferimento null
, e questo è il NullPointerException
per cui viene generata l' NullPointerException
.
Ci sono un paio di modi per osservare questo:
Se i campi
c
ed
devono essere wrapper primitivi, allora non dovremmo fare affidamento sull'inizializzazione di default, o dovremmo testare pernull
. Per ex è l'approccio corretto a meno che non vi sia un significato definito per i campi nello statonull
.Se i campi non hanno bisogno di essere involucri primitivi, allora è un errore renderli involucri primitivi. Oltre a questo problema, i wrapper primitivi hanno overhead aggiuntivi rispetto ai tipi primitivi.
La lezione qui è di non usare tipi di wrapper primitivi a meno che tu non ne abbia davvero bisogno.
1 - Questa classe non è un esempio di buona pratica di codifica. Ad esempio, una classe ben progettata non avrebbe campi pubblici. Tuttavia, questo non è il punto di questo esempio.
Pitfall - Uso di null per rappresentare una matrice o una raccolta vuota
Alcuni programmatori pensano che sia una buona idea risparmiare spazio usando un null
per rappresentare una matrice o una collezione vuota. Anche se è vero che puoi risparmiare una piccola quantità di spazio, il rovescio della medaglia è che rende il tuo codice più complicato e più fragile. Confronta queste due versioni di un metodo per sommare un array:
La prima versione è come si codificherà normalmente il metodo:
/**
* Sum the values in an array of integers.
* @arg values the array to be summed
* @return the sum
**/
public int sum(int[] values) {
int sum = 0;
for (int value : values) {
sum += value;
}
return sum;
}
La seconda versione è il modo in cui è necessario codificare il metodo se si ha l'abitudine di utilizzare null
per rappresentare una matrice vuota.
/**
* Sum the values in an array of integers.
* @arg values the array to be summed, or null.
* @return the sum, or zero if the array is null.
**/
public int sum(int[] values) {
int sum = 0;
if (values != null) {
for (int value : values) {
sum += value;
}
}
return sum;
}
Come puoi vedere, il codice è un po 'più complicato. Questo è direttamente attribuibile alla decisione di utilizzare null
in questo modo.
Considerare ora se questa matrice che potrebbe essere un null
viene utilizzata in molti posti. In ogni luogo in cui lo si utilizza, è necessario considerare se è necessario eseguire il test per null
. Se si perde un test null
che deve essere presente, si rischia una NullPointerException
. Quindi, la strategia di usare null
in questo modo porta a rendere la tua applicazione più fragile; cioè più vulnerabile alle conseguenze degli errori del programmatore.
La lezione qui è usare matrici vuote e liste vuote quando questo è ciò che intendi.
int[] values = new int[0]; // always empty
List<Integer> list = new ArrayList(); // initially empty
List<Integer> list = Collections.emptyList(); // always empty
L'overhead dello spazio è piccolo e ci sono altri modi per ridurlo al minimo se questa è una cosa utile da fare.
Pitfall - "Making good" null inaspettati
Su StackOverflow, spesso vediamo un codice come questo in Answers:
public String joinStrings(String a, String b) {
if (a == null) {
a = "";
}
if (b == null) {
b = "";
}
return a + ": " + b;
}
Spesso, questo è accompagnato da un'asserzione che è "best practice" per testare null
come questo per evitare NullPointerException
.
È la migliore pratica? In breve: No.
Ci sono alcune ipotesi sottostanti che devono essere messe in discussione prima che possiamo dire se è una buona idea farlo nelle nostre joinStrings
:
Che cosa significa "a" o "b" essere nulli?
Un valore di String
può essere zero o più caratteri, quindi abbiamo già un modo per rappresentare una stringa vuota. null
significa qualcosa di diverso da ""
? Se no, allora è problematico avere due modi per rappresentare una stringa vuota.
Il null proviene da una variabile non inizializzata?
Un null
può provenire da un campo non inizializzato o da un elemento di matrice non inizializzata. Il valore potrebbe non essere inizializzato dal design o per errore. Se è stato per caso, questo è un bug.
Il null rappresenta un "non so" o "un valore mancante"?
A volte un null
può avere un significato genuino; per esempio che il valore reale di una variabile è sconosciuto o non disponibile o "opzionale". In Java 8, la classe Optional
fornisce un modo migliore di esprimerlo.
Se questo è un bug (o un errore di progettazione) dovremmo "fare del bene"?
Un'interpretazione del codice è che stiamo "facendo del bene" un null
inaspettato usando una stringa vuota al suo posto. È la strategia corretta? Sarebbe meglio lasciare che NullPointerException
verifichi, quindi catturare l'eccezione più in alto nello stack e registrarlo come un bug?
Il problema di "fare del bene" è che è in grado di nascondere il problema o renderlo più difficile da diagnosticare.
Questo è efficiente / buono per la qualità del codice?
Se l'approccio del "buon funzionamento" viene utilizzato in modo coerente, il tuo codice conterrà molti test nulli "difensivi". Questo renderà più lungo e difficile da leggere. Inoltre, tutto questo test e "fare il bravo" è suscettibile di influire sulle prestazioni della vostra applicazione.
In sintesi
Se null
è un valore significativo, il test per il caso null
è l'approccio corretto. Il corollario è che se un valore null
è significativo, questo dovrebbe essere chiaramente documentato nei javadoc di tutti i metodi che accettano il valore null
o lo restituiscono.
In caso contrario, si tratta di un'idea migliore per il trattamento di un inaspettato null
come un errore di programmazione, e lasciare che il NullPointerException
accadere in modo che lo sviluppatore viene a sapere che c'è un problema nel codice.
Pitfall - Restituisce null invece di generare un'eccezione
Alcuni programmatori Java hanno un'avversione generale a lanciare o propagare eccezioni. Questo porta al codice come il seguente:
public Reader getReader(String pathname) {
try {
return new BufferedReader(FileReader(pathname));
} catch (IOException ex) {
System.out.println("Open failed: " + ex.getMessage());
return null;
}
}
Quindi qual è il problema?
Il problema è che getReader
restituisce un valore null
come valore speciale per indicare che il Reader
non può essere aperto. Ora il valore restituito deve essere testato per vedere se è null
prima che venga utilizzato. Se il test viene NullPointerException
, il risultato sarà NullPointerException
.
Ci sono in realtà tre problemi qui:
- L'
IOException
stato catturato troppo presto. - La struttura di questo codice significa che esiste il rischio di perdita di una risorsa.
- È stato utilizzato un valore
null
quindi è stato restituito perché nessunReader
"reale" era disponibile per il reso.
In effetti, supponendo che l'eccezione abbia dovuto essere catturata in anticipo in questo modo, c'erano un paio di alternative per restituire null
:
- Sarebbe possibile implementare una classe
NullReader
; ad esempio quello in cui le operazioni dell'API si comportano come se il lettore fosse già nella posizione di "fine del file". - Con Java 8, sarebbe possibile dichiarare
getReader
come restituire unOptional<Reader>
.
Pitfall - Non verificare se un flusso I / O non è nemmeno inizializzato quando si chiude
Per evitare perdite di memoria, non si dovrebbe dimenticare di chiudere un flusso di input o un flusso di output il cui lavoro è stato eseguito. Questo di solito è fatto con una frase try
- catch
- finally
senza la parte catch
:
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
out.close();
}
}
Mentre il codice sopra potrebbe sembrare innocente, ha un difetto che può rendere impossibile il debugging. Se la riga dove out
è inizializzata ( out = new FileOutputStream(filename)
) genera un'eccezione, quindi out
sarà null
quando out.close()
viene eseguito, risultando in una brutta NullPointerException
!
Per evitare ciò, assicurati semplicemente che lo stream non sia null
prima di provare a chiuderlo.
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
if (out != null)
out.close();
}
}
Un approccio ancora migliore è quello di try
-con-risorse, dal momento che sarà chiudere automaticamente il flusso con una probabilità da 0 a gettare un NPE, senza la necessità di un finally
bloccare.
void writeNullBytesToAFile(int count, String filename) throws IOException {
try (FileOutputStream out = new FileOutputStream(filename)) {
for(; count > 0; count--)
out.write(0);
}
}
Pitfall - Usando la "notazione Yoda" per evitare NullPointerException
Un sacco di codice di esempio pubblicato su StackOverflow include snippet come questo:
if ("A".equals(someString)) {
// do something
}
Ciò "previene" o "evita" una possibile NullPointerException
nel caso in cui someString
sia null
. Inoltre, è discutibile che
"A".equals(someString)
è meglio di:
someString != null && someString.equals("A")
(È più conciso, e in alcune circostanze potrebbe essere più efficiente. Tuttavia, come discuteremo di seguito, la concisione potrebbe essere negativa.)
Tuttavia, la vera trappola consiste nell'usare il test Yoda per evitare NullPointerExceptions
come una questione di abitudine.
Quando scrivi "A".equals(someString)
realtà stai "facendo del bene" nel caso in cui someString
sia null
. Ma come un altro esempio ( Pitfall - "Making good" null inaspettati ) spiega, "fare buoni" valori null
può essere dannoso per una serie di motivi.
Ciò significa che le condizioni Yoda non sono "best practice" 1 . A meno che non sia previsto il valore null
, è meglio consentire l' NullPointerException
modo da ottenere un errore di test dell'unità (o una segnalazione di errore). Ciò consente di trovare e correggere il bug che ha causato l'apparizione del null
inaspettato / indesiderato.
Le condizioni Yoda devono essere utilizzate solo nei casi in cui è previsto il valore null
perché l'oggetto che si sta provando proviene da un'API documentata che restituisce un valore null
. E, discutibilmente, potrebbe essere meglio usare uno dei modi meno carini per esprimere il test, perché questo aiuta a mettere in evidenza il test null
a qualcuno che sta rivedendo il tuo codice.
1 - Secondo Wikipedia : "Le migliori pratiche di codifica sono un insieme di regole informali che la comunità di sviluppo del software ha imparato nel tempo e che possono aiutare a migliorare la qualità del software." . L'uso della notazione Yoda non raggiunge questo risultato. In molte situazioni, peggiora il codice.