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 0
Spoilers a continuación.
Si imprime X
en escala (largo) y redefine X = scale(10) + 3
, las impresiones serán X = 0
entonces X = 3
. Esto significa que X
se establece temporalmente en 0
y luego se establece en 3
. Esto 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? ¿Está final
mal definido?
Aquí está el código que me interesa.
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 de 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 final
etiqueta. 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
él, obtiene el siguiente resultado:
a=5
a=5
que no involucra la parte principal de mi pregunta: ¿Cómo final
cambia una variable su variable?
fuente
X
miembro es como referirse a un miembro de la subclase antes de que el constructor de la superclase haya terminado, ese es su problema y no la definición definal
.A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Respuestas:
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:
No asignamos explícitamente un valor a
x
, aunque apunta anull
, su valor predeterminado. Compare eso con §4.12.5 :Tenga en cuenta que esto solo es válido para ese tipo de variables, como en nuestro ejemplo. No es válido para las variables locales, consulte el siguiente ejemplo:
Del mismo párrafo de JLS:
Variables finales
Ahora echamos un vistazo a
final
, desde §4.12.4 :Explicación
Ahora volviendo al ejemplo, ligeramente modificado:
Sale
Recordemos lo que hemos aprendido. Dentro del método, a
assign
la variable todavía noX
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 delassign
método, a la variableX
se le asigna el valor1
y porfinal
eso ya no podemos cambiarlo. Entonces, lo siguiente no funcionaría debido afinal
: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
¿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 se restringe el acceso a los campos si el campo aún no se ha inicializado.
Enumera algunas reglas relevantes para las variables de clase:
Es simple, el
X = X + 1
es atrapado por esas reglas, el método no tiene acceso. Incluso enumeran este escenario y dan un ejemplo:fuente
X
desde el método. No me importaría tanto. Solo depende de cómo exactamente JLS define las cosas para trabajar en detalle. Nunca usaría un código así, solo está explotando algunas reglas en el JLS.forwards references
(eso también es parte de JLS). esto es tan simple sin esta respuesta tan apilada stackoverflow.com/a/49371279/1059372Nada que ver con la final aquí.
Como está en el nivel de instancia o clase, mantiene el valor predeterminado si todavía no se asigna nada. Esa es la razón por la que ve
0
cuando accede a él sin asignar.Si accede
X
sin asignar completamente, contiene los valores predeterminados de long que es0
, de ahí los resultados.fuente
No es un error
Cuando la primera llamada a
scale
se llama desdeIntenta evaluar
return X * value
.X
aún no se le ha asignado un valor y, por lo tanto,long
se utiliza el valor predeterminado para a (que es0
).Entonces esa línea de código se evalúa,
X * 10
es decir,0 * 10
cuál es0
.fuente
X = scale(10) + 3
. DesdeX
, cuando se hace referencia desde el método, es0
. Pero luego lo es3
. Entonces OP piensa queX
se le asignan dos valores diferentes, lo que entraría en conflictofinal
.return X * value
.X
No se le ha asignado un valor todavía y, por tanto, toma el valor por defecto para unalong
que es0
. "? No se dice queX
se le asigna el valor predeterminado, sino queX
se "reemplaza" (no cite ese término;)) por el valor predeterminado.No es un error en absoluto, simplemente no es una forma ilegal de referencias directas, nada más.
Simplemente lo permite la Especificación.
Para tomar su ejemplo, esto es exactamente donde esto coincide:
Está haciendo una referencia directa a
scale
eso que no es ilegal de ninguna manera como se dijo anteriormente, pero le permite obtener el valor predeterminado deX
. De nuevo, esto está permitido por la especificación (para ser más exactos, no está prohibido), por lo que funciona bienfuente
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:
El código de bytes generado sería similar al siguiente:
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:
scale(10)
para asignar elstatic final
campoX
.scale(long)
función se ejecuta mientras la clase se inicializa parcialmente leyendo el valor no inicializadoX
cuyo valor predeterminado es long o 0.0 * 10
asigna el valor deX
y se completa el cargador de clases.scale(5)
que multiplica 5 por elX
valor ahora inicializado de 0 que devuelve 0.El campo final estático
X
solo se asigna una vez, preservando la garantía de lafinal
palabra clave. Para la consulta posterior de sumar 3 en la asignación, el paso 5 anterior se convierte en la evaluación de0 * 10 + 3
cuál es el valor3
y el método principal imprimirá el resultado de3 * 5
cuál es el valor15
.fuente
La lectura de un campo no inicializado de un objeto debería dar como resultado 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 no está bien 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. Lo que significa que 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.
fuente