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:
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, su 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 las 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:
A una variable local ( §14.4 , §14.14 ) se le debe dar un valor explícito antes de usarla, ya sea por inicialización ( §14.4 ) o asignación ( §15.26 ), de manera que se pueda verificar usando las reglas para la asignación definitiva ( § 16 (Asignación definida) ).
Variables finales
Ahora echamos un vistazo a final, desde §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, a assignla 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 del assignmétodo, a la variable Xse le asigna el valor 1y por finaleso ya no podemos cambiarlo. Entonces, lo siguiente no funcionaría debido a 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 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:
Para una referencia por nombre simple a una variable de clase fdeclarada en clase o interfaz C, es un error en tiempo de compilación si :
La referencia aparece en un inicializador de variable de clase de Co en un inicializador estático de C( §8.7 ); y
La referencia aparece en el inicializador del fpropio declarador o en un punto a la izquierda del fdeclarador; 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 + 1es atrapado por esas reglas, el método no tiene acceso. 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 de variables iusa el método de clase peek para acceder al valor de la variable jantes de que jhaya sido inicializado por su inicializador de variables, en cuyo punto todavía tiene su valor predeterminado ( §4.12.5 ).
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.