Является ли окончательный нечетким?


во-первых, головоломка: Что печатает следующий код?

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 177

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

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

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

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

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