Perché questo codice Java viene compilato?


96

Nell'ambito del metodo o della classe, la riga seguente viene compilata (con avviso):

int x = x = 1;

Nell'ambito della classe, in cui le variabili ottengono i valori predefiniti , quanto segue restituisce l'errore di "riferimento non definito":

int x = x + 1;

Non è il primo che x = x = 1dovrebbe finire con lo stesso errore di "riferimento non definito"? O forse la seconda riga int x = x + 1dovrebbe essere compilata? O c'è qualcosa che mi manca?


1
Se aggiungi la parola chiave staticnella variabile dell'ambito della classe, come in static int x = x + 1;, otterrai lo stesso errore? Perché in C # fa la differenza se è statico o non statico.
Jeppe Stig Nielsen

static int x = x + 1fallisce in Java.
Marcin

1
in c # entrambi int a = this.a + 1;e int b = 1; int a = b + 1;nell'ambito della classe (entrambi sono ok in Java) falliscono, probabilmente a causa di §17.4.5.2 - "Un inizializzatore di variabile per un campo istanza non può fare riferimento all'istanza che viene creata." Non so se è esplicitamente consentito da qualche parte, ma statico non ha tale restrizione. In Java le regole sono diverse e static int x = x + 1non riesce per la stessa ragione che int x = x + 1fa
MSAM

Quella risposta con un bytecode cancella ogni dubbio.
rgripper

Risposte:


101

tl; dr

Per i campi , int b = b + 1è illegale perché bè un riferimento in avanti illegale a b. Puoi effettivamente risolvere questo problema scrivendo int b = this.b + 1, che viene compilato senza lamentele.

Per le variabili locali , int d = d + 1è illegale perché dnon viene inizializzato prima dell'uso. Questo non è il caso dei campi, che sono sempre inizializzati per impostazione predefinita.

Puoi vedere la differenza tentando di compilare

int x = (x = 1) + x;

come dichiarazione di campo e come dichiarazione di variabile locale. Il primo fallirà, ma il secondo avrà successo, a causa della differenza di semantica.

introduzione

Prima di tutto, le regole per gli inizializzatori di campo e di variabili locali sono molto diverse. Quindi questa risposta affronterà le regole in due parti.

Useremo questo programma di test in tutto:

public class test {
    int a = a = 1;
    int b = b + 1;
    public static void Main(String[] args) {
        int c = c = 1;
        int d = d + 1;
    }
}

La dichiarazione di bnon è valida e ha esito negativo con un illegal forward referenceerrore.
La dichiarazione di dnon è valida e ha esito negativo con un variable d might not have been initializederrore.

Il fatto che questi errori siano diversi dovrebbe suggerire che anche i motivi degli errori sono diversi.

Campi

Gli inizializzatori di campo in Java sono regolati da JLS §8.3.2 , Inizializzazione dei campi.

L' ambito di un campo è definito in JLS §6.3 , Scopo di una dichiarazione.

Le regole rilevanti sono:

  • L'ambito di una dichiarazione di un membro mdichiarato o ereditato da un tipo di classe C (§8.1.6) è l'intero corpo di C, comprese le dichiarazioni di tipo annidate.
  • Le espressioni di inizializzazione per le variabili di istanza possono utilizzare il semplice nome di qualsiasi variabile statica dichiarata o ereditata dalla classe, anche una la cui dichiarazione si verifica successivamente in modo testuale.
  • L'uso di variabili di istanza le cui dichiarazioni appaiono in modo testuale dopo l'uso è talvolta limitato, anche se queste variabili di istanza rientrano nell'ambito. Vedere §8.3.2.3 per le regole precise che governano il riferimento diretto alle variabili di istanza.

§8.3.2.3 dice:

La dichiarazione di un membro deve apparire testualmente prima di essere utilizzata solo se il membro è un campo di istanza (rispettivamente statico) di una classe o di un'interfaccia C e tutte le seguenti condizioni sono valide:

  • L'utilizzo si verifica in un inizializzatore di variabile di istanza (rispettivamente statica) di C o in un inizializzatore di istanza (rispettivamente statica) di C.
  • L'utilizzo non si trova sul lato sinistro di un compito.
  • L'utilizzo avviene tramite un semplice nome.
  • C è la classe o l'interfaccia più interna che racchiude l'utilizzo.

Puoi effettivamente fare riferimento ai campi prima che siano stati dichiarati, tranne in alcuni casi. Queste restrizioni hanno lo scopo di impedire codice simile

int j = i;
int i = j;

dalla compilazione. La specifica Java dice che "le limitazioni di cui sopra sono progettate per catturare, in fase di compilazione, inizializzazioni circolari o altrimenti non corrette".

A cosa si riducono effettivamente queste regole?

