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));
}
}
- La JVM carga el RecursiveStatic como punto de entrada del jar.
- El cargador de clases ejecuta el inicializador estático cuando se carga la definición de clase.
-
El inicializador llama a la
scale(10)
funciónscale(10)
para asignar el campostatic final
X
-
La función de
scale(long)
ejecuta mientras la clase se inicializa parcialmente leyendo el valor no inicializado deX
que es el valor predeterminado de largo o 0. -
El valor de
0 * 10
se asigna aX
y se completa el cargador de clases. -
La JVM ejecuta la
scale(5)
llamada al método principal público de vacío estáticoscale(5)
que multiplica 5 por el valorX
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 interfazC
, 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 deC
( §8.7 ); yLa referencia aparece en el inicializador del propio declarador de
f
o en un punto a la izquierda del declarador def
; yLa 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 variablej
antes de quej
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
.