Подтвердить что ты не робот

Почему попытка печати неинициализированной переменной не всегда приводит к появлению сообщения об ошибке

Некоторые могут найти его похожим на вопрос SO Завершают ли переменные Java Final значения по умолчанию?, но этот ответ не полностью решает это, так как этот вопрос не прямо распечатайте значение x внутри блока инициализатора экземпляра.

Проблема возникает, когда я пытаюсь распечатать x непосредственно внутри блока инициализатора экземпляра, присвоив значение x до конца блока:

Случай 1

class HelloWorld {

    final int x;

    {
        System.out.println(x);
        x = 7;
        System.out.println(x);    
    }

    HelloWorld() {
        System.out.println("hi");
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}

Это дает ошибку времени компиляции, указывающую, что переменная x не была инициализирована.

$ javac HelloWorld.java
HelloWorld.java:6: error: variable x might not have been initialized
        System.out.println(x);
                           ^
1 error

Случай 2

Вместо прямой печати я вызываю функцию для печати:

class HelloWorld {

    final int x;

    {
        printX();
        x = 7;
        printX();
    }

    HelloWorld() {
        System.out.println("hi");
    }

    void printX() {
        System.out.println(x);
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}

Это правильно компилируется и дает результат

0
7
hi

Какова концептуальная разница между двумя случаями?

4b9b3361

Ответ 1

В JLS, §8.3.3. Прямые ссылки во время инициализации поля, в нем указано, что существует ошибка времени компиляции, когда:

Использование переменных экземпляра, чьи объявления появляются текстовыми после использования, иногда ограничены, хотя эти переменные экземпляра находятся в сфере охвата. В частности, это ошибка времени компиляции, если все следующие верно:

  • Объявление переменной экземпляра в классе или интерфейсе C появляется после использования переменной экземпляра text

  • Использование - простое имя либо в инициализаторе переменной экземпляра C, либо в инициализаторе экземпляра C;

  • Использование не в левой части задания;

  • C - самый внутренний класс или интерфейс, охватывающий использование.

Следующие правила приводятся с несколькими примерами, наиболее близкими к вашему:

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 Начальные значения переменных).

Итак, чтобы обобщить ваш второй пример, компилируется и выполняется отлично, потому что компилятор не проверяет, была ли инициализирована переменная x при вызове printX() и когда printX() выполняется в Runtime, x будет присвоено значение по умолчанию (0).

Ответ 2

Чтение JLS, ответ, как представляется, находится в разделе 16.2.2:

Пустое поле final member V определенно назначено (и, кроме того, не определенно не назначено) перед блоком (§14.2), являющимся телом любого метода в области V и до объявления любой класс, объявленный в рамках V.

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

Однако, когда вы обращаетесь к полю вне метода, он считается неназначенным, следовательно, является ошибкой компиляции. Следующий код также не будет компилироваться:

public class Main {
    final int x;
    {
        method();
        System.out.println(x);
        x = 7;
    }
    void method() { }
    public static void main(String[] args) { }
}

потому что:

  • V присваивается [un] перед любым другим оператором S блока iff V является [un] назначенным после оператора, непосредственно предшествующего S в блоке.

Поскольку окончательное поле x не назначено перед вызовом метода, оно по-прежнему не назначается после него.

Эта заметка в JLS также актуальна:

Обратите внимание, что нет правил, которые позволили бы нам заключить, что V определенно не назначен перед блоком, который является телом любого конструктора, метода, инициализатора экземпляра или статического инициализатора, объявленного в C. Мы можем неофициально заключить, что V не определенно не назначается перед блоком, который является телом любого конструктора, метода, инициализатора экземпляра или статического инициализатора, объявленного в C, но нет необходимости, чтобы такое правило было указано явно.

Ответ 3

Хорошо, вот мои 2 цента.

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

Нет ошибок. Случай:

Поэтому, когда вы используете внутри метода, оно уже имеет значение.

 1) If you initialize it, that value.
 2) If not, the default value of data type. 

Ошибка:

Когда вы делаете это в блоке инициализации, который вы видите ошибки.

Если вы посмотрите на docs of initialization block

{
    // whatever code is needed for initialization goes here
}

и

Компилятор Java копирует блоки инициализации в каждый конструктор. Поэтому этот подход может использоваться для совместного использования блока кода между несколькими конструкторами.

В глазах компилятора ваш код буквально равен

class HelloWorld {

    final int x;
    HelloWorld() {
        System.out.println(x);  ------------ ERROR here obviously
        x = 7;
        System.out.println(x);  
        System.out.println("hi");
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}

Вы используете его до его инициализации.

Ответ 4

Разница в том, что в первом случае вы вызываете System.out.println из блока инициализации, поэтому блок, который вызывается перед конструктором. В первой строке

System.out.println(x);

variable x еще не инициализирован, поэтому вы получаете ошибку компиляции.

Но во втором случае вы вызываете метод экземпляра, который не знает, была ли инициализирована переменная, поэтому у вас нет ошибки компиляции, и вы можете увидеть значение по умолчанию для x

Ответ 5

Случай 1:

Дает вам ошибку компиляции,

Потому что в System.out.println(x);

вы пытаетесь напечатать x, который никогда не был инициализирован.

Случай 2:

Работает, потому что вы прямо не используете какие-либо литеральные значения, вместо этого вы вызываете некоторый метод, который является правильным.

Общее правило:

Если вы пытаетесь получить доступ к любой переменной, которая никогда не инициализируется то он даст ошибку компиляции.

Ответ 6

Мы имеем дело с блоком инициализатора. Компилятор Java копирует блоки инициализации в каждый конструктор.

Ошибка компилятора не возникает во втором примере, поскольку печать x находится в другом фрейме, см. спецификацию.