In breve, le regole in sostanza dicono che devi dichiarare un campo prima di un riferimento a quel campo se (a) il riferimento è in un inizializzatore, (b) il riferimento non viene assegnato, (c) il riferimento è un nome semplice (nessun qualificatore come this.) e (d) non vi si accede dall'interno di una classe interna. Quindi, un riferimento in avanti che soddisfa tutte e quattro le condizioni è illegale, ma un riferimento in avanti che non riesce in almeno una condizione è OK.

int a = a = 1;compila perché viola (b): il riferimento a è stato assegnato, quindi è legale fare riferimento aprima della adichiarazione completa di.

int b = this.b + 1compila anche perché viola (c): il riferimento this.bnon è un nome semplice (è qualificato con this.). Questo strano costrutto è ancora perfettamente ben definito, perché this.bha valore zero.

Quindi, fondamentalmente, le restrizioni sui riferimenti ai campi all'interno degli inizializzatori impediscono int a = a + 1di essere compilati correttamente.

Si noti che la dichiarazione del campo nonint b = (b = 1) + b verrà compilata, perché il finale è ancora un riferimento in avanti illegale.b

Variabili locali

Le dichiarazioni di variabili locali sono regolate da JLS §14.4 , Dichiarazioni di dichiarazione di variabili locali.

L' ambito di una variabile locale è definito in JLS §6.3 , Scopo di una dichiarazione:

  • Lo scopo 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 eventuali ulteriori dichiaratori a destra nell'istruzione di dichiarazione della variabile locale.

Notare che gli inizializzatori rientrano nell'ambito della variabile dichiarata. Allora perché non int d = d + 1;compila?

Il motivo è dovuto alla regola di Java sull'assegnazione definitiva ( JLS §16 ). L'assegnazione definita fondamentalmente dice che ogni accesso a una variabile locale deve avere un precedente assegnamento a quella variabile, e il compilatore Java controlla loop e rami per garantire che l'assegnazione avvenga sempre prima di qualsiasi utilizzo (questo è il motivo per cui l'assegnazione definita ha un'intera sezione di specifica dedicata ad esso). La regola di base è:

  • Ad ogni accesso di una variabile locale o di un campo finale vuoto x, xdeve essere assegnato definitivamente prima dell'accesso, altrimenti si verifica un errore in fase di compilazione.

In int d = d + 1;, l'accesso a dviene risolto alla variabile locale fine, ma poiché dnon è stato assegnato prima dell'accesso d, il compilatore emette un errore. In int c = c = 1, c = 1accade prima, che assegna ce quindi cviene inizializzato sul risultato di quell'assegnazione (che è 1).

Si noti che a causa di regole di assegnazione definite, la dichiarazione della variabile locale int d = (d = 1) + d; verrà compilata correttamente (a differenza della dichiarazione del campo int b = (b = 1) + b), perché dviene assegnata definitivamente quando dviene raggiunta la finale .


+1 per i riferimenti, tuttavia penso che tu abbia sbagliato questa dicitura: "int a = a = 1; compila perché viola (b)", se violasse uno qualsiasi dei 4 requisiti non si compilerebbe. Tuttavia non è così dal momento che IS sul lato sinistro di una cessione (doppio negativo in termini di JLS non aiuta molto qui). In int b = b + 1b è a destra (non a sinistra) dell'assegnazione quindi violerebbe questo ...
msam

... Quello di cui non sono troppo sicuro è il seguente: queste 4 condizioni devono essere soddisfatte se la dichiarazione non compare testualmente prima dell'incarico, in questo caso penso che la dichiarazione appaia "testualmente" prima dell'incarico int x = x = 1, in cui caso niente di tutto questo si applicherebbe.
msam

@msam: è un po 'confuso, ma fondamentalmente devi violare una delle quattro condizioni per fare un riferimento in avanti. Se il tuo riferimento in avanti soddisfa tutte e quattro le condizioni, è illegale.
nneonneo

@msam: Inoltre, la dichiarazione completa diventa effettiva solo dopo l'inizializzatore.
nneonneo

@mrfishie: grande risposta, ma c'è una sorprendente quantità di profondità nelle specifiche Java. La domanda non è così semplice come sembra in superficie. (Una volta ho scritto un sottoinsieme di compilatore Java, quindi ho familiarità con molti dei dettagli di JLS).
nneonneo

86
int x = x = 1;

è equivalente a

int x = 1;
x = x; //warning here

nel frattempo

int x = x + 1; 

per prima cosa dobbiamo calcolare x+1ma il valore di x non è noto, quindi ottieni un errore (il compilatore sa che il valore di x non è noto)


4
Questo, oltre al suggerimento sull'associatività a destra di OpenSauce, l'ho trovato molto utile.
TobiMcNamobi

1
Pensavo che il valore di ritorno di un'assegnazione fosse il valore assegnato, non il valore della variabile.
zzzzBov

