Является ли окончательный нечетким?
во-первых, головоломка: Что печатает следующий код?
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 в размере(долго) и redefine X = scale(10) + 3,
отпечатки будут X = 0 затем X = 3.
Это значит, что X установлено значение 0 и далее до 3.
Это нарушение final!
static модификатор, в сочетании с помощью конечного модификатора, также используется для определения констант. Последний модификатор указывает, что значение это поле не может изменить.
Источник: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 static final field initialization order.
Я считаю, что этот вопрос не дубликат с
другой вопрос касается порядка инициализации в то время как
мой вопрос касается циклической инициализации в сочетании с final тег.
Из другого вопроса только я бы не смог понять, почему код в моем вопросе не ошибиться.
это особенно ясно, глядя на выход, который получает Эрнесто:
когда a тег final, он получает следующие результаты:
a=5
a=5
который не включает в себя основную часть моего вопроса: как это final переменная изменение переменной?
6 ответов:
очень интересная находка. Чтобы понять это, нам нужно углубиться в спецификацию языка Java (JLS).
причина в том, что
finalтолько один задание. Однако значение по умолчанию-no задание. На самом деле, каждый такую переменную (переменная класса, переменная экземпляра, компонент массива) указывает на его значение по умолчанию С самого начала, перед задания. Этот первое назначение затем изменяет ссылку.
переменные класса и значение по умолчанию
взгляните на следующий пример:
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:
A локальная переменная (§14.4,§14.14) должны быть явно задано значение перед его использованием, либо инициализацией (§14.4) или назначения (§15.26), таким образом, что можно проверить, используя правила для определенного назначения (§16 (Определенного Назначения)).
Final переменные
теперь посмотрим на
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собственный Декларатор или в точке слева от ; иссылка не находится в левой части выражения присваивания (§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потому что инициализатор переменной для
iиспользует метод класса Peek для доступа к значению переменнойjдоjбыл инициализирован его инициализатор переменной, в этот момент он по-прежнему имеет значение по умолчанию (§4.12.5).
ничего общего с финала.
поскольку он находится на уровне экземпляра или класса, он содержит значение по умолчанию, если еще ничего не назначено. Вот почему вы видите
0при доступе к нему без назначения.Если вы обращаетесь к
Xбез полного назначения, он содержит значения по умолчанию long, который является0, отсюда и результаты.
не ошибка.
когда первый звонок
scaleвызывается изprivate static final long X = scale(10);он пытается оценить
return X * value.Xеще не присвоено значение и поэтому значение по умолчанию дляlongиспользуется (что является0).так что строка кода возвращает
X * 10т. е.0 * 10что это0.
это не ошибка вообще, проще говоря, это не незаконная форма прямых ссылок, не более того.
String x = y; String y = "a"; // this will not compile String x = getIt(); // this will compile, but will be null String y = "a"; public String getIt(){ return y; }Это просто разрешено спецификацией.
чтобы взять ваш пример, это именно то, где это соответствует:
private static final long X = scale(10) + 3;Вы делаете прямая ссылка до
scaleэто не является незаконным в любом случае, как было сказано ранее, но позволяет получить значение по умолчаниюX. опять же, это разрешено спецификацией (to точнее это не запрещено), так что работает просто отлично
члены уровня класса могут быть инициализированы в коде в определении класса. Скомпилированный байт-код не может инициализировать члены класса рядный. (Члены экземпляра обрабатываются аналогично, но это не относится к заданному вопросу.)
когда пишешь что-то вроде следующего:
public class Demo1 { private static final long DemoLong1 = 1000; }сгенерированный байт-код будет похож на следующий:
public class Demo2 { private static final long DemoLong2; static { DemoLong2 = 1000; } }код инициализации помещается в статический инициализатор, который является запуск при первой загрузке класса загрузчиком класса. С этими знаниями ваш исходный образец будет похож на следующее:
public class RecursiveStatic { private static final long X; private static long scale(long value) { return X * value; } static { X = scale(10); } public static void main(String[] args) { System.out.println(scale(5)); } }
- JVM загружает RecursiveStatic в качестве точки входа jar.
- загрузчик классов запускает статический инициализатор при загрузке определения класса.
- инициализатор вызывает функцию
scale(10)присвоитьstatic finalполеX.- The
scale(long)функции как класс частично инициализированное чтение неинициализированного значенияXкоторый является значением по умолчанию long или 0.- значение
0 * 10назначенаXи загрузчик классов завершается.- JVM запускает публичный статический метод void main, вызывающий
scale(5), который умножает 5 на инициализированныйXзначение 0 возвращается 0.статическое конечное поле
Xназначается только один раз, сохраняя гарантию, удерживаемуюfinalключевое слово. Для последующий запрос добавления 3 в присвоение, Шаг 5 выше становится оценкой0 * 10 + 3значение3и основной метод выведет результат3 * 5значение15.
чтение неинициализированного поля объекта должно привести к ошибке компиляции. К сожалению для Java, это не так.
Я думаю, что основная причина, почему это так "спрятана" глубоко в определение того, как объекты создаются и строятся, хотя я не знаю подробностей стандарта.
в некотором смысле, final плохо определен, потому что он даже не выполняет то, что его заявленная цель связана с этой проблемой. Однако, если все ваши классы правильно написано, у вас нет этой проблемы. Смысл всех полей во всех конструкторов и никакой объект никогда не создается без вызова одного из конструкторов. Что кажется естественным, пока вы не должны использовать библиотеку сериализации.