Perché le variabili locali non vengono inizializzate in Java?


103

C'era qualche ragione per cui i progettisti di Java ritenevano che alle variabili locali non dovesse essere assegnato un valore predefinito? Seriamente, se alle variabili di istanza può essere assegnato un valore predefinito, perché non possiamo fare lo stesso per le variabili locali?

E porta anche a problemi come spiegato in questo commento a un post sul blog :

Bene, questa regola è molto frustrante quando si cerca di chiudere una risorsa in un blocco finale. Se istanzio la risorsa all'interno di una prova, ma provo a chiuderla entro la fine, ottengo questo errore. Se sposto l'istanza al di fuori del tentativo, ottengo un altro errore che indica che a deve essere all'interno di un tentativo.

Molto frustrante.



1
Mi dispiace per questo ... questa domanda non è comparsa quando stavo digitando la domanda .. tuttavia, immagino che ci sia una differenza tra le due domande ... Voglio sapere perché i designer di Java l'hanno progettata in questo modo, mentre la domanda che hai indicato non chiede che ...
Shivasubramanian A

Vedi anche questa domanda C # correlata: stackoverflow.com/questions/1542824/…
Raedwald

Semplicemente, perché è facile per il compilatore tenere traccia delle variabili locali non inizializzate. Se potesse fare lo stesso con altre variabili, lo farebbe. Il compilatore sta solo cercando di aiutarti.
rustyx

Risposte:


62

Le variabili locali vengono dichiarate principalmente per eseguire alcuni calcoli. Quindi è la decisione del programmatore di impostare il valore della variabile e non dovrebbe assumere un valore predefinito. Se il programmatore, per errore, non ha inizializzato una variabile locale e assume il valore predefinito, l'output potrebbe essere un valore imprevisto. Quindi, in caso di variabili locali, il compilatore chiederà al programmatore di inizializzare con un valore prima di accedere alla variabile per evitare l'utilizzo di valori indefiniti.


23

Il "problema" a cui ti colleghi sembra descrivere questa situazione:

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

La lamentela del commentatore è che il compilatore si oppone alla riga nella finallysezione, sostenendo che sopotrebbe essere non inizializzata. Il commento menziona quindi un altro modo di scrivere il codice, probabilmente qualcosa del genere:

// Do some work here ...
SomeObject so = new SomeObject();
try {
  so.DoUsefulThings();
} finally {
  so.CleanUp();
}

Il commentatore non è soddisfatto di quella soluzione perché il compilatore dice quindi che il codice "deve essere all'interno di una prova". Immagino che ciò significhi che parte del codice potrebbe sollevare un'eccezione che non viene più gestita. Non ne sono sicuro. Nessuna delle versioni del mio codice gestisce eccezioni, quindi qualsiasi cosa relativa alle eccezioni nella prima versione dovrebbe funzionare allo stesso modo nella seconda.

Comunque, questa seconda versione del codice è il modo corretto per scriverlo. Nella prima versione, il messaggio di errore del compilatore era corretto. La sovariabile potrebbe non essere inizializzata. In particolare, se il SomeObjectcostruttore fallisce, sonon verrà inizializzato e quindi sarà un errore tentare di chiamare so.CleanUp. Entra sempre nella trysezione dopo aver acquisito la risorsa che la finallysezione finalizza.

Il blocco try- finallydopo l' soinizializzazione serve solo a proteggere l' SomeObjectistanza, per assicurarsi che venga ripulita indipendentemente da cos'altro accade. Se ci sono altre cose che hanno bisogno di correre, ma non sono legati al fatto che l' SomeObjectistanza è stata immobile allocato, allora dovrebbero andare in un altro try - finallyblocco, probabilmente uno che avvolge quello che ho mostrato.

Richiedere che le variabili siano assegnate manualmente prima dell'uso non porta a problemi reali. Porta solo a problemi minori, ma il tuo codice sarà migliore per questo. Avrai variabili con ambito più limitato e try- finallyblocchi che non cercano di proteggere troppo.

