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 blocco synchronized ,
  • 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 di Map restituirà un valore null se lo chiami con una chiave che non ha una mappatura.
  • I getResource(path) e getResourceAsStream(path) nelle API ClassLoader e Class restituiranno null se non è possibile trovare la risorsa.
  • Il metodo get() nell'API di Reference restituirà null se il garbage collector ha cancellato il riferimento.
  • Vari metodi getXxxx nelle API servlet Java EE restituiscono null 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 e d devono essere wrapper primitivi, allora non dovremmo fare affidamento sull'inizializzazione di default, o dovremmo testare per null . Per ex è l'approccio corretto a meno che non vi sia un significato definito per i campi nello stato null .

  • 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:

  1. L' IOException stato catturato troppo presto.
  2. La struttura di questo codice significa che esiste il rischio di perdita di una risorsa.
  3. È stato utilizzato un valore null quindi è stato restituito perché nessun Reader "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 :

  1. 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".
  2. Con Java 8, sarebbe possibile dichiarare getReader come restituire un Optional<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.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow