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

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

Для следующего кода:

public class StaticFinal
{
    private final static int i ;
    public StaticFinal()
    {}
}

Я получаю ошибку времени компиляции:

StaticFinal.java:7: variable i might not have been initialized
        {}
         ^
1 error

Что соответствует JLS8.3.1.2, в котором говорится, что:

Это ошибка времени компиляции, если пустая конечная переменная класса (§4.12.4) определенно не назначена (§16.8) статическим инициализатором (§8.7) класса, в котором она объявлена.

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

public class StaticFinal
{
    private final static int i ;
    public StaticFinal()throws InstantiationException
    {
        throw new InstantiationException("Can't instantiate"); // Don't let the constructor to complete.
    }
}

Здесь конструктор никогда не завершается, потому что InstantiationException выбрасывается в середине конструктора. И этот код компилируется отлично!
Почему это? Почему этот код не показывает ошибку времени компиляции об инициализации переменной final i?


ИЗМЕНИТЬ
Я компилирую его с помощью javac 1.6.0_25 в командной строке (не используя какой-либо IDE)

4b9b3361

Ответ 1

Интересно, что код скомпилирует, будет ли поле помечено static - и в IntelliJ оно будет жаловаться (но компилировать) на статическое поле и не говорить слово с нестатическим полем.

Вы правы в том, что в JLS §8.1.3.2 есть определенные правила относительно [статических] окончательных полей. Тем не менее, есть несколько других правил вокруг финальных полей, которые здесь играют большую роль, исходя из спецификации языка Java §4.12.4 - которые определяют семантика компиляции поля final.

Но прежде чем мы сможем попасть в этот шар воска, нам нужно определить, что происходит, когда мы видим throws -, который дается нам §14.18, акцент мой:

Оператор throw вызывает исключение (§11). Результатом является немедленная передача управления (§11.3) , которая может выходить из нескольких операторов и нескольких конструкторов, инициализаторов экземпляра, статических инициализаторов и оценок инициализатора поля и вызовов методов, пока оператор try (§14.20) не будет обнаружил, что улавливает выброшенное значение. Если такой оператор try не найден, то выполнение потока (§17), выполняющего бросок, завершается (§11.3) после вызова метода uncaughtException для группы потоков, к которой принадлежит поток.

В условиях непрофессионала - во время выполнения, если мы сталкиваемся с оператором throws, он может прервать выполнение конструктора (формально, "завершается внезапно" ), в результате чего объект не будет создан или построен в неполное состояние. Это может быть дыра в безопасности, в зависимости от платформы и частичной полноты конструктора.

Что ожидает JVM, данное в §4.5, состоит в том, что поле с ACC_FINAL установлено никогда не имеет значения после построения объекта:

Объявлено окончательное; никогда не назначаемый непосредственно после построения объекта (JLS § 17.5).

Итак, у нас немного рассол - мы ожидаем поведения этого во время выполнения, но не во время компиляции. И почему IntelliJ поднимает мягкую суету, когда у меня есть static в этом поле, но не тогда, когда я этого не делаю?

Сначала вернемся к throws - там только ошибка времени компиляции с этим утверждением, если одна из этих трех частей не удовлетворена

  • Выбрасываемое выражение не отмечено или равно null,
  • Вы try до catch исключение, и вы catch введите его с правильным типом или
  • Выбрасываемое выражение - это то, что действительно может быть брошено, согласно §8.4.6 и §8.8.5.

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

Если оператор throw содержится в объявлении конструктора, но его значение не попадает в какой-либо оператор try, который его содержит, то выражение создания экземпляра класса, вызывающее конструктор, будет завершено внезапно из-за броска (§15.9.4).

Теперь, в это пустое поле final. Там любопытная пьеса для них - их назначение имеет значение только после завершения конструктора, подчеркивая их.

Чистая конечная переменная экземпляра должна быть определенно назначена (§16.9) в конце каждого конструктора (§8.8) класса, в котором она объявлена; в противном случае возникает ошибка времени компиляции.

Что делать, если мы никогда не дойдем до конца конструктора?


Первая программа: обычное создание поля static final, декомпилированное:

// class version 51.0 (51)
// access flags 0x21
public class com/stackoverflow/sandbox/DecompileThis {

    // compiled from: DecompileThis.java

    // access flags 0x1A
    private final static I i = 10

    // access flags 0x1
    public <init>()V
            L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
            L1
    LINENUMBER 9 L1
            RETURN // <- Pay close attention here.
    L2
    LOCALVARIABLE this Lcom/stackoverflow/sandbox/DecompileThis; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

Обратите внимание, что мы действительно вызываем инструкцию RETURN после успешного вызова нашего <init>. Имеет смысл и совершенно законна.

Вторая программа: выбрасывает конструктор и пустое поле static final, декомпилируется:

// class version 51.0 (51)
// access flags 0x21
public class com/stackoverflow/sandbox/DecompileThis {

  // compiled from: DecompileThis.java

  // access flags 0x1A
  private final static I i

  // access flags 0x1
  public <init>()V throws java/lang/InstantiationException 
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 8 L1
    NEW java/lang/InstantiationException
    DUP
    LDC "Nothin' doin'."
    INVOKESPECIAL java/lang/InstantiationException.<init> (Ljava/lang/String;)V
    ATHROW // <-- Eeek, where'd my RETURN instruction go?!
   L2
    LOCALVARIABLE this Lcom/stackoverflow/sandbox/DecompileThis; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1
}

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

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

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


Возвращаясь к этому, это имеет смысл в контексте, поскольку следующее объявление в Java является законным, а тела методов конгруэнтны к телам-конструкторам:

public boolean trueOrDie(int val) {
    if(val > 0) {
        return true;
    } else {
        throw new IllegalStateException("Non-natural number!?");
    }
}

Ответ 2

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

Исключая Eclipse, у которого есть какой-то инкрементный компилятор (и поэтому он может сразу обнаружить проблему), командная строка javac выполняет компиляцию с одним выстрелом. Теперь первый фрагмент

public class StaticFinal {
    private final static int i ;
}

который в основном тот же, что и пустой конструктор (как в первом примере), бросает ошибку времени компиляции, и это нормально, потому что соблюдает спецификации.

Во втором фрагменте я думаю, что в компиляторе есть ошибка; кажется, что компилятор принимает некоторые решения на основе того, что делает конструктор. Это более очевидно, если вы попытаетесь скомпилировать этот файл,

public class StaticFinal
{
    private final static int i ;

    public StaticFinal() 
    {
        throw new RuntimeException("Can't instantiate"); 
    }
}

Это более странно, чем ваш пример, потому что неконтролируемое исключение не объявляется в сигнатуре метода и будет (по крайней мере, это то, что я думал перед чтением этого сообщения), обнаруженное только во время выполнения.

Наблюдение за поведением, которое я мог бы сказать (но неправильно по спецификациям), что.

Для статических конечных переменных компилятор пытается увидеть, явно ли они инициализированы или инициализированы в статическом блоке intializer, но по какой-то странной причине он ищет что-то в конструкторе:

  • если они инициализированы в конструкторе, компилятор произведет ошибку (вы не можете назначить там значение для конечной статической переменной)
  • Если конструктор пуст, компилятор произведет ошибку (если вы скомпилируете первый пример, тот, у которого есть явный конструктор нулевого аргумента, компилятор разбивается, указывая на закрывающую скобку конструктора как строку ошибки).
  • если класс не может быть создан, потому что конструктор не завершил , потому что исключение выбрано (это неверно, например, если вы пишете System.exit(1), а не бросаете исключение...it не будет компилироваться!), тогда значение по умолчанию будет присвоено статической переменной (!)

Ответ 3

После добавления основного метода для печати кода i. Код печатает значение 0. Это означает, что java-компилятор автоматически инициализирует я со значением 0. Я написал его в IntelliJ и должен был отключить проверку кода, чтобы иметь возможность создавать код. В противном случае это не позволит мне дать мне ту же ошибку, что и вы, прежде чем выбросить исключение.

Код JAVA: не инициализирован

public class StaticFinal {
    private final static int i;
    public StaticFinal(){
        throw new InstantiationError("Can't instantiate!");
    }

    public static void main(String args[]) {
        System.out.print(i);
    }

}

декомпилированные

Идентичные

Код JAVA: Инициализировано

public class StaticFinal {
    private final static int i = 0;
    public StaticFinal(){
        throw new InstantiationError("Can't instantiate!");
    }

    public static void main(String args[]) {
        System.out.print(StaticFinal.i);
    }

}

декомпилированные

public class StaticFinal
{

    public StaticFinal()
    {
        throw new InstantiationError("Can't instantiate!");
    }

    public static void main(String args[])
    {
        System.out.print(0);
    }

    private static final int i = 0;
}

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

Обязательно скажите вам, что вы заметили это.

Похожие вопросы: Здесь

Ответ 4

Я бы сказал это просто потому, что когда вы добавляете Throws, вы в основном обрабатываете ошибку, поэтому компилятор идет "о, ну, наверное, он знает, что он делает". В конце концов, он все равно дает ошибку времени выполнения.