Почему мы не можем использовать неинициализированную локальную переменную для доступа к статическому контенту из типа этой переменной?

При попытке получить доступ к статическому полю x через неинициализированную локальную переменную Foo foo; foo.x я получаю ошибку компиляции Variable 'foo' might not have been initialized.

class Foo{
    public static int x = 1;

    public static void main(String[] args) {
        Foo foo;
        System.out.println(foo.x); // Error: Variable 'foo' might not have been initialized
    }
}

Может показаться, что эта ошибка имеет смысл, но только до тех пор, пока мы не поймем, что для доступа к члену static компилятор фактически не использует значение переменной, а только ее тип type.

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

Foo foo = null;
System.out.println(foo.x); //compiles and while running prints 1!!! 

Такой сценарий работает, потому что компилятор понимает, что x является статическим, и обрабатывает foo.x так, как он был написан как Foo.x (по крайней мере, я так думал до сих пор).

Так почему же компилятор вдруг настаивает на том, чтобы foo имел значение, которое он НЕ будет использовать в этом месте?

(Disclaimer: This is not code which would be used in real application, just interesting phenomenon which I couldn't find answer to on Qaru so I decided to ask about it.)

Ответы

Ответ 1

§15.11. Выражения доступа к полю:

Если поле статическое:

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

Там, где раньше говорится, что доступ к полю определяется Primary.Identifier.

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

EDIT:

Вот короткий пример, чтобы наглядно продемонстрировать, что Primary оценивается, даже если результат отбрасывается:

class Foo {
    public static int x = 1;

    public static Foo dummyFoo() throws InterruptedException {
        Thread.sleep(5000);
        return null;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println(dummyFoo().x);
        System.out.println(Foo.x);
    }
}

Здесь вы можете видеть, что dummyFoo() все еще оценивается, потому что print задерживается на 5 секунд Thread.sleep(), даже если он всегда возвращает значение null, которое отбрасывается.

Если выражение не было оценено, print появилось бы мгновенно, что можно увидеть, когда класс Foo используется напрямую для доступа к x с помощью Foo.x.

Примечание: вызов метода также считается Primary, показанным в §15.8 Первичные выражения.

Ответ 2

  Глава 16. Определенное задание

Каждая локальная переменная (§14.4) и каждое пустое конечное поле (§4.12.4, §8.3.1.2) должны иметь определенно назначенное значение при любом доступе к его значению.

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

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

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

Ответ 3

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

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

System.out.println(Foo.x);

Переменная 'foo' - это нежелательная служебная информация, которая должна быть удалена, и ошибки и предупреждения компилятора можно рассматривать как помощь, ведущую к этому.

Ответ 4

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

  • Каждый фрагмент кода либо имеет значение, либо вызывает ошибку компиляции.
  • (Для статики, поскольку экземпляр не нужен, Foo.x является естественным.)
  • Теперь, что нам делать с foo.x (доступ через переменную экземпляра)?
    • Это может быть ошибка компиляции, как в С# или
    • Это имеет значение. Поскольку Foo.x уже означает "простой доступ к x", разумно, чтобы выражение foo.x имело другое значение; то есть каждая часть выражения действительна и имеет доступ к x.

Пусть надежда знает кто-то знающий настоящую причину. :-)