¿Por qué se compila este código Java?

96

En el ámbito del método o de la clase, la línea siguiente se compila (con advertencia):

int x = x = 1;

En el ámbito de la clase, donde las variables obtienen sus valores predeterminados , lo siguiente da un error de 'referencia indefinida':

int x = x + 1;

¿No es el primero que x = x = 1debería terminar con el mismo error de 'referencia indefinida'? ¿O tal vez int x = x + 1debería compilarse la segunda línea ? ¿O hay algo que me estoy perdiendo?

Marcin
fuente
1
Si agrega la palabra clave staticen la variable de alcance de clase, como en static int x = x + 1;, ¿obtendrá el mismo error? Porque en C # marca la diferencia si es estático o no estático.
Jeppe Stig Nielsen
static int x = x + 1falla en Java.
Marcin
1
en c # tanto int a = this.a + 1;y int b = 1; int a = b + 1;en ámbito de clase (ambos de los cuales están bien en Java) fallar, probablemente debido a §17.4.5.2 - "Una variable de inicialización para un campo de instancia no se puede hacer referencia a la instancia que se está creando." No sé si está explícitamente permitido en algún lugar, pero la estática no tiene tal restricción. En Java, las reglas son diferentes y static int x = x + 1fallan por la misma razón que lo int x = x + 1hace
msam
Esa respuesta con un bytecode despeja cualquier duda.
rgripper

Respuestas:

101

tl; dr

Para los campos , int b = b + 1es ilegal porque bes una referencia hacia adelante ilegal a b. De hecho, puede solucionar esto escribiendo int b = this.b + 1, que se compila sin quejas.

Para las variables locales , int d = d + 1es ilegal porque dno se inicializa antes de su uso. Este no es el caso de los campos, que siempre se inicializan por defecto.

Puede ver la diferencia al intentar compilar

int x = (x = 1) + x;

como una declaración de campo y como una declaración de variable local. El primero fallará, pero el segundo tendrá éxito debido a la diferencia de semántica.

Introducción

En primer lugar, las reglas para los inicializadores de variables locales y de campo son muy diferentes. Entonces, esta respuesta abordará las reglas en dos partes.

Usaremos este programa de prueba en todo momento:

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 declaración de bno es válida y falla con un illegal forward referenceerror.
La declaración de dno es válida y falla con un variable d might not have been initializederror.

El hecho de que estos errores sean diferentes debería indicar que las razones de los errores también son diferentes.

Campos

Los inicializadores de campo en Java se rigen por JLS §8.3.2 , Inicialización de campos.

El alcance de un campo se define en JLS §6.3 , Alcance de una declaración.

Las reglas relevantes son:

  • El alcance de una declaración de un miembro mdeclarado o heredado por un tipo de clase C (§8.1.6) es el cuerpo completo de C, incluidas las declaraciones de tipo anidado.
  • Las expresiones de inicialización, por ejemplo, las variables, pueden usar el nombre simple de cualquier variable estática declarada o heredada por la clase, incluso una cuya declaración se produzca textualmente más tarde.
  • El uso de variables de instancia cuyas declaraciones aparecen textualmente después del uso a veces está restringido, aunque estas variables de instancia están dentro del alcance. Consulte §8.3.2.3 para conocer las reglas precisas que rigen la referencia directa a las variables de instancia.

§8.3.2.3 dice:

La declaración de un miembro debe aparecer textualmente antes de que se use solo si el miembro es un campo de instancia (respectivamente estático) de una clase o interfaz C y se cumplen todas las siguientes condiciones:

  • El uso ocurre en un inicializador de variable de instancia (respectivamente estático) de C o en un inicializador de instancia (respectivamente estático) de C.
  • El uso no está en el lado izquierdo de una tarea.
  • El uso es a través de un nombre simple.
  • C es la clase o interfaz más interna que encierra el uso.

De hecho, puede hacer referencia a los campos antes de que se hayan declarado, excepto en ciertos casos. Estas restricciones están destinadas a evitar códigos como

int j = i;
int i = j;

de la compilación. La especificación de Java dice que "las restricciones anteriores están diseñadas para detectar, en tiempo de compilación, inicializaciones circulares o con formato incorrecto".

¿A qué se reducen realmente estas reglas?

En resumen, las reglas básicamente dicen que debe declarar un campo antes de una referencia a ese campo si (a) la referencia está en un inicializador, (b) la referencia no se está asignando, (c) la referencia es un nombre simple (sin calificadores como this.) y (d) no se está accediendo desde dentro de una clase interna. Por tanto, una referencia directa que satisfaga las cuatro condiciones es ilegal, pero una referencia directa que falla en al menos una condición está bien.

int a = a = 1;compila porque viola (b): la referencia a la que a se está asignando, por lo que es legal hacer referencia aantes de ala declaración completa de '.

int b = this.b + 1también compila porque viola (c): la referencia this.bno es un nombre simple (está calificado con this.). Esta extraña construcción todavía está perfectamente bien definida, porque this.btiene el valor cero.

Entonces, básicamente, las restricciones en las referencias de campo dentro de los inicializadores impiden que int a = a + 1se compile correctamente.

Observe que la declaración de campo int b = (b = 1) + bse fallan para compilar, porque la final bes todavía una referencia ilegal hacia adelante.

Variables locales

Las declaraciones de variables locales se rigen por JLS §14.4 , Declaraciones de declaración de variables locales.

El alcance de una variable local se define en JLS §6.3 , Alcance de una declaración:

  • El alcance de una declaración de variable local en un bloque (§14.4) es el resto del bloque en el que aparece la declaración, comenzando con su propio inicializador e incluyendo cualquier otro declarador a la derecha en la declaración de declaración de variable local.

Tenga en cuenta que los inicializadores están dentro del alcance de la variable que se declara. Entonces, ¿por qué no se int d = d + 1;compila?

La razón se debe a la regla de Java sobre la asignación definitiva ( JLS §16 ). La asignación definida básicamente dice que cada acceso a una variable local debe tener una asignación anterior a esa variable, y el compilador de Java verifica los bucles y las ramas para garantizar que la asignación siempre ocurra antes de cualquier uso (esta es la razón por la que la asignación definitiva tiene una sección de especificación completa dedicada lo). La regla básica es:

  • Para cada acceso de una variable local o campo final en blanco x, xdebe asignarse definitivamente antes del acceso, o se producirá un error en tiempo de compilación.

En int d = d + 1;, el acceso a dse resuelve bien a la variable local, pero como dno se ha asignado antes dse accede, el compilador emite un error. En int c = c = 1, c = 1ocurre primero, que asigna c, y luego cse inicializa con el resultado de esa asignación (que es 1).

Tenga en cuenta que debido a las reglas de asignación definidas, la declaración de la variable local int d = (d = 1) + d; se compilará correctamente (a diferencia de la declaración de campo int b = (b = 1) + b), porque ddefinitivamente se asigna cuando dse alcanza la final .

neonneo
fuente
+1 para las referencias, sin embargo, creo que se equivocó en esta redacción: "int a = a = 1; compila porque viola (b)", si viola cualquiera de los 4 requisitos, no compilaría. Sin embargo, no es así, ya que ESTÁ en el lado izquierdo de una tarea (el doble negativo en la redacción de JLS no ayuda mucho aquí). En int b = b + 1b está a la derecha (no a la izquierda) de la asignación, por lo que violaría esto ...
msam
... De lo que no estoy muy seguro es de lo siguiente: esas 4 condiciones deben cumplirse si la declaración no aparece textualmente antes de la asignación, en este caso creo que la declaración aparece "textualmente" antes de la asignación int x = x = 1, en la que caso de que nada de esto se aplicaría.
msam
@msam: Es un poco confuso, pero básicamente tienes que violar una de las cuatro condiciones para poder hacer una referencia directa. Si su referencia directa cumple las cuatro condiciones, es ilegal.
nneonneo
@msam: Además, la declaración completa solo entra en vigor después del inicializador.
nneonneo
@mrfishie: Gran respuesta, pero hay una sorprendente profundidad en la especificación de Java. La pregunta no es tan simple como parece en la superficie. (Escribí un compilador de subconjunto de Java una vez, por lo que estoy bastante familiarizado con muchos de los entresijos de JLS).
nneonneo
86
int x = x = 1;

es equivalente a

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

mientras en

int x = x + 1; 

primero tenemos que calcular, x+1pero el valor de x no se conoce, por lo que obtiene un error (el compilador sabe que el valor de x no se conoce)

msam
fuente
4
Esto más la sugerencia sobre la asociatividad correcta de OpenSauce que encontré muy útil.
TobiMcNamobi
1
Pensé que el valor de retorno de una asignación era el valor asignado, no el valor de la variable.
zzzzBov
2
@zzzzBov es correcto. int x = x = 1;es equivalente a int x = (x = 1), no x = 1; x = x; . No debería recibir una advertencia del compilador por hacer esto.
nneonneo
int x = x = 1;s equivalente a int x = (x = 1)debido a la asociatividad derecha del =operador
Grijesh Chauhan
1
@nneonneo y int x = (x = 1)es equivalente a int x; x = 1; x = x;(declaración de variable, evaluación del inicializador de campo, asignación de variable al resultado de dicha evaluación), de ahí la advertencia
msam
41

Es aproximadamente equivalente a:

int x;
x = 1;
x = 1;

En primer lugar, int <var> = <expression>;siempre equivale a

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

En este caso, su expresión es x = 1, que también es una declaración. x = 1es una declaración válida, ya que la var xya ha sido declarada. También es una expresión con el valor 1, que luego se asigna de xnuevo.

OpenSauce
fuente
Ok, pero si fue como dices, ¿por qué en el alcance de la clase la segunda declaración da un error? Quiero decir que obtienes el 0valor predeterminado para ints, por lo que esperaría que el resultado sea 1, no undefined reference.
Marcin
Eche un vistazo a la respuesta de @izogfif. Parece que funciona, porque el compilador de C ++ asigna valores predeterminados a las variables. De la misma manera que lo hace java para las variables de nivel de clase.
Marcin
@Marcin: en Java, los ints no se inicializan a 0 cuando son variables locales. Solo se inicializan a 0 si son variables miembro. Entonces, en su segunda línea, x + 1no tiene un valor definido, porque xno está inicializado.
OpenSauce
1
@OpenSauce Pero x se define como una variable miembro ("en el ámbito de la clase").
Jacob Raihle
@JacobRaihle: Ah, está bien, no vi esa parte. No estoy seguro de que el compilador generará el código de bytes para inicializar una var a 0 si ve que hay una instrucción de inicialización explícita. Hay un artículo aquí que entra en algunos detalles sobre la inicialización de clases y objetos, aunque no creo que aborde este problema exacto: javaworld.com/jw-11-2001/jw-1102-java101.html
OpenSauce
12

En Java o en cualquier lenguaje moderno, la asignación proviene de la derecha.

Suponga que si tiene dos variables xey,

int z = x = y = 5;

Esta declaración es válida y así es como el compilador los divide.

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

Pero en tu caso

int x = x + 1;

El compilador dio una excepción porque se divide así.

x = 1; // oops, it isn't declared because assignment comes from the right.
Sri Harsha Chilakapati
fuente
la advertencia está en x = x no x = 1
Asim Ghaffar
8

int x = x = 1; no es igual a:

int x;
x = 1;
x = x;

javap nos ayuda nuevamente, estas son instrucciones JVM generadas para este código:

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

más como:

int x = 1;
x = 1;

Aquí no hay razón para arrojar un error de referencia indefinido. Ahora se usa la variable antes de su inicialización, por lo que este código cumple completamente con la especificación. De hecho, no hay uso de variable en absoluto , solo asignaciones. Y el compilador JIT irá aún más lejos, eliminará tales construcciones. Honestamente, no entiendo cómo este código está conectado con la especificación de JLS de inicialización y uso de variables. Sin uso, sin problemas. ;)

Por favor corrija si me equivoco. No puedo entender por qué otras respuestas, que se refieren a muchos párrafos de JLS, recopilan tantas ventajas. Estos párrafos no tienen nada en común con este caso. Solo dos asignaciones en serie y nada más.

Si escribimos:

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

es igual a:

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

La mayoría de las expresiones a la derecha se asignan a las variables una por una, sin recursividad. Podemos alterar las variables de la forma que queramos:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
Mikhail
fuente
7

En int x = x + 1;agrega 1 a X, por lo que lo que es el valor de x, no es creado todavía.

Pero en int x=x=1;se compilará sin error porque asigna 1 a x.

Alya'a Gamal
fuente
5

Su primer fragmento de código contiene un segundo en =lugar de un más. Esto se compilará en cualquier lugar, mientras que la segunda parte del código no se compilará en ningún lugar.

Joe Elleson
fuente
5

En la segunda parte del código, x se usa antes de su declaración, mientras que en la primera parte del código solo se asigna dos veces, lo que no tiene sentido pero es válido.

WilQu
fuente
5

Vamos a desglosarlo paso a paso, asociativo a la derecha

int x = x = 1

x = 1, asignar 1 a una variable x

int x = x, asigne lo que x es a sí mismo, como un int. Dado que x se asignó previamente como 1, conserva 1, aunque de forma redundante.

Eso se compila bien.

int x = x + 1

x + 1, agregue uno a una variable x. Sin embargo, si x no está definido, se producirá un error de compilación.

int x = x + 1, por lo tanto, esta línea compila errores ya que la parte derecha de los iguales no compilará agregando uno a una variable no asignada

Steventnorris
fuente
No, es asociativo a la derecha cuando hay dos =operadores, por lo que es lo mismo que int x = (x = 1);.
Jeppe Stig Nielsen
Ah, mis órdenes. Lo siento por eso. Debería haberlos hecho al revés. Lo he cambiado ahora.
steventnorris
3

El segundo int x=x=1es compilar porque está asignando el valor a la x, pero en otro caso int x=x+1aquí la variable x no está inicializada, recuerde que en Java la variable local no está inicializada al valor predeterminado. Nota Si está ( int x=x+1) en el alcance de la clase también, también dará un error de compilación ya que la variable no se crea.

Krushna
fuente
2
int x = x + 1;

se compila correctamente en Visual Studio 2008 con advertencia

warning C4700: uninitialized local variable 'x' used`
izogfif
fuente
2
Interesante. ¿Es C / C ++?
Marcin
@Marcin: sí, es C ++. @msam: lo siento, creo que vi la etiqueta en clugar de, javapero aparentemente fue la otra pregunta.
izogfif
Compila porque en C ++, los compiladores asignan valores predeterminados para tipos primitivos. Use bool y;y y==truedevolverá falso.
Sri Harsha Chilakapati
@SriHarshaChilakapati, ¿es algún tipo de estándar en el compilador C ++? Porque cuando compilo void main() { int x = x + 1; printf("%d ", x); }en Visual Studio 2008, en Debug obtengo la excepción Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.y en Release obtengo el número 1896199921impreso en la consola.
izogfif
1
@SriHarshaChilakapati Hablando de otros lenguajes: En C #, para un staticcampo (variable estática de nivel de clase), se aplican las mismas reglas. Por ejemplo, un campo declarado como public static int x = x + 1;compila sin advertencia en Visual C #. ¿Posiblemente lo mismo en Java?
Jeppe Stig Nielsen
2

x no se inicializa en x = x + 1;.

El lenguaje de programación Java tiene un tipo estático, lo que significa que todas las variables deben declararse primero antes de que puedan usarse.

Ver tipos de datos primitivos

Mohan Raj B
fuente
3
La necesidad de inicializar las variables antes de usar sus valores no tiene nada que ver con la escritura estática. Escrito estáticamente: debe declarar de qué tipo es una variable. Inicializar antes de usar: debe tener un valor demostrable antes de poder usar el valor.
Jon Bright
@JonBright: La necesidad de declarar tipos de variables tampoco tiene nada que ver con la escritura estática. Por ejemplo, existen lenguajes tipados estáticamente con inferencia de tipos.
hammar
@hammar, a mi modo de ver, puede argumentarlo de dos maneras: con la inferencia de tipos, está declarando implícitamente el tipo de la variable de una manera que el sistema puede inferir. O bien, la inferencia de tipos es una tercera forma, donde las variables no se escriben dinámicamente en tiempo de ejecución, sino que están a nivel de fuente, dependiendo de su uso y las inferencias así hechas. De cualquier manera, la afirmación sigue siendo cierta. Pero tienes razón, no estaba pensando en otros sistemas de tipos.
Jon Bright
2

La línea de código no se compila con una advertencia debido a cómo funciona realmente el código. Cuando ejecuta el código int x = x = 1, Java primero crea la variable x, tal como se define. Luego ejecuta el código de asignación ( x = 1). Como xya está definido, el sistema no tiene errores configurados xen 1. Esto devuelve el valor 1, porque ahora es el valor de x. Por lo tanto, xahora finalmente se establece como 1.
Java básicamente ejecuta el código como si fuera esto:

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

Sin embargo, en su segundo fragmento de código, int x = x + 1la + 1declaración xdebe definirse, lo que para entonces no es así. Dado que las declaraciones de asignación siempre significan que el código a la derecha de =se ejecuta primero, el código fallará porque no xestá definido. Java ejecutaría el código así:

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

Cumplier leyó declaraciones de derecha a izquierda y diseñamos para hacer lo contrario. Por eso le molestó al principio. Haga de esto un hábito para leer declaraciones (código) de derecha a izquierda, no tendrá ese problema.

Ramiz Uddin
fuente