Может ли конечная переменная быть переназначена в catch, даже если назначение является последней операцией в try?


Я вполне убежден, что здесь

final int i;
try { i = calculateIndex(); }
catch (Exception e) { i = 1; }

i не может быть уже назначен, если элемент управления достигает catch-block. Однако компилятор Java не согласен и утверждает the final local variable i may already have been assigned.

есть ли еще какая-то тонкость, которую я здесь упускаю, или это просто слабость модели, используемой спецификацией языка Java для определения потенциальных переназначений? Мое главное беспокойство-это такие вещи, как Thread.stop(), что может привести к исключению выбрасывается " из тонкого воздух", но я все еще не вижу, как он может быть брошен после задания, которое, по-видимому, является самым последним действием в блоке try.

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

final int i = calculateIndex().getOrElse(1);

Я думаю, что этот случай использования служит довольно хорошей мотивацией, чтобы разрешить этот особый случай, когда i is определенно не назначен в catch-блок.

обновление

после некоторых размышлений я еще более уверен, что это просто слабость модели JLS: если я объявляю аксиому "в представленном примере,i определенно не назначается, когда контроль достигает catch-block", он не будет конфликтовать с любой другой аксиомой или теоремой. Компилятор не допустит никакого чтения i прежде чем он будет назначен в catch-блоке, так что факт ли i был назначен или нет, не может быть соблюден.

12 55

12 ответов:

JLS охота:

это ошибка времени компиляции, если конечная переменная назначена, если она определенно не назначена (§16) непосредственно перед назначением.

Quoth Глава 16:

V определенно не назначается перед блоком catch, если выполняются все следующие условия:

V определенно не назначен после блока try.
V является определенно неприсвоенным перед каждым оператор return, который принадлежит блоку try.
V определенно не назначается после e в каждом операторе формы throw e, который принадлежит блоку try.
V определенно не назначается после каждого оператора assert, который происходит в блоке try.
V определенно не назначается перед каждым оператором break, который принадлежит блоку try и чья цель break содержит (или является) оператором try.
V определенно не назначается перед каждым утверждением continue, что принадлежит блоку try и цель continue которого содержит оператор try.

жирным-мое. После try блокировать непонятно ли i назначена.

кроме того, в Примере

final int i;
try {
    i = foo();
    bar();
}
catch(Exception e) { // e might come from bar
    i = 1;
}

полужирный текст только условие, предотвращающее фактическое ошибочное назначение i=1 от того, чтобы быть незаконным. Таким образом, этого достаточно, чтобы доказать, что более тонкое условие "определенно неназначенного" необходимо, чтобы позволить код в исходном сообщении.

если спецификация была пересмотрена, чтобы заменить это условие на

V является определенно неприсвоенным после блока try, если блок catch ловит исключение непроверенное.
V определенно не назначается до последнего оператора, способного вызывать исключение типа, пойманного блоком catch, если блок catch ловит непроверенное исключение.

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

Я представил JSR для этого, который я ожидаю быть проигнорированным, но мне было любопытно посмотреть, как они обрабатываются. Технически номер факса является обязательным полем, я надеюсь, что это не слишком много повреждений, если я ввел +1-000-000-000 там.

Я думаю, что JVM, к сожалению, правильно. Хотя интуитивно правильно смотреть на код, это имеет смысл в контексте просмотра IL. Я создал простой метод run (), который в основном имитирует ваш случай (упрощенные комментарии здесь):

0: aload_0
1: invokevirtual  #5; // calculateIndex
4: istore_1
5: goto  17
// here's the catch block
17: // is after the catch

Итак, хотя вы не можете легко написать код для проверки этого, потому что он не будет компилироваться, вызов метода, сохранение значения и переход к После catch-это три отдельные операции. Ты может (однако маловероятно это может быть) есть исключение (поток.прерывание () кажется лучшим примером) между шагом 4 и шагом 5. Это приведет к входу в блок catch после Я был установлен.

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

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

final int i;
int temp;
try { temp = calculateIndex(); }
catch (IOException e) { temp = 1; }
i = temp;

Это резюме самых сильных аргументов в пользу тезиса о том, что нынешние правила для определенного назначения не могут быть ослаблены без нарушения последовательности (а), за которым следуют мои контраргументы (Б):

  • A: на уровне байт-кода запись в переменную не является последней инструкцией в блоке try: например, последняя инструкция обычно будет goto перейти через обработку исключений код;

  • B: но если правила гласят, что i и определенно не назначен в блоке catch его значение может не наблюдаться. Ненаблюдаемого значения так же хорошо, как никакого значения;

  • A: даже если компилятор заявляет i как определенно не назначен инструмент отладки все еще мог видеть ценность;

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

