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

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

Я совершенно убежден, что здесь

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

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

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

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

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

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

UPDATE

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

4b9b3361

Ответ 1

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

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

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

  • A: даже если компилятор объявляет i как определенно неназначенный, инструмент отладки все еще может видеть значение;

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

Окончательное заключение:

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

Ответ 2

JLS:

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

Четвертая глава 16:

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

V после блока try определенно не назначен.
V определенно не назначен перед каждым оператором return, который принадлежит блоку try.
V определенно не назначается после e в каждом выражении формы throw, которое принадлежит блоку try.
V определенно не назначается после каждого утверждения assert, который встречается в блоке try.
V определенно не назначается перед каждым оператором break, принадлежащим блоку try, и чью цель прерывания содержит (или является) оператор 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 поймает неконтролируемое исключение.

Тогда я верю, что ваш код будет законным. (К лучшему из моего ad-hoc-анализа.)

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

Ответ 3

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

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

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

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

Ответ 4

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

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

Ответ 5

Вы правы, что если назначение является самой последней операцией в блоке 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() произошел после назначения я (например, в предложении finally), или мы используем оператор try-with-resources с ресурсом, метод закрытия которого генерирует исключение.

Учет этого будет еще более сложным для спецификации.

Наконец, есть простая работа:

final int i = calculateIndex();

и

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

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

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

Ответ 6

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

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

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

Ответ 7

Но 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

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

Ответ 8

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

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

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

(Integer Wrapper Type + finally + оператор "Elvis" для проверки, является ли 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;}
}

Ответ 9

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

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 не следит за этим модалом, тогда в этом случае конечная переменная может быть назначена дважды!!!

Ответ 10

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

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

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

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

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

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

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;
    }
}

Теперь указанные выше COULD были реорганизованы, как первоначально было опубликовано с помощью метода 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", не имеет большого значения. Однако, как указано в ответе OP, нельзя предположить, что существует только одно значение по умолчанию. Если логика, чтобы установить это, становится только немного сложной, она все равно может легко находиться в инкапсулированном обработчике исключений. Тем не менее, в какой-то момент, его тоже, возможно, придется реорганизовать в свой собственный метод. Оба случая по-прежнему позволяют инкапсулированному методу выполнять его функцию и действительно вычислять индекс.

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

Ответ 11

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

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

Справка: Спецификация языка Java Определенное задание раздел "16.2.15 try Statement"

Ответ 12

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

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 произвольно в вашем рефакторинге), это плохое.

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

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