Perché i miei campi sono inizializzati su null o sul valore predefinito di zero quando li ho dichiarati e inizializzati nel costruttore della mia class?

Questo dovrebbe essere una domanda e una risposta canonica per domande simili dove il problema è il risultato dell’ombra .


Ho definito due campi nella mia class, uno di un tipo di riferimento e uno di tipo primitivo. Nel costruttore della class, provo a inizializzarli con alcuni valori personalizzati.

Quando successivamente eseguo una query per i valori di quei campi, ritornano con i valori predefiniti di Java per essi, null per il tipo di riferimento e 0 per il tipo primitivo. Perché sta succedendo?

Ecco un esempio riproducibile:

 public class Sample { public static void main(String[] args) throws Exception { StringArray array = new StringArray(); System.out.println(array.getCapacity()); // prints 0 System.out.println(array.getElements()); // prints null } } class StringArray { private String[] elements; private int capacity; public StringArray() { int capacity = 10; String[] elements; elements = new String[capacity]; } public int getCapacity() { return capacity; } public String[] getElements() { return elements; } } 

Mi aspettavo getCapacity() per restituire il valore 10 e getElements() per restituire un’istanza dell’array correttamente inizializzata.

Le quadro (pacchetti, tipi, metodi, variabili, ecc.) Definite in un programma Java hanno nomi . Questi sono usati per riferirsi a quelle quadro in altre parti di un programma.

Il linguaggio Java definisce uno scope per ciascun nome

L’ ambito di una dichiarazione è la regione del programma all’interno della quale l’ quadro dichiarata dalla dichiarazione può essere riferita usando un nome semplice, a condizione che sia visibile (§6.4.1).

In altre parole, scope è un concetto di tempo di compilazione che determina dove un nome può essere usato per riferirsi a qualche quadro di programma.

Il programma che hai pubblicato ha più dichiarazioni. Iniziamo con

 private String[] elements; private int capacity; 

Queste sono dichiarazioni di campo , chiamate anche variabili di istanza , es. un tipo di membro dichiarato in un corpo di class . Gli stati della specifica del linguaggio Java

L’ambito di una dichiarazione di un membro m dichiarato in o ereditato da un tipo di class C (§8.1.6) è l’intero corpo di C , comprese le dichiarazioni di tipo annidate.

Ciò significa che è ansible utilizzare gli elements nomi e la capacity all’interno del corpo di StringArray per fare riferimento a tali campi.

Le prime due dichiarazioni nel tuo corpo del costruttore

 public StringArray() { int capacity = 10; String[] elements; elements = new String[capacity]; } 

sono in realtà dichiarazioni di dichiarazione delle variabili locali

Una dichiarazione di dichiarazione di variabili locali dichiara uno o più nomi di variabili locali.

Queste due affermazioni introducono due nuovi nomi nel tuo programma. Accade solo che quei nomi siano uguali ai tuoi campi “. Nel tuo esempio, la dichiarazione di variabile locale per capacity contiene anche un inizializzatore che inizializza quella variabile locale , non il campo con lo stesso nome. Il tuo campo denominato capacity è inizializzato sul valore predefinito per il suo tipo, es. il valore 0 .

Il caso degli elements è un po ‘diverso. La dichiarazione di dichiarazione delle variabili locali introduce un nuovo nome, ma per quanto riguarda l’ espressione di assegnazione ?

 elements = new String[capacity]; 

A quale quadro si riferiscono gli elements ?

Le regole dello stato di portata

L’ambito di una dichiarazione di variabile locale in un blocco (§14.4) è il resto del blocco in cui appare la dichiarazione, a partire dal proprio inizializzatore e includendo qualsiasi altro dichiaratore a destra nell’istruzione di dichiarazione delle variabili locali.

Il blocco, in questo caso, è il corpo del costruttore. Ma il corpo del costruttore è parte del corpo di StringArray , il che significa che i nomi dei campi sono anche in ambito. Quindi, come fa Java a determinare a cosa ti stai riferendo?

Java introduce il concetto di Shadowing per disambiguare.

Alcune dichiarazioni possono essere ombreggiate in parte del loro ambito da un’altra dichiarazione con lo stesso nome, nel qual caso un nome semplice non può essere utilizzato per fare riferimento all’ quadro dichiarata.

(un nome semplice è un identificatore singolo, ad esempio elements .)

La documentazione afferma anche

Una dichiarazione d di una variabile locale o un parametro di eccezione denominato n shadows , in tutto l’ambito di d , (a) le dichiarazioni di tutti gli altri campi denominati n che sono in ambito nel punto in cui si verifica d e (b) le dichiarazioni di qualsiasi altre variabili denominate n che si trovano nell’ambito nel punto in cui si verifica d ma non sono dichiarate nella class più interna in cui d è dichiarato.

Ciò significa che la variabile locale denominata elements ha la priorità sul campo denominato elements . L’espressione

 elements = new String[capacity]; 

sta quindi inizializzando la variabile locale, non il campo. Il campo è inizializzato sul valore predefinito per il suo tipo, es. il valore null .

All’interno dei tuoi metodi getCapacity e getElements , i nomi utilizzati nelle rispettive dichiarazioni di return riferiscono ai campi poiché le loro dichiarazioni sono le uniche in ambito in quel particolare punto del programma. Poiché i campi sono stati inizializzati su 0 e null , questi sono i valori restituiti.

La soluzione è di eliminare del tutto le dichiarazioni delle variabili locali e quindi i nomi si riferiscono alle variabili di istanza, come originariamente desiderato. Per esempio

 public StringArray() { capacity = 10; elements = new String[capacity]; } 

Ombre con i parametri del costruttore

Simile alla situazione descritta sopra, potresti avere parametri formali (costruttore o metodo) che ombreggiano i campi con lo stesso nome. Per esempio

 public StringArray(int capacity) { capacity = 10; } 

Lo stato delle regole ombreggiate

Una dichiarazione d di un campo o parametro formale denominato n shadows, in tutto lo scope di d , le dichiarazioni di qualsiasi altra variabile denominata n che si trovano nell’ambito nel punto in cui si verifica d .

Nell’esempio precedente, la dichiarazione della capacity parametro del costruttore ombreggia la dichiarazione della variabile di istanza denominata anche capacity . È quindi imansible fare riferimento alla variabile di istanza con il suo nome semplice. In questi casi, dobbiamo fare riferimento ad esso con il suo nome qualificato .

Un nome qualificato è costituito da un nome, un “.” token e un identificatore.

In questo caso, possiamo usare l’ espressione primaria come parte di un’espressione di accesso al campo per fare riferimento alla variabile di istanza. Per esempio

 public StringArray(int capacity) { this.capacity = 10; // to initialize the field with the value 10 // or this.capacity = capacity; // to initialize the field with the value of the constructor argument } 

Esistono regole Shadowing per ogni tipo di variabile , metodo e tipo.

La mia raccomandazione è di usare nomi univoci, laddove ansible, in modo da evitare del tutto il comportamento.

int capacity = 10; nel tuo costruttore sta dichiarando una capacity variabile locale che oscura il campo della class.

Il rimedio è di eliminare l’ int :

capacity = 10;

Questo cambierà il valore del campo. Idem per l’altro campo della class.

Il tuo IDE non ti ha avvertito di questo shadowing?

Un’altra convenzione ampiamente accettata è quella di avere un prefisso (o suffisso – qualunque cosa tu preferisca) aggiunto ai membri della class per distinguerli dalle variabili locali.

Ad esempio i membri della class con prefisso m_ :

 class StringArray { private String[] m_elements; private int m_capacity; public StringArray(int capacity) { m_capacity = capacity; m_elements = new String[capacity]; } public int getCapacity() { return m_capacity; } public String[] getElements() { return m_elements; } } 

La maggior parte degli IDE ha già il supporto disponibile per questa notazione, di seguito è riportato per Eclipse

inserisci la descrizione dell'immagine qui

Ci sono due parti per usare le variabili in java / c / c ++. Uno è dichiarare la variabile e l’altro è usare la variabile (se si assegna un valore o lo si utilizza in un calcolo).

Quando dichiari una variabile devi dichiararne il tipo. Quindi useresti

 int x; // to declare the variable x = 7; // to set its value 

Non devi ri-dichiarare una variabile quando la usi:

 int x; int x = 7; 

se la variabile è nello stesso ambito, si otterrà un errore del compilatore; tuttavia, come stai scoprendo, se la variabile è in un ambito diverso maschererai la prima dichiarazione.