Se le variabili locali avessero valori predefiniti, sonel primo esempio sarebbe stato null. Non avrebbe risolto davvero nulla. Invece di ottenere un errore in fase di compilazione nel finallyblocco, avresti un in NullPointerExceptionagguato lì che potrebbe nascondere qualsiasi altra eccezione potrebbe verificarsi nella sezione "Fai un po 'di lavoro qui" del codice. (Oppure le eccezioni nelle finallysezioni si concatenano automaticamente all'eccezione precedente? Non ricordo. Anche così, avresti un'eccezione in più rispetto a quella reale.)


2
perché non avere solo un if (so! = null) ... nel blocco finalmente?
izb

ciò causerà comunque l'avviso / errore del compilatore - non penso che il compilatore lo capisca se il controllo (ma lo sto solo facendo fuori memoria, non testato).
Chii

6
Metterei solo SomeObject so = null prima di provare e mettere il controllo null alla clausola finalmente. In questo modo non ci saranno avvisi del compilatore.
Juha Syrjälä

Perché complicare le cose? Scrivi il blocco di prova in questo modo e SAPERNE che la variabile ha un valore valido. Nessun controllo null richiesto.
Rob Kennedy,

1
Rob, il tuo esempio "new SomeObject ()" è semplice e non dovrebbero essere generate eccezioni lì, ma se la chiamata può generare eccezioni, allora sarebbe meglio che si verifichi all'interno del try-block in modo che possa essere gestita.
Sarel Botha

12

Inoltre, nell'esempio seguente, potrebbe essere stata lanciata un'eccezione all'interno della costruzione SomeObject, nel qual caso la variabile 'so' sarebbe nulla e la chiamata a CleanUp genererà una NullPointerException

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Quello che tendo a fare è questo:

SomeObject so = null;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  if (so != null) {
     so.CleanUp(); // safe
  }
}

12
Cosa preferiresti fare?
Electric Monk

2
Sì, brutto. Sì, è quello che faccio anche io.
SMBiggs

@ElectricMonk Quale forma ritieni sia migliore, quella che hai mostrato o quella mostrata qui nel metodo getContents (..): javapractices.com/topic/TopicAction.do?Id=126
Atom

11

Si noti che le variabili di istanza / membro finali non vengono inizializzate per impostazione predefinita. Perché quelli sono definitivi e non possono essere modificati successivamente nel programma. Questo è il motivo per cui Java non fornisce alcun valore predefinito per loro e forza il programmatore a inizializzarlo.

D'altra parte, le variabili membro non finali possono essere modificate in seguito. Quindi il compilatore non lascia che rimangano non inizializzati, appunto, perché possono essere modificati in seguito. Per quanto riguarda le variabili locali, l'ambito delle variabili locali è molto più ristretto. Il compilatore sa quando viene utilizzato. Quindi, forzare il programmatore a inizializzare la variabile ha senso.


9

La risposta effettiva alla tua domanda è perché le variabili del metodo vengono istanziate semplicemente aggiungendo un numero al puntatore dello stack. Azzerarli sarebbe un passo in più. Per le variabili di classe, vengono inserite nella memoria inizializzata sull'heap.

Perché non fare il passo in più? Fai un passo indietro - Nessuno ha detto che l '"avvertimento" in questo caso è una cosa molto buona.

Non dovresti mai inizializzare la tua variabile su zero o null al primo passaggio (quando la codifichi per la prima volta). Assegnalo al valore effettivo o non assegnarlo affatto perché se non lo fai, java può dirti quando hai davvero sbagliato. Prendi la risposta di Electric Monk come un ottimo esempio. Nel primo caso, è davvero incredibilmente utile che ti stia dicendo che se il try () fallisce perché il costruttore di SomeObject ha generato un'eccezione, allora ti ritroverai con un NPE nel finalmente. Se il costruttore non può generare un'eccezione, non dovrebbe essere nel tentativo.

Questo avviso è un fantastico controllore di programmatori cattivi multi-percorso che mi ha salvato dal fare cose stupide poiché controlla ogni percorso e si assicura che se hai usato la variabile in qualche percorso, dovresti inizializzarla in ogni percorso che conduce ad esso . Ora non inizializzo mai esplicitamente le variabili fino a quando non determino che è la cosa corretta da fare.

