Очень интересная находка. Чтобы понять это, нам нужно углубиться в спецификацию языка Java ( JLS). ).
Причина в том, что final
допускается только одно назначение . Однако значением по умолчанию не является присвоение . Фактически, каждая такая переменная ( переменная класса, переменная экземпляра, компонент массива) указывает на свое значение по умолчанию с начала, до присвоений . Первое назначение затем меняет ссылку.
Переменные класса и значение по умолчанию
Взгляните на следующий пример:
private static Object x;
public static void main(String[] args) {
System.out.println(x); // Prints 'null'
}
Мы явно не присваивали значение x
, хотя оно указывает на null
его значение по умолчанию. Сравните это с §4.12.5 :
Начальные значения переменных
Каждая переменная класса, переменная экземпляра или компонент массива инициализируется значением по умолчанию при его создании ( §15.9 , §15.10.2 )
Обратите внимание, что это верно только для таких переменных, как в нашем примере. Это не относится к локальным переменным, см. Следующий пример:
public static void main(String[] args) {
Object x;
System.out.println(x);
// Compile-time error:
// variable x might not have been initialized
}
Из того же абзаца JLS:
Локальная переменная ( §14.4 , §14.14 ) должно быть явно присвоено значение , прежде чем она используется, либо инициализации ( §14.4 ) или присваивания ( §15.26 ), таким образом , что можно проверить , используя правила для определенного присваивания ( § 16 (Определенное задание) ).
Конечные переменные
Теперь мы посмотрим на final
, из §4.12.4 :
окончательные переменные
Переменная может быть объявлена как финальная . Окончательная переменная может быть только назначены один раз . Это ошибка времени компиляции, если конечная переменная назначена, если только она не была определенно назначена непосредственно перед назначением ( §16 (Определенное назначение) ).
объяснение
Теперь вернемся к вашему примеру, слегка модифицированному:
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;
}
Выводит
Before: 0
After: 1
Вспомните, что мы узнали. Внутри метода assign
переменной X
был не назначен значение пока. Следовательно, он указывает на свое значение по умолчанию, поскольку он является переменной класса, и в соответствии с JLS эти переменные всегда сразу указывают на свои значения по умолчанию (в отличие от локальных переменных). После assign
метода переменной X
присваивается значение, 1
и из-за этого final
мы больше не можем его менять. Таким образом, следующее не будет работать из-за final
:
private static long assign() {
// Assign X
X = 1;
// Second assign after method will crash
return X + 1;
}
Пример в JLS
Благодаря @Andrew я нашел абзац JLS, который охватывает именно этот сценарий, он также демонстрирует его.
Но сначала давайте посмотрим на
private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer
Почему это не разрешено, а доступ из метода есть? Взгляните на §8.3.3 котором говорится о том, когда доступ к полям ограничен, если поле еще не было инициализировано.
В нем перечислены некоторые правила, относящиеся к переменным класса:
Для ссылки простым именем на переменную класса, f
объявленную в классе или интерфейсе C
, это ошибка времени компиляции, если :
Ссылка появляется либо в инициализаторе переменной класса, C
либо в статическом инициализаторе C
( §8.7 ); и
Ссылка появляется либо в инициализаторе f
собственного декларатора, либо в точке слева от f
декларатора; и
Ссылка не находится на левой стороне выражения присваивания ( §15.26 ); и
Внутренний класс или интерфейс, содержащий ссылку, это C
.
Все просто, X = X + 1
эти правила попадают в ловушку, а метод доступа - нет. Они даже перечисляют этот сценарий и приводят пример:
Доступ по этим методам не проверяется, поэтому:
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);
}
}
производит вывод:
0
потому что инициализатор переменной for i
использует метод класса peek для доступа к значению переменной j
до того, как j
он был инициализирован ее инициализатором переменной, и в этот момент он все еще имеет значение по умолчанию ( §4.12.5 ).
X
член похож на обращение к члену подкласса до того, как конструктор суперкласса закончил, это ваша проблема, а не определениеfinal
.