Очень интересная находка. Чтобы понять это, нам нужно углубиться в спецификацию языка 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.