Inoltre, non è meglio dire esplicitamente "int size = 0" piuttosto che "int size" e fare in modo che il prossimo programmatore capisca che si intende che sia zero?

Il rovescio della medaglia non riesco a trovare un unico motivo valido per far sì che il compilatore inizializzi tutte le variabili non inizializzate su 0.


1
Sì, e ce ne sono altri in cui più o meno deve essere inizializzato su null a causa del modo in cui scorre il codice - non avrei dovuto dire "mai" aggiornato la risposta per riflettere questo.
Bill K

4

Penso che lo scopo principale fosse mantenere la somiglianza con C / C ++. Tuttavia, il compilatore rileva e avverte dell'utilizzo di variabili non inizializzate che ridurranno il problema al minimo. Dal punto di vista delle prestazioni, è un po 'più veloce consentire di dichiarare variabili non inizializzate poiché il compilatore non dovrà scrivere un'istruzione di assegnazione, anche se si sovrascrive il valore della variabile nell'istruzione successiva.


1
Probabilmente, il compilatore potrebbe determinare se si assegna sempre alla variabile prima di fare qualsiasi cosa con essa e, in tal caso, sopprimere un'assegnazione automatica di valore predefinito. Se il compilatore non è in grado di determinare se un'assegnazione avviene prima dell'accesso, genererà l'assegnazione predefinita.
Greg Hewgill

2
Sì, ma si potrebbe obiettare che consente al programmatore di sapere se per errore ha lasciato la variabile non inizializzata.
Mehrdad Afshari

1
Il compilatore potrebbe farlo anche in entrambi i casi. :) Personalmente, preferirei che il compilatore consideri una variabile non inizializzata come un errore. Significa che potrei aver commesso un errore da qualche parte.
Greg Hewgill

Non sono un tipo Java, ma mi piace il modo in cui C # lo gestisce. La differenza è che in quel caso il compilatore ha dovuto emettere un avviso, che potrebbe farti ricevere un paio di centinaia di avvisi per il tuo programma corretto;)
Mehrdad Afshari

Avverte anche per le variabili membro?
Adeel Ansari

4

(Può sembrare strano pubblicare una nuova risposta così a lungo dopo la domanda, ma un duplicato uscito .)

Per me, il motivo si riduce a questo: lo scopo delle variabili locali è diverso dallo scopo delle variabili di istanza. Le variabili locali devono essere utilizzate come parte di un calcolo; le variabili di istanza sono lì per contenere lo stato. Se usi una variabile locale senza assegnarle un valore, è quasi certamente un errore logico.

Detto questo, potrei totalmente accettare di richiedere che le variabili di istanza siano sempre inizializzate esplicitamente; l'errore si verificherebbe su qualsiasi costruttore dove il risultato consente una variabile di istanza non inizializzata (ad esempio, non inizializzata alla dichiarazione e non nel costruttore). Ma non è questa la decisione Gosling, et. al., scattata nei primi anni '90, quindi eccoci qui. (E non sto dicendo che abbiano fatto la chiamata sbagliata.)

Tuttavia, non sono riuscito a superare le variabili locali predefinite. Sì, non dovremmo fare affidamento sui compilatori per ricontrollare la nostra logica, e uno no, ma è comunque utile quando il compilatore ne rileva uno. :-)


"Detto questo, potrei assolutamente accettare di richiedere che le variabili di istanza fossero sempre inizializzate esplicitamente ..." che, FWIW, è la direzione che hanno preso in TypeScript.
TJ Crowder

3

È più efficiente non inizializzare le variabili e, nel caso di variabili locali, è sicuro farlo, perché l'inizializzazione può essere monitorata dal compilatore.

Nei casi in cui hai bisogno di inizializzare una variabile puoi sempre farlo da solo, quindi non è un problema.


3

L'idea alla base delle variabili locali è che esistono solo all'interno dell'ambito limitato per il quale sono necessarie. In quanto tale, dovrebbero esserci poche ragioni per incertezza sul valore, o almeno, da dove proviene tale valore. Potrei immaginare molti errori derivanti dall'avere un valore predefinito per le variabili locali.

Ad esempio, si consideri il seguente codice semplice ... ( NB si supponga a scopo dimostrativo che alle variabili locali venga assegnato un valore predefinito, come specificato, se non esplicitamente inizializzato )

