ejemplo create java final class-variables static-initialization

create - text box java



¿El final está mal definido? (6)

Primero, un rompecabezas: ¿qué imprime el siguiente código?

public class RecursiveStatic { public static void main(String[] args) { System.out.println(scale(5)); } private static final long X = scale(10); private static long scale(long value) { return X * value; } }

Responder:

0

Spoilers a continuación.

Si imprime X en escala (larga) y redefine X = scale(10) + 3 , las impresiones serán X = 0 luego X = 3 . Esto significa que X se establece temporalmente en 0 y luego se establece en 3 . Esta es una violación de final !

El modificador estático, en combinación con el modificador final, también se utiliza para definir constantes. El modificador final indica que el valor de este campo no puede cambiar .

Fuente: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [énfasis agregado]

Mi pregunta: ¿es esto un error? ¿El final está mal definido?

Aquí está el código que me interesa. A X se le asignan dos valores diferentes: 0 y 3 . Creo que esto es una violación de final .

public class RecursiveStatic { public static void main(String[] args) { System.out.println(scale(5)); } private static final long X = scale(10) + 3; private static long scale(long value) { System.out.println("X = " + X); return X * value; } }

Esta pregunta se ha marcado como un posible duplicado del orden de inicialización del campo final estático de Java . Creo que esta pregunta no es un duplicado, ya que la otra pregunta aborda el orden de inicialización, mientras que mi pregunta aborda una inicialización cíclica combinada con la etiqueta final . Solo con la otra pregunta, no podría entender por qué el código en mi pregunta no comete un error.

Esto es especialmente claro al observar el resultado que obtiene ernesto: cuando a se etiqueta con final , obtiene el siguiente resultado:

a=5 a=5

lo cual no involucra la parte principal de mi pregunta: ¿Cómo una variable final cambia su variable?


La lectura de un campo no inicializado de un objeto debería provocar un error de compilación. Desafortunadamente para Java, no lo hace.

Creo que la razón fundamental por la que este es el caso está "oculta" en lo profundo de la definición de cómo se instancian y construyen los objetos, aunque no conozco los detalles del estándar.

En cierto sentido, final está mal definido porque ni siquiera cumple con su propósito debido a este problema. Sin embargo, si todas sus clases están escritas correctamente, no tiene este problema. Es decir, todos los campos siempre se establecen en todos los constructores y nunca se crea ningún objeto sin llamar a uno de sus constructores. Eso parece natural hasta que tenga que usar una biblioteca de serialización.


Los miembros de nivel de clase se pueden inicializar en código dentro de la definición de clase. El bytecode compilado no puede inicializar los miembros de la clase en línea. (Los miembros de la instancia se manejan de manera similar, pero esto no es relevante para la pregunta proporcionada).

Cuando uno escribe algo como lo siguiente:

public class Demo1 { private static final long DemoLong1 = 1000; }

El código de bytes generado sería similar al siguiente:

public class Demo2 { private static final long DemoLong2; static { DemoLong2 = 1000; } }

El código de inicialización se coloca dentro de un inicializador estático que se ejecuta cuando el cargador de clases carga por primera vez la clase. Con este conocimiento, su muestra original sería similar a la siguiente:

public class RecursiveStatic { private static final long X; private static long scale(long value) { return X * value; } static { X = scale(10); } public static void main(String[] args) { System.out.println(scale(5)); } }

  1. La JVM carga el RecursiveStatic como punto de entrada del jar.
  2. El cargador de clases ejecuta el inicializador estático cuando se carga la definición de clase.
  3. El inicializador llama a la scale(10) función scale(10) para asignar el campo static final X
  4. La función de scale(long) ejecuta mientras la clase se inicializa parcialmente leyendo el valor no inicializado de X que es el valor predeterminado de largo o 0.
  5. El valor de 0 * 10 se asigna a X y se completa el cargador de clases.
  6. La JVM ejecuta la scale(5) llamada al método principal público de vacío estático scale(5) que multiplica 5 por el valor X ahora inicializado de 0 que devuelve 0.

El campo final estático X solo se asigna una vez, preservando la garantía que posee la palabra clave final . Para la consulta posterior de agregar 3 en la asignación, el paso 5 anterior se convierte en la evaluación de 0 * 10 + 3 que es el valor 3 y el método principal imprimirá el resultado de 3 * 5 que es el valor 15 .


Nada que ver con la final aquí.

Como está en el nivel de instancia o clase, tiene el valor predeterminado si aún no se asigna nada. Esa es la razón por la que ve 0 cuando accede a él sin asignar.

Si accede a X sin asignarlo completamente, contiene los valores predeterminados de long, que es 0 , de ahí los resultados.


No es un error en absoluto, simplemente no es una forma ilegal de referencias directas, nada más.

String x = y; String y = "a"; // this will not compile String x = getIt(); // this will compile, but will be null String y = "a"; public String getIt(){ return y; }

Simplemente lo permite la Especificación.

Para tomar su ejemplo, esto es exactamente donde esto coincide:

private static final long X = scale(10) + 3;

Está haciendo una referencia directa a la scale que no es ilegal de ninguna manera como se dijo anteriormente, pero le permite obtener el valor predeterminado de X De nuevo, esto está permitido por la especificación (para ser más exactos, no está prohibido), por lo que funciona bien


Un hallazgo muy interesante. Para entenderlo, debemos profundizar en la Especificación del lenguaje Java ( JLS ).

La razón es que final solo permite una asignación . El valor predeterminado, sin embargo, no es una asignación . De hecho, cada una de esas variables ( variable de clase, variable de instancia, componente de matriz) apunta a su valor predeterminado desde el principio, antes de las asignaciones . La primera asignación luego cambia la referencia.

Variables de clase y valor predeterminado

Eche un vistazo al siguiente ejemplo:

private static Object x; public static void main(String[] args) { System.out.println(x); // Prints ''null'' }

No asignamos explícitamente un valor a x , aunque apunta a null , es el valor predeterminado. Compare eso con §4.12.5 :

Valores iniciales de variables

Cada variable de clase , variable de instancia o componente de matriz se inicializa con un valor predeterminado cuando se crea ( §15.9 , §15.10.2 )

Tenga en cuenta que esto solo es válido para ese tipo de variables, como en nuestro ejemplo. No es válido para variables locales, consulte el siguiente ejemplo:

public static void main(String[] args) { Object x; System.out.println(x); // Compile-time error: // variable x might not have been initialized }

Del mismo párrafo de JLS:

Una variable local ( §14.4 , §14.14 ) debe recibir explícitamente un valor antes de su uso, ya sea por inicialización ( §14.4 ) o asignación ( §15.26 ), de manera que pueda verificarse utilizando las reglas para la asignación definitiva ( § 16 (Asignación definida) ).

Variables finales

Ahora echamos un vistazo a final , de §4.12.4 :

Variables finales

Una variable puede ser declarada final . Una variable final solo se puede asignar a una vez . Es un error en tiempo de compilación si se asigna una variable final a menos que esté definitivamente sin asignar inmediatamente antes de la asignación ( §16 (Asignación definida) ).

Explicación

Ahora volviendo al ejemplo, ligeramente modificado:

public static void main(String[] args) { System.out.println("After: " + X); } private static final long X = assign(); private static long assign() { // Access the value before first assignment System.out.println("Before: " + X); return X + 1; }

Sale

Before: 0 After: 1

Recordemos lo que hemos aprendido. Dentro del método assign la variable X todavía no se le asignó un valor. Por lo tanto, apunta a su valor predeterminado, ya que es una variable de clase y, según el JLS, esas variables siempre apuntan inmediatamente a sus valores predeterminados (en contraste con las variables locales). Después del método de assign , a la variable X se le asigna el valor 1 y, debido al final , ya no podemos cambiarlo. Entonces lo siguiente no funcionaría debido a la final :

private static long assign() { // Assign X X = 1; // Second assign after method will crash return X + 1; }

Ejemplo en el JLS

Gracias a @Andrew encontré un párrafo JLS que cubre exactamente este escenario, también lo demuestra.

Pero primero echemos un vistazo a

private static final long X = X + 1; // Compile-time error: // self-reference in initializer

¿Por qué esto no está permitido, mientras que el acceso desde el método sí lo está? Eche un vistazo a §8.3.3 que trata sobre cuándo los accesos a los campos están restringidos si el campo aún no se ha inicializado.

Enumera algunas reglas relevantes para las variables de clase:

Para una referencia por nombre simple a una variable de clase f declarada en la clase o interfaz C , es un error en tiempo de compilación si :

  • La referencia aparece en un inicializador variable de clase de C o en un inicializador estático de C ( §8.7 ); y

  • La referencia aparece en el inicializador del propio declarador de f o en un punto a la izquierda del declarador de f ; y

  • La referencia no está en el lado izquierdo de una expresión de asignación ( §15.26 ); y

  • La clase o interfaz más interna que encierra la referencia es C

Es simple, el X = X + 1 está atrapado por esas reglas, el método de acceso no. Incluso enumeran este escenario y dan un ejemplo:

Los accesos por métodos no se verifican de esta manera, así que:

class Z { static int peek() { return j; } static int i = peek(); static int j = 1; } class Test { public static void main(String[] args) { System.out.println(Z.i); } }

produce la salida:

0

porque el inicializador variable para i usa el método de clase peek para acceder al valor de la variable j antes de que j haya sido inicializado por su inicializador variable, en cuyo punto todavía tiene su valor predeterminado ( §4.12.5 ).


No es un error

Cuando se llama la primera llamada a scale desde

private static final long X = scale(10);

Intenta evaluar el return X * value . X todavía no se le ha asignado un valor y, por lo tanto, se utiliza el valor predeterminado durante un long (que es 0 ).

Entonces esa línea de código se evalúa como X * 10 es decir, 0 * 10 que es 0 .