Окончательный Вывод:

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

вы правы, что если назначение является самой последней операцией в блоке try, мы знаем, что при входе в блок catch переменная не будет назначена. Однако формализация понятия "самая последняя операция" добавила бы к спецификации значительную сложность. Рассмотрим:

try {
    foo = bar();
    if (foo) {
        i = 4;
    } else {
        i = 7;
    }
}

будет ли эта функция полезна? Я так не думаю, потому что конечная переменная должна быть назначена ровно когда-то, а не максимум раз. В вашем случае переменная будет неназначена, если Error бросается. Вы можете не беспокоиться об этом, если переменная все равно выходит из области видимости, но это не всегда так (может быть еще один блок catch, ловящий Error, в том же или окружающем заявлении try). Например, рассмотрим:

final int i;
try {
    try {
        i = foo();
    } catch (Exception e) {
        bar();
        i = 1;
    }
} catch (Throwable t) {
    i = 0;
}

это правильно, но не было бы, если бы вызов bar() произошел после назначения i( например, в предложении finally), или мы используем оператор try-with-resources с ресурсом, чье закрытие метод создает исключение.

Бухгалтерский учет для того, что бы добавить еще больше сложности в спецификации.

наконец, есть простая работа вокруг:

final int i = calculateIndex();

и

int calculateIndex() {
    try {
        // calculate it
        return calculatedIndex;
    } catch (Exception e) {
        return 0;
    }
}

это делает очевидным, что я назначен.

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

1   final int i;
2   try { i = calculateIndex(); }
3   catch (Exception e) { 
4       i = 1; 
5   }

OP уже отмечает, что в строке 4 я, возможно, уже был назначен. Например, посредством резьбы.stop (), который является асинхронным исключением, см. http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5

теперь установите точку останова в строке 4, и вы можете наблюдать состояние переменной i до 1-это assignd. Таким образом, ослабление наблюдаемого поведения будет идти против Инструмент Виртуальной Машины Java™ Интерфейс

просматривая javadoc, кажется, нет подкласса Exception может быть выдано только после того, как я назначен. С теоретической точки зрения JLS, кажется Error может быть брошен сразу после назначения i (например VirtualMachineError).

кажется, нет требования JLS для компилятора, чтобы определить, могу ли я быть ранее установлен, когда достигается блок catch, различая, ловите ли вы Exception или Error/Throwable, подразумевая, что это слабость модели JLS.

почему бы и нет попробуйте следующее? (скомпилировали и протестировали)

(целочисленный тип оболочки + наконец + оператор "Элвис", чтобы проверить, является ли null):

import myUtils.ExpressionUtil;
....
Integer i0 = null; 
final int i;
try { i0 = calculateIndex(); }   // method may return int - autoboxed to Integer!
catch (Exception e) {} 
finally { i = nvl(i0,1); }       


package myUtils;
class ExpressionUtil {
    // Custom-made, because shorthand Elvis operator left out of Java 7
    Integer nvl(Integer i0, Integer i1) { return (i0 == null) ? i1 : i0;}
}

Я думаю, что есть одна ситуация, когда эта модель действовать как спасательный круг. Рассмотрим код, приведенный ниже:

final Integer i;
try
{
    i = new Integer(10);----->(1)
}catch(Exception ex)
{
    i = new Integer(20);
}

Теперь рассмотрим строку (1). Большинство компиляторов JIT создает объект в следующей последовательности (код psuedo):

mem = allocate();   //Allocate memory 
ctorInteger(instance);//Invoke constructor for Singleton passing instance.
i = mem;        //Make instance i non-null

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

mem = allocate();   //Allocate memory 
i = mem;        //Make instance i non-null
ctorInteger(instance);  //Invoke constructor for Singleton passing instance.

теперь предположим,JIT выполняет out of order writes при создании объекта в линию (1). И предположим, что при выполнении конструктора возникает исключение. В таком случае,catch блок будет иметь i что это not null . Если JVM не следует этому модальному, то в этом случае конечная переменная может быть назначена дважды!!!

но i может быть задан

    int i;
    try {
        i = calculateIndex();  // suppose func returns true
        System.out.println("i=" + i);
        throw new IOException();
    } catch (IOException e) {
        i = 1;
        System.out.println("i=" + i);
    }