System.out.println("Enter grade");
int grade = new Scanner(System.in).nextInt(); //I won't bother with exception handling here, to cut down on lines.
char letterGrade; //let us assume the default value for a char is '\0'
if (grade >= 90)
    letterGrade = 'A';
else if (grade >= 80)
    letterGrade = 'B';
else if (grade >= 70)
    letterGrade = 'C';
else if (grade >= 60)
    letterGrade = 'D';
else
    letterGrade = 'F';
System.out.println("Your grade is " + letterGrade);

Quando tutto è stato detto e fatto, supponendo che il compilatore abbia assegnato un valore predefinito di '\ 0' a letterGrade , questo codice come scritto funzionerebbe correttamente. Tuttavia, cosa succede se dimentichiamo la dichiarazione else?

Un'esecuzione di prova del nostro codice potrebbe comportare quanto segue

Enter grade
43
Your grade is

Questo risultato, sebbene prevedibile, non era sicuramente l'intento del programmatore. In effetti, probabilmente nella stragrande maggioranza dei casi (o almeno in un numero significativo), il valore predefinito non sarebbe il valore desiderato , quindi nella stragrande maggioranza dei casi il valore predefinito risulterebbe in errore. Ha più senso forzare il programmatore ad assegnare un valore iniziale a una variabile locale prima di usarla, poiché il problema di debug causato dalla dimenticanza di = 1in for(int i = 1; i < 10; i++)supera di gran lunga la comodità di non dover includere = 0in for(int i; i < 10; i++).

È vero che i blocchi try-catch-latest potrebbero diventare un po 'confusi (ma in realtà non è un catch-22 come sembra suggerire la citazione), quando ad esempio un oggetto lancia un'eccezione controllata nel suo costruttore, ma per uno motivo o un altro, qualcosa deve essere fatto a questo oggetto alla fine del blocco alla fine. Un perfetto esempio di questo è quando si tratta di risorse, che devono essere chiuse.

Un modo per gestire questo problema in passato potrebbe essere così ...

Scanner s = null; //declared and initialized to null outside the block. This gives us the needed scope, and an initial value.
try {
    s = new Scanner(new FileInputStream(new File("filename.txt")));
    int someInt = s.nextInt();
} catch (InputMismatchException e) {
    System.out.println("Some error message");
} catch (IOException e) {
    System.out.println("different error message"); 
} finally {
    if (s != null) //in case exception during initialization prevents assignment of new non-null value to s.
        s.close();
}

Tuttavia, a partire da Java 7, questo blocco finalmente non è più necessario utilizzando try-with-resources, in questo modo.

try (Scanner s = new Scanner(new FileInputStream(new File("filename.txt")))) {
...
...
} catch(IOException e) {
    System.out.println("different error message");
}

Detto questo, (come suggerisce il nome) funziona solo con le risorse.

E mentre il primo esempio è un po 'schifoso, questo forse parla più del modo in cui try-catch-infine o queste classi sono implementate che non delle variabili locali e di come sono implementate.

È vero che i campi vengono inizializzati su un valore predefinito, ma questo è leggermente diverso. Quando dici, ad esempio, int[] arr = new int[10];non appena hai inizializzato questo array, l'oggetto esiste in memoria in una data posizione. Supponiamo per un momento che non ci siano valori predefiniti, ma invece il valore iniziale è qualsiasi serie di 1 e 0 si trovi in ​​quella posizione di memoria al momento. Ciò potrebbe portare a comportamenti non deterministici in un certo numero di casi.

Supponiamo di avere ...

int[] arr = new int[10];
if(arr[0] == 0)
    System.out.println("Same.");
else
    System.out.println("Not same.");

Sarebbe perfettamente possibile che Same.potrebbe essere visualizzato in una corsa e Not same.potrebbe essere visualizzato in un'altra. Il problema potrebbe diventare ancora più grave una volta che inizi a parlare di variabili di riferimento.

String[] s = new String[5];

Secondo la definizione, ogni elemento di s dovrebbe puntare a una stringa (o è nullo). Tuttavia, se il valore iniziale è una qualsiasi serie di 0 e 1 che si verifica in questa posizione di memoria, non solo non c'è garanzia che otterrai gli stessi risultati ogni volta, ma non c'è neppure alcuna garanzia che l'oggetto s [0] punti a (supponendo che punti a qualcosa di significativo) anche è una stringa (forse è un coniglio,: p )! Questa mancanza di attenzione per il tipo andrebbe contro praticamente tutto ciò che rende Java Java. Quindi, mentre avere valori di default per le variabili locali potrebbe essere visto come facoltativo nel migliore dei casi, avere valori di default per le variabili di istanza è più vicino a una necessità .


1

se non sbaglio, un altro motivo potrebbe essere

Fornire il valore predefinito delle variabili membro fa parte del caricamento della classe

Il caricamento della classe è una cosa in fase di esecuzione in java significa che quando crei un oggetto la classe viene caricata con il caricamento della classe, solo le variabili membro vengono inizializzate con il valore predefinito JVM non impiega tempo per dare il valore predefinito alle tue variabili locali perché alcuni metodi non lo saranno mai chiamato perché la chiamata al metodo può essere condizionale, quindi perché prendere tempo per dare loro il valore predefinito e ridurre le prestazioni se questi valori predefiniti non verranno mai utilizzati.


0

Eclipse ti dà anche avvisi di variabili non inizializzate, quindi diventa comunque abbastanza ovvio. Personalmente penso che sia una buona cosa che questo sia il comportamento predefinito, altrimenti la tua applicazione potrebbe utilizzare valori imprevisti, e invece del compilatore che genera un errore non farà nulla (ma forse darà un avvertimento) e poi ti graffierai la tua testa sul perché certe cose non si comportano come dovrebbero.


0

Le variabili locali sono archiviate in uno stack, ma le variabili di istanza sono archiviate nell'heap, quindi ci sono alcune possibilità che venga letto un valore precedente nello stack invece di un valore predefinito come accade nell'heap. Per questo motivo jvm non consente di utilizzare una variabile locale senza inizializzarla.


2
Completamente sbagliato ... tutte le non primitive Java sono memorizzate nell'heap indipendentemente da quando e come vengono costruite
gshauger

Prima di Java 7, le variabili di istanza vengono memorizzate nell'heap e le variabili locali si trovano nello stack. Tuttavia, qualsiasi oggetto a cui fa riferimento una variabile locale verrà trovato nell'heap. A partire da Java 7, il "compilatore Java Hotspot Server" potrebbe eseguire "analisi di fuga" e decidere di allocare alcuni oggetti sullo stack invece che sull'heap.
mamills

0

La variabile di istanza avrà valori predefiniti ma le variabili locali non potrebbero avere valori predefiniti. Poiché le variabili locali sono fondamentalmente nei metodi / comportamento, il suo scopo principale è eseguire alcune operazioni o calcoli. Pertanto, non è una buona idea impostare valori predefiniti per le variabili locali. In caso contrario, è molto difficile e richiede tempo verificare i motivi delle risposte inaspettate.


-1

La risposta è che le variabili di istanza possono essere inizializzate nel costruttore della classe o in qualsiasi metodo di classe, ma in caso di variabili locali, una volta definito ciò che nel metodo rimane per sempre nella classe.


-2

Potrei pensare di seguire 2 motivi

  1. Come dice la maggior parte delle risposte ponendo il vincolo di inizializzare la variabile locale, si garantisce che alla variabile locale venga assegnato un valore come vuole il programmatore e si assicura che i risultati attesi vengano calcolati.
  2. Le variabili di istanza possono essere nascoste dichiarando variabili locali (stesso nome): per garantire il comportamento previsto, alle variabili locali viene forzato l'inizializzazione di un valore. (Lo eviterei totalmente però)

I campi non possono essere sovrascritti. Al massimo possono essere nascosti e non vedo come l'occultamento potrebbe interferire con un controllo per l'inizializzazione?
Meriton

Nascosto a destra Se si decide di creare una variabile locale con lo stesso nome dell'istanza, a causa di questo vincolo, la variabile locale verrà inizializzata con un valore pupo selezionato (diverso dal valore della variabile di istanza)
Mitra
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.