Не определено ли окончательное?

Сначала загадка: что печатает следующий код?

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 в масштабе (long) и переопределите 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 переменная меняет свою переменную?

Ответ 1

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

потому что инициализатор переменной для i использует метод peek метода для доступа к значению переменной j до того, как j был инициализирован его инициализатором переменной, после чего он по- прежнему имеет значение по умолчанию (§4.12.5).

Ответ 2

Здесь нет ничего общего.

Поскольку он находится на уровне экземпляра или класса, он сохраняет значение по умолчанию, если ничего не назначено. Именно по этой причине вы видите 0 когда получаете доступ к ней, не назначая.

Если вы получаете доступ к X без полного назначения, оно содержит значения по умолчанию long, которое равно 0, следовательно, результаты.

Ответ 3

Не ошибка.

Когда первый вызов scale вызывается из

private static final long X = scale(10);

Он пытается оценить значение return X * value. X еще не присвоено значение, и поэтому используется значение по умолчанию для long (которое равно 0).

Таким образом, строка кода оценивается как X * 10 то есть 0 * 10 что равно 0.

Ответ 4

Это вовсе не ошибка, просто говоря, что это не незаконная форма прямых ссылок, не более того.

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 опять же, это разрешено Spec (точнее, это не запрещено), поэтому он работает просто отлично

Ответ 5

Элементы уровня класса могут быть инициализированы кодом в определении класса. Скомпилированный байт-код не может инициализировать члены класса inline. (Члены экземпляра обрабатываются аналогично, но это не относится к предоставленному вопросу).

Когда вы пишете что-то вроде следующего:

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));
    }
}
  1. JVM загружает RecursiveStatic в качестве точки входа в jar.
  2. Загрузчик классов запускает статический инициализатор при загрузке определения класса.
  3. Инициализатор вызывает scale(10) функций scale(10) чтобы назначить static final поле X
  4. Функция scale(long) выполняется, пока класс частично инициализирован, считывая неинициализированное значение X которое является значением по умолчанию long или 0.
  5. Значение 0 * 10 назначается X и загрузчик классов завершается.
  6. JVM запускает общедоступный статический метод void, вызывающий scale(5) который умножает 5 на теперь инициализированное значение X 0, возвращающее 0.

Статическое последнее поле X назначается лишь один раз, сохраняя гарантию проводится в final ключевом слове. Для последующего запроса добавления 3 в присваивание, шаг 5 выше становится оценкой 0 * 10 + 3 которая является значением 3 и основной метод напечатает результат 3 * 5 который является значением 15.

Ответ 6

Чтение неинициализированного поля объекта должно приводить к ошибке компиляции. К сожалению, для Java это не так.

Я думаю, что фундаментальная причина, по которой это происходит, "скрыта" глубоко в определении того, как объекты создаются и создаются, хотя я не знаю деталей стандарта.

В некотором смысле, окончание плохо определено, потому что оно даже не выполняет то, что его заявленная цель вызвана этой проблемой. Однако, если все ваши классы правильно написаны, у вас нет этой проблемы. Значение всех полей всегда задается во всех конструкторах и ни один объект не создается без вызова одного из его конструкторов. Это кажется естественным, пока вы не будете использовать библиотеку сериализации.