выход

i=0
i=1

и это означает, что он не может быть окончательным

РЕДАКТИРОВАНИЕ ОТВЕТА НА ОСНОВЕ ВОПРОСОВ ИЗ OP

Это действительно в ответ на комментарий:

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

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

приближаясь к этому с противоположного направления, от" дизайна " вниз, я вижу проблемы. Я думаю, что именно М. Фаулер собрал различные " плохие запахи "в Книгу:"рефакторинг: улучшение дизайна существующего кода". Здесь (и, вероятно, во многих, многих других местах) Описан рефакторинг "метод извлечения".

public void someMethod() {
    final int i;
    try {
        int intermediateVal = 35;
        intermediateVal += 56;
        i = intermediateVal*3;
    } catch (Exception e) {
        // would like to be able to set i = 1 here;
    }
}

Итак, выше мог быть переработан, так как изначально опубликовано с calculateIndex метод. Однако, если рефакторинг "метод извлечения", определенный Фаулером, полностью применен, то он получает это [примечание: удаление "e" намеренно отличается от вашего метод.]

public void someMethod() {
    final int i =  calculateIndx();
}

private int calculateIndx() {
    try {
        int intermediateVal = 35;
        intermediateVal += 56;
        return intermediateVal*3;
    } catch (Exception e) {
        return 1;  // or other default values or other way of setting
    }
}

Итак, с точки зрения "дизайна" проблема заключается в коде, который у вас есть. Ваш метод "calculateIndex" не вычисляет индекс. Это только делает иногда. В остальное время вычисление выполняет обработчик исключений.

кроме того, этот рефакторинг гораздо более приспособлен к изменениям. Например, если вам нужно изменить то, что я предполагал, было значением по умолчанию " 1 " на "2", ничего страшного. Однако, как указывает ОП ответ в кавычках, нельзя предположить, что есть только одно значение по умолчанию. Если логика для установки этого становится только немного сложной, она все равно может легко находиться в инкапсулированном обработчике исключений. Однако, в какой-то момент, он тоже может быть преобразован в свой собственный метод. Оба случая все еще позволяют инкапсулированному методу выполнять свою функцию и действительно вычислять индекс.

в итоге, когда я прихожу сюда и смотрю на то, что я считаю правильным кодом, то нет вопрос компилятора для обсуждения. (Я совершенно уверен, что вы не согласитесь: это прекрасно, я просто хочу быть более ясным о моей точке зрения.) Что касается предупреждений компилятора, которые появляются для неправильного кода, они помогают мне понять в первую очередь, что что-то не так. В этом случае необходим рефакторинг.

согласно спецификациям JLS hunting, выполненным "djechlin", спецификации сообщают, когда переменная определенно не назначена. Поэтому spec говорит, что в этих сценариях безопасно разрешить назначение.Могут быть сценарии, отличные от упомянутых в спецификациях, в которых переменная все еще может быть неназначена, и это будет зависеть от компилятора, чтобы принять это разумное решение, если оно может обнаружить и разрешить назначение.

Spec никоим образом не упоминает в сценарии, указанном вами, что компилятор должен отметьте ошибку. Таким образом, это зависит от реализации компилятора спецификации, если он достаточно умен, чтобы обнаружить такие сценарии.

ссылки: Спецификация Языка Java Определенное Задание раздел "16.2.15 операторы try"

я столкнулся с точно такой же проблемой Марио, и прочитал это очень интереснейших дискуссий. Я просто решил свою проблему тем, что:

private final int i;

public Byte(String hex) {
    int calc;
    try {
        calc = Integer.parseInt(hex, 16);
    } catch (NumberFormatException e) {
        calc = 0;
    }
    finally {
      i = calc;
    }
}

@Joeg, я должен признать, что мне очень понравился ваш пост о дизайне, особенно это предложение: calculateIndx () иногда вычисляет индекс, но можем ли мы сказать то же самое о parseInt() ? Разве это не роль calculateIndex (), чтобы бросить и, таким образом, не вычислить индекс, когда это невозможно, а затем сделать это возврат неправильного значения (1 является произвольным в вашем рефакторинге) - это плохо imho.

@Марко, я не понял ваш ответ на Joeg о после строки 4 и перед строкой 5... Я еще недостаточно силен в мире java (25y c++, но только 1 в java...), но мне главное в этом случае, когда компилятор прав : я мог бы быть инициализирован дважды в случае Joeg по.

[все, что я говорю-это очень-очень скромному мнению]