Может ли конечная переменная быть переназначена в 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
назначен или нет, не может быть соблюден.
Ответы
Ответ 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...), но я считаю, что этот случай является правильным, если компилятор прав: я мог бы дважды инициализироваться в случае Джоге.
[Все, что я говорю, очень скромное мнение]