Una scoperta molto interessante. Per capirlo dobbiamo scavare nella Java Language Specification ( JLS ).
Il motivo è che final
consente solo un compito . Il valore predefinito, tuttavia, non è un'assegnazione . In effetti, ciascuna di tali variabili ( variabile di classe, variabile di istanza, componente di matrice) punta al suo valore predefinito dall'inizio, prima delle assegnazioni . Il primo compito quindi modifica il riferimento.
Variabili di classe e valore predefinito
Dai un'occhiata al seguente esempio:
private static Object x;
public static void main(String[] args) {
System.out.println(x); // Prints 'null'
}
Non abbiamo assegnato esplicitamente un valore a x
, sebbene punti a null
, è il valore predefinito. Confrontalo con §4.12.5 :
Valori iniziali delle variabili
Ogni variabile di classe , variabile di istanza o componente dell'array viene inizializzata con un valore predefinito quando viene creata ( §15.9 , §15.10.2 )
Nota che questo vale solo per quel tipo di variabili, come nel nostro esempio. Non vale per le variabili locali, vedere il seguente esempio:
public static void main(String[] args) {
Object x;
System.out.println(x);
// Compile-time error:
// variable x might not have been initialized
}
Dallo stesso paragrafo JLS:
Una variabile locale ( §14.4 , §14.14 ) deve ricevere esplicitamente un valore prima di essere utilizzata, mediante inizializzazione ( §14.4 ) o assegnazione ( §15.26 ), in un modo che può essere verificato utilizzando le regole per l'assegnazione definitiva ( § 16 (Assegnazione definita) ).
Variabili finali
Ora diamo un'occhiata final
, dal §4.12.4 :
Variabili finali
Una variabile può essere dichiarata finale . Un ultimo variabile può essere solo assegnato a volta . È un errore in fase di compilazione se viene assegnata una variabile finale a meno che non sia definitivamente non assegnata immediatamente prima dell'assegnazione ( §16 (Assegnazione definita) ).
Spiegazione
Ora torniamo al tuo esempio, leggermente modificato:
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;
}
Emette
Before: 0
After: 1
Ricorda ciò che abbiamo imparato. All'interno del metodo assign
alla variabile non èX
stato ancora assegnato un valore. Pertanto, punta al suo valore predefinito poiché è una variabile di classe e secondo la JLS tali variabili puntano sempre immediatamente ai loro valori predefiniti (contrariamente alle variabili locali). Dopo il assign
metodo alla variabile X
viene assegnato il valore 1
e per final
questo non possiamo più cambiarlo. Quindi il seguente non funzionerebbe a causa di final
:
private static long assign() {
// Assign X
X = 1;
// Second assign after method will crash
return X + 1;
}
Esempio nel JLS
Grazie a @Andrew ho trovato un paragrafo JLS che copre esattamente questo scenario, lo dimostra anche.
Ma prima diamo un'occhiata
private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer
Perché questo non è consentito, mentre l'accesso dal metodo è? Dai un'occhiata al § 8.3.3 che parla di quando gli accessi ai campi sono limitati se il campo non è stato ancora inizializzato.
Elenca alcune regole rilevanti per le variabili di classe:
Per un riferimento in base al nome semplice a una variabile di classe f
dichiarata in classe o interfaccia C
, è un errore di compilazione se :
Il riferimento appare in un inizializzatore variabile di classe di C
o in un inizializzatore statico di C
( §8.7 ); e
Il riferimento appare o nell'inizializzatore del f
proprio dichiaratore o in un punto alla sinistra del f
dichiaratore; e
Il riferimento non si trova sul lato sinistro di un'espressione di assegnazione ( §15.26 ); e
La classe o l'interfaccia più interna che racchiude il riferimento è C
.
È semplice, X = X + 1
viene catturato da queste regole, l'accesso al metodo no. Elencano persino questo scenario e danno un esempio:
Gli accessi con i metodi non vengono controllati in questo modo, quindi:
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 l'output:
0
poiché l'inizializzatore della variabile i
utilizza il metodo di classe peek per accedere al valore della variabile j
prima che j
sia stato inizializzato dal suo inizializzatore di variabile, a quel punto ha ancora il suo valore predefinito ( §4.12.5 ).
X
membro è come fare riferimento a un membro della sottoclasse prima che il costruttore della superclasse abbia terminato, questo è il tuo problema e non la definizione difinal
.