Сначала загадка: что печатает следующий код?
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale(5));
}
private static final long X = scale(10);
private static long scale(long value) {
return X * value;
}
}
Ответ:
0
Спойлеры ниже.
Если вы печатаете X
в масштабе (долго) и переопределяете X = scale(10) + 3
, печать будет X = 0
тогда X = 3
. Это означает, что X
временно установлено значение, 0
а затем установлено значение 3
. Это нарушение final
!
Модификатор static в сочетании с модификатором final также используется для определения констант. Последний модификатор указывает, что значение этого поля не может измениться .
Источник: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [выделение добавлено]
Мой вопрос: это ошибка? Это final
плохо определено?
Вот код, который меня интересует.
X
Ему присваиваются два разных значения: 0
и 3
. Я считаю это нарушением final
.
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale(5));
}
private static final long X = scale(10) + 3;
private static long scale(long value) {
System.out.println("X = " + X);
return X * value;
}
}
Этот вопрос был помечен как возможный дубликат статического конечного порядка инициализации поля Java . Я считаю, что этот вопрос не является дубликатом, так как другой вопрос касается порядка инициализации, в то время как мой вопрос касается циклической инициализации в сочетании с final
тегом. Только из другого вопроса я не смог бы понять, почему код в моем вопросе не делает ошибку.
Это особенно ясно, если посмотреть на вывод, который получает Эрнесто: когда a
он помечен final
, он получает следующий вывод:
a=5
a=5
который не затрагивает основную часть моего вопроса: как final
переменная меняет свою переменную?
источник
X
член похож на обращение к члену подкласса до того, как конструктор суперкласса закончил, это ваша проблема, а не определениеfinal
.A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Ответы:
Очень интересная находка. Чтобы понять это, нам нужно углубиться в спецификацию языка Java ( JLS). ).
Причина в том, что
final
допускается только одно назначение . Однако значением по умолчанию не является присвоение . Фактически, каждая такая переменная ( переменная класса, переменная экземпляра, компонент массива) указывает на свое значение по умолчанию с начала, до присвоений . Первое назначение затем меняет ссылку.Переменные класса и значение по умолчанию
Взгляните на следующий пример:
Мы явно не присваивали значение
x
, хотя оно указывает наnull
его значение по умолчанию. Сравните это с §4.12.5 :Обратите внимание, что это верно только для таких переменных, как в нашем примере. Это не относится к локальным переменным, см. Следующий пример:
Из того же абзаца JLS:
Конечные переменные
Теперь мы посмотрим на
final
, из §4.12.4 :объяснение
Теперь вернемся к вашему примеру, слегка модифицированному:
Выводит
Вспомните, что мы узнали. Внутри метода
assign
переменнойX
был не назначен значение пока. Следовательно, он указывает на свое значение по умолчанию, поскольку он является переменной класса, и в соответствии с JLS эти переменные всегда сразу указывают на свои значения по умолчанию (в отличие от локальных переменных). Послеassign
метода переменнойX
присваивается значение,1
и из-за этогоfinal
мы больше не можем его менять. Таким образом, следующее не будет работать из-заfinal
:Пример в JLS
Благодаря @Andrew я нашел абзац JLS, который охватывает именно этот сценарий, он также демонстрирует его.
Но сначала давайте посмотрим на
Почему это не разрешено, а доступ из метода есть? Взгляните на §8.3.3 котором говорится о том, когда доступ к полям ограничен, если поле еще не было инициализировано.
В нем перечислены некоторые правила, относящиеся к переменным класса:
Все просто,
X = X + 1
эти правила попадают в ловушку, а метод доступа - нет. Они даже перечисляют этот сценарий и приводят пример:источник
X
из метода. Я бы не возражал против этого. Это зависит только от того, как именно JLS определяет вещи для работы в деталях. Я бы никогда не использовал такой код, он просто использует некоторые правила в JLS.forwards references
(который также является частью JLS). это так просто без этого лунного ответа stackoverflow.com/a/49371279/1059372Ничего общего с финалом здесь.
Поскольку он находится на уровне экземпляра или класса, он содержит значение по умолчанию, если еще ничего не назначено. Это причина, которую вы видите,
0
когда получаете доступ к ней без назначения.Если вы
X
получаете доступ без полного назначения, он содержит значения по умолчанию long0
, а значит, и результаты.источник
Не ошибка
Когда первый вызов
scale
вызывается изОн пытается оценить
return X * value
.X
еще не было присвоено значение, и поэтому используется значение по умолчанию для along
(которое0
).Так , что строка кода принимает значение
X * 10
т.е.0 * 10
что есть0
.источник
X = scale(10) + 3
. Так какX
при ссылке на метод, есть0
. Но потом это так3
. Таким образом, OP считает, чтоX
ему присвоены два разных значения, которые могут конфликтовать сfinal
.return X * value
. ЕмуX
еще не присвоено значение и, следовательно, он принимает значение по умолчанию,long
которое есть0
. »? Не сказано, чтоX
ему присваивается значение по умолчанию, ноX
он «заменяется» (пожалуйста, не заключайте в кавычки этот термин;)) значением по умолчанию.Это вовсе не ошибка, проще говоря, это не незаконная форма прямых ссылок, не более того.
Это просто разрешено спецификацией.
Чтобы взять ваш пример, это именно то, где это соответствует:
Вы делаете прямую ссылку на это,
scale
что не является незаконным, как сказано выше, но позволяет вам получить значение по умолчаниюX
. Опять же, это разрешено Spec (точнее, это не запрещено), поэтому он работает просто отличноисточник
Члены уровня класса могут быть инициализированы в коде в пределах определения класса. Скомпилированный байт-код не может инициализировать встроенные члены класса. (Члены экземпляра обрабатываются аналогично, но это не относится к предоставленному вопросу.)
Когда кто-то пишет что-то вроде следующего:
Сгенерированный байт-код будет похож на следующее:
Код инициализации помещается в статический инициализатор, который запускается при первой загрузке класса загрузчиком классов. С этим знанием ваш оригинальный образец будет похож на следующее:
scale(10)
для назначенияstatic final
поляX
.scale(long)
Функция работает в то время как класс частично инициализирован чтение неинициализированного значенияX
которых является по умолчанию длинных или 0.0 * 10
присваиваетсяX
и загрузчик классов завершается.scale(5)
который умножает 5 на теперь инициализированноеX
значение 0, возвращающее 0.Статическое окончательное поле
X
присваивается только один раз, сохраняя гарантию, сохраняемуюfinal
ключевым словом. Для последующего запроса добавления 3 в присваивании вышеуказанный шаг 5 становится оценкой,0 * 10 + 3
являющейся значением,3
а основной метод напечатает результат,3 * 5
которым является значение15
.источник
Чтение неинициализированного поля объекта должно привести к ошибке компиляции. К сожалению для Java, это не так.
Я думаю, что основная причина, почему это так, «скрыта» глубоко в определении того, как объекты создаются и создаются, хотя я не знаю деталей стандарта.
В некотором смысле, final плохо определен, потому что он даже не выполняет своей заявленной цели из-за этой проблемы. Однако, если все ваши классы написаны правильно, у вас нет этой проблемы. Это означает, что все поля всегда установлены во всех конструкторах, и ни один объект не создается без вызова одного из его конструкторов. Это кажется естественным, пока вы не используете библиотеку сериализации.
источник