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
, 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 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 del assign
método, a la variable X
se le asigna el valor 1
y por final
eso 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 f
declarada 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 C
o en un inicializador estático de C
( §8.7 ); y
La referencia aparece en el inicializador del f
propio declarador o en un punto a la izquierda del f
declarador; 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
es 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 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 de variables, en cuyo punto todavía tiene su valor predeterminado ( §4.12.5 ).
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
.