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 Xen escala (largo) y redefine X = scale(10) + 3, las impresiones serán X = 0entonces X = 3. Esto significa que Xse establece temporalmente en 0y 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á finalmal definido?
Aquí está el código que me interesa.
XSe le asignan dos valores diferentes: 0y 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 finaletiqueta. 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 ase etiqueta con finalél, obtiene el siguiente resultado:
a=5
a=5
que no involucra la parte principal de mi pregunta: ¿Cómo finalcambia una variable su variable?
fuente

Xmiembro 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
finalsolo 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
assignla variable todavía noXse 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 delassignmétodo, a la variableXse le asigna el valor1y porfinaleso 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 + 1es atrapado por esas reglas, el método no tiene acceso. Incluso enumeran este escenario y dan un ejemplo:fuente
Xdesde 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
0cuando accede a él sin asignar.Si accede
Xsin asignar completamente, contiene los valores predeterminados de long que es0, de ahí los resultados.fuente
No es un error
Cuando la primera llamada a
scalese llama desdeIntenta evaluar
return X * value.Xaún no se le ha asignado un valor y, por lo tanto,longse utiliza el valor predeterminado para a (que es0).Entonces esa línea de código se evalúa,
X * 10es decir,0 * 10cuál es0.fuente
X = scale(10) + 3. DesdeX, cuando se hace referencia desde el método, es0. Pero luego lo es3. Entonces OP piensa queXse le asignan dos valores diferentes, lo que entraría en conflictofinal.return X * value.XNo se le ha asignado un valor todavía y, por tanto, toma el valor por defecto para unalongque es0. "? No se dice queXse le asigna el valor predeterminado, sino queXse "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
scaleeso 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 finalcampoX.scale(long)función se ejecuta mientras la clase se inicializa parcialmente leyendo el valor no inicializadoXcuyo valor predeterminado es long o 0.0 * 10asigna el valor deXy se completa el cargador de clases.scale(5)que multiplica 5 por elXvalor ahora inicializado de 0 que devuelve 0.El campo final estático
Xsolo se asigna una vez, preservando la garantía de lafinalpalabra clave. Para la consulta posterior de sumar 3 en la asignación, el paso 5 anterior se convierte en la evaluación de0 * 10 + 3cuál es el valor3y el método principal imprimirá el resultado de3 * 5cuá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