2
@zzzzBov è corretto. int x = x = 1;è equivalente a int x = (x = 1), non x = 1; x = x; . Non dovresti ricevere un avviso del compilatore per eseguire questa operazione.
nneonneo

int x = x = 1;s equivalente a int a x = (x = 1)causa dell'associatività a destra =dell'operatore
Grijesh Chauhan

1
@nneonneo ed int x = (x = 1)è equivalente a int x; x = 1; x = x;(dichiarazione di variabile, valutazione dell'inizializzatore di campo, assegnazione di variabile al risultato di detta valutazione), da qui l'avviso
msam

41

È più o meno equivalente a:

int x;
x = 1;
x = 1;

In primo luogo, int <var> = <expression>;è sempre equivalente a

int <var>;
<var> = <expression>;

In questo caso, la tua espressione è x = 1, che è anche un'affermazione. x = 1è un'istruzione valida, poiché la var xè già stata dichiarata. È anche un'espressione con il valore 1, che viene quindi assegnato di xnuovo.


Ok, ma se è andata come dici tu, perché nell'ambito della classe la seconda affermazione dà un errore? Voglio dire che ottieni il 0valore predefinito per int, quindi mi aspetto che il risultato sia 1, non il undefined reference.
Marcin

Dai un'occhiata alla risposta @izogfif. Sembra funzionare, perché il compilatore C ++ assegna valori predefiniti alle variabili. Allo stesso modo in cui Java fa per le variabili a livello di classe.
Marcin

@ Marcin: in Java, gli int non sono inizializzati a 0 quando sono variabili locali. Vengono inizializzati su 0 solo se sono variabili membro. Quindi nella seconda riga x + 1non ha un valore definito, perché xnon è inizializzato.
OpenSauce

1
@OpenSauce But x è definita come variabile membro ("nell'ambito della classe").
Jacob Raihle

@JacobRaihle: Ah ok, non ho notato quella parte. Non sono sicuro che il bytecode per inizializzare una var a 0 verrà generato dal compilatore se vede che c'è un'istruzione di inizializzazione esplicita. C'è un articolo qui che entra in alcuni dettagli sull'inizializzazione
OpenSauce

12

In java o in qualsiasi linguaggio moderno, l'assegnazione viene da destra.

Supponiamo di avere due variabili x e y,

int z = x = y = 5;

Questa affermazione è valida ed è così che il compilatore le divide.

y = 5;
x = y;
z = x; // which will be 5

Ma nel tuo caso

int x = x + 1;

Il compilatore ha fatto un'eccezione perché si divide in questo modo.

x = 1; // oops, it isn't declared because assignment comes from the right.

l'avviso è su x = x non x = 1
Asim Ghaffar

8

int x = x = 1; non è uguale a:

int x;
x = 1;
x = x;

javap ci aiuta di nuovo, queste sono istruzioni JVM generate per questo codice:

0: iconst_1    //load constant to stack
1: dup         //duplicate it
2: istore_1    //set x to constant
3: istore_1    //set x to constant

più come:

int x = 1;
x = 1;

Non c'è motivo per lanciare un errore di riferimento non definito. Ora c'è l'utilizzo della variabile prima della sua inizializzazione, quindi questo codice è pienamente conforme alle specifiche. In effetti non c'è alcun utilizzo di variabili , solo assegnazioni. E il compilatore JIT andrà ancora oltre, eliminerà tali costruzioni. Onestamente, non capisco come questo codice sia connesso alle specifiche di JLS di inizializzazione e utilizzo delle variabili. Nessun utilizzo nessun problema. ;)

Correggi se sbaglio. Non riesco a capire perché altre risposte, che fanno riferimento a molti paragrafi JLS, raccolgono così tanti vantaggi. Questi paragrafi non hanno nulla in comune con questo caso. Solo due incarichi in serie e non di più.

Se scriviamo:

int b, c, d, e, f;
int a = b = c = d = e = f = 5;

è uguale a:

f = 5
e = 5
d = 5
c = 5
b = 5
a = 5

La maggior parte delle espressioni a destra viene assegnata alle variabili una per una, senza alcuna ricorsione. Possiamo confondere le variabili come preferiamo:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;

7

In int x = x + 1;aggiungi 1 ax, quindi qual è il valore di x, non è ancora stato creato.

Ma int x=x=1;verrà compilato senza errori perché si assegna 1 a x.


5

La tua prima parte di codice contiene una seconda =invece di un plus. Questo verrà compilato ovunque mentre la seconda parte di codice non verrà compilata in nessuna delle due posizioni.


5

Nella seconda parte di codice, x viene utilizzata prima della sua dichiarazione, mentre nella prima parte di codice viene assegnata solo due volte, il che non ha senso ma è valido.


5

Analizziamolo passo dopo passo, giusta associativa

int x = x = 1

x = 1, assegna 1 a una variabile x

int x = x, assegna ciò che x è a se stesso, come int. Poiché x era stato precedentemente assegnato come 1, mantiene 1, anche se in modo ridondante.

Si compila bene.

int x = x + 1

x + 1, aggiungine uno a una variabile x. Tuttavia, x non essendo definito, causerà un errore di compilazione.

int x = x + 1, quindi questa riga compila gli errori poiché la parte destra dell'uguale non compilerà aggiungendone uno a una variabile non assegnata


No, è associativo a destra quando ci sono due =operatori, quindi è uguale a int x = (x = 1);.
Jeppe Stig Nielsen

Ah, i miei ordini fuori. Mi dispiace per quello. Avrebbe dovuto farli al contrario. L'ho cambiato adesso.
steventnorris

3

Il secondo int x=x=1è compilare perché stai assegnando il valore alla x ma in altri casi int x=x+1qui la variabile x non è inizializzata, Ricorda in java le variabili locali non sono inizializzate al valore predefinito. Nota Se è ( int x=x+1) anche nell'ambito della classe, restituirà anche un errore di compilazione poiché la variabile non viene creata.


2
int x = x + 1;

si compila correttamente in Visual Studio 2008 con avviso

warning C4700: uninitialized local variable 'x' used`

2
Interessante. È C / C ++?
Marcin

@ Marcin: sì, è C ++. @msam: scusa, penso di aver visto tag cinvece di javama a quanto pare era l'altra domanda.
izogfif

Si compila perché in C ++, i compilatori assegnano valori predefiniti per i tipi primitivi. Usa bool y;e y==truerestituirà false.
Sri Harsha Chilakapati

@SriHarshaChilakapati, è una specie di standard nel compilatore C ++? Perché quando compilo void main() { int x = x + 1; printf("%d ", x); }in Visual Studio 2008, in Debug ottengo l'eccezione Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.e in Release ottengo il numero 1896199921stampato nella console.
izogfif

1
@SriHarshaChilakapati Parlando di altri linguaggi: In C #, per un staticcampo (variabile statica a livello di classe), si applicano le stesse regole. Ad esempio, un campo dichiarato come viene public static int x = x + 1;compilato senza preavviso in Visual C #. Forse lo stesso in Java?
Jeppe Stig Nielsen

2

x non è inizializzato in x = x + 1;.

Il linguaggio di programmazione Java è tipizzato staticamente, il che significa che tutte le variabili devono essere dichiarate prima di poter essere utilizzate.

Vedi tipi di dati primitivi


3
La necessità di inizializzare le variabili prima di utilizzare i loro valori non ha nulla a che fare con la tipizzazione statica. Digitato staticamente: è necessario dichiarare di che tipo è una variabile. Inizializza prima dell'uso: deve avere un valore dimostrabile prima di poterlo utilizzare.
Jon Bright

@ JonBright: Anche la necessità di dichiarare tipi di variabili non ha nulla a che fare con la tipizzazione statica. Ad esempio, esistono linguaggi tipizzati staticamente con inferenza del tipo.
Hammar

@hammar, per come la vedo io, puoi argomentare in due modi: con l'inferenza del tipo, dichiari implicitamente il tipo della variabile in un modo che il sistema può dedurre. Oppure, l'inferenza del tipo è un terzo modo, in cui le variabili non sono tipizzate dinamicamente in fase di esecuzione, ma sono a livello di origine, a seconda del loro utilizzo e delle inferenze così fatte. In ogni caso, l'affermazione rimane vera. Ma hai ragione, non stavo pensando ad altri tipi di sistemi.
Jon Bright

2

La riga di codice non viene compilata con un avviso a causa del funzionamento effettivo del codice. Quando esegui il codice int x = x = 1, Java crea prima la variabile x, come definito. Quindi esegue il codice di assegnazione ( x = 1). Poiché xè già definito, il sistema non ha errori impostati xsu 1. Ciò restituisce il valore 1, perché quello è ora il valore di x. Pertanto, xora è finalmente impostato come 1.
Java fondamentalmente esegue il codice come se fosse questo:

int x;
x = (x = 1); // (x = 1) returns 1 so there is no error

Tuttavia, nella seconda parte di codice, int x = x + 1l' + 1istruzione richiede xdi essere definita, cosa che a quel punto non lo è. Poiché le istruzioni di assegnazione indicano sempre che il codice a destra di =viene eseguito per primo, il codice fallirà perché xnon è definito. Java eseguirà il codice in questo modo:

int x;
x = x + 1; // this line causes the error because `x` is undefined

-1

Complier ha letto le dichiarazioni da destra a sinistra e abbiamo progettato di fare il contrario. Ecco perché all'inizio è stato infastidito. Rendi questo l'abitudine di leggere le dichiarazioni (codice) da destra a sinistra non avrai questo problema.

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.