Почему изменение возвращаемой переменной в блоке finally не меняет возвращаемого значения?

146

У меня есть простой класс Java, как показано ниже:

public class Test {

    private String s;

    public String foo() {
        try {
            s = "dev";
            return s;
        } 
        finally {
            s = "override variable s";
            System.out.println("Entry in finally Block");  
        }
    }

    public static void main(String[] xyz) {
        Test obj = new Test();
        System.out.println(obj.foo());
    }
}

И вывод этого кода такой:

Entry in finally Block
dev  

Почему sне переопределяется в finallyблоке, а контролируется вывод на печать?

Dev
источник
11
Вы должны поместить оператор return в блок finally, повторите со мной, блок finally всегда выполняется
Хуан Антонио Гомес Мориано
1
в блоке try он возвращает s
Devendra
6
Порядок заявлений имеет значение. Вы возвращаетесь, sпрежде чем изменить его значение.
Питер Лори
6
Но вы можете вернуть новое значение в finallyблоке , в отличие от C # (что вы не можете)
Элвин Вонг

Ответы:

167

В tryблоке завершается с исполнением returnзаявления и стоимостью sна момент returnоператор выполняет это значение , возвращаемое методом. Тот факт, что finallyпредложение позже изменяет значение s(после завершения returnоператора), не изменяет (в этот момент) возвращаемое значение.

Обратите внимание, что вышеизложенное касается изменений самого значения sв finallyблоке, а не объекта, на который sссылается. Если бы sбыла ссылка на изменяемый объект (а Stringэто не так) и содержимое объекта было изменено в finallyблоке, то эти изменения будут видны в возвращаемом значении.

Подробные правила того, как все это работает, можно найти в разделе 14.20.2 спецификации языка Java. . Обратите внимание, что выполнение returnоператора считается внезапным завершением tryблока (применяется раздел, начинающийся с « Если выполнение блока try завершается внезапно по любой другой причине R .... »). См. Раздел 14.17 JLS, чтобы узнать, почему returnоператор является внезапным завершением блока.

Более подробно: если как tryблок, так и finallyблок try-finallyоператора внезапно завершаются из-за returnоператоров, то применяются следующие правила из §14.20.2:

Если выполнение tryблока завершается преждевременно по какой-либо другой причине R [кроме выдачи исключения], тогда выполняется finallyблок, и тогда есть выбор:

  • Если finallyблок завершается нормально, то tryоператор завершается преждевременно по причине R.
  • Если finallyблок завершается внезапно по причине S, тоtry оператор завершается преждевременно по причине S (и причина R отбрасывается).

В результате returnоператор в finallyблоке определяет возвращаемое значение всего try-finallyоператора, а возвращаемое значение из tryблока отбрасывается. Подобное происходит в try-catch-finallyоператоре, если tryблок выдает исключение, он перехватывается catchблоком, и у catchблока и у finallyблока есть returnоператоры.

Тед Хопп
источник
Если я использую класс StringBuilder вместо String, чем добавить некоторое значение в блок finally, он изменит возвращаемое значение. Почему?
Девендра
6
@dev - я обсуждаю это во втором параграфе моего ответа. В ситуации, которую вы описываете, finallyблок не меняет, какой объект возвращается ( StringBuilder), но он может изменить внутреннюю часть объекта. В finallyблоке выполняется перед методом фактически возвращает (даже если returnоператор закончил), так что эти изменения происходят до того , как код вызова видит возвращаемое значение.
Тед Хопп
попробуйте с List, вы получите такое же поведение, как StringBuilder.
Йогеш Праджапати
1
@yogeshprajapati - Да. То же самое относится и к любым изменяемым значением возврата ( StringBuilder, List, Set, объявление до тошноты): если вы меняете содержимое в finallyблоке, то эти изменения проявляются в вызывающем коде , когда метод , наконец , завершает свою работу.
Тед Хопп
Это говорит о том , что происходит, но не говорит почему (было бы особенно полезно, если бы указывало, где это было описано в JLS).
TJ Crowder
65

Потому что возвращаемое значение помещается в стек перед вызовом finally.

Тордек
источник
3
Это правда, но это не отвечает на вопрос ОП, почему возвращаемая строка не изменилась. Это связано с неизменяемостью строк и ссылками на объекты, а не с возвратом возвращаемого значения в стек.
templatetypedef
Обе проблемы связаны.
Тордек
2
@templatetypedef, даже если бы String были изменяемыми, использование =не изменило бы его.
Оуэн
1
@Saul - точка зрения Оуэна (что правильно) заключалась в том, что неизменность не имеет ничего общего с тем, почему finallyблок OP не влияет на возвращаемое значение. Я думаю, что то, к чему может привести templatetypedef (хотя это не ясно), это то, что возвращаемое значение является ссылкой на неизменяемый объект, даже изменяя код в finallyблоке (кроме использования другогоreturn оператора) не может повлиять на значение, возвращаемое из метода.
Тед Хопп,
1
@templatetypedef Нет, это не так. Ничего общего с неизменяемостью строк. То же самое случилось бы с любым другим типом. Это связано с вычислением выражения возврата перед входом в блок finally, другими словами, exacfly «помещает возвращаемое значение в стек».
маркиз Лорн
32

Если мы заглянем внутрь байт-кода, то заметим, что JDK провел значительную оптимизацию, а метод foo () выглядит следующим образом:

String tmp = null;
try {
    s = "dev"
    tmp = s;
    s = "override variable s";
    return tmp;
} catch (RuntimeException e){
    s = "override variable s";
    throw e;
}

И байт-код:

0:  ldc #7;         //loading String "dev"
2:  putstatic   #8; //storing it to a static variable
5:  getstatic   #8; //loading "dev" from a static variable
8:  astore_0        //storing "dev" to a temp variable
9:  ldc #9;         //loading String "override variable s"
11: putstatic   #8; //setting a static variable
14: aload_0         //loading a temp avariable
15: areturn         //returning it
16: astore_1
17: ldc #9;         //loading String "override variable s"
19: putstatic   #8; //setting a static variable
22: aload_1
23: athrow

java сохранил строку «dev» от изменения до возвращения. На самом деле здесь нет окончательного блока вообще.

Михаил
источник
Это не оптимизация. Это просто реализация требуемой семантики.
Маркиз Лорн
22

Здесь следует отметить 2 вещи:

  • Строки неизменны. Когда вы устанавливаете s для «переопределить переменную s», вы устанавливаете s для обращения к встроенной строке, не изменяя встроенный буфер символов объекта s, чтобы изменить его на «переопределить переменную s».
  • Вы помещаете ссылку на s в стек, чтобы вернуться к вызывающему коду. После этого (когда выполняется блок finally), изменение ссылки не должно ничего делать для возвращаемого значения в стеке.
0xCAFEBABE
источник
1
так что если я возьму stringbuffer, то жало будет переопределено ??
Девендра
10
@dev - Если вы измените содержимое буфера в finallyпредложении, это будет видно в вызывающем коде. Однако, если вы назначите новый буфер строк s, то поведение будет таким же, как и сейчас.
Тед Хопп
да, если я добавляю в строковый буфер в конце концов, изменения блока отражаются на выходе.
Девендра
@ 0xCAFEBABE Вы также даете очень хороший ответ и концепции, я очень благодарен вам.
Девендра
13

Я немного изменил твой код, чтобы доказать смысл Теда.

Как видите, выход sдействительно изменился, но после возврата.

public class Test {

public String s;

public String foo() {

    try {
        s = "dev";
        return s;
    } finally {
        s = "override variable s";
        System.out.println("Entry in finally Block");

    }
}

public static void main(String[] xyz) {
    Test obj = new Test();
    System.out.println(obj.foo());
    System.out.println(obj.s);
}
}

Вывод:

Entry in finally Block 
dev 
override variable s
Фрэнк
источник
почему переопределенная строка не возвращается.
Девендра
2
Как уже сказали Тед и Тордек, «возвращаемое значение помещается в стек до того, как будет выполнено окончание»
Франк
1
Хотя это хорошая дополнительная информация, я не желаю повышать ее, поскольку она (сама по себе) не отвечает на вопрос.
Иоахим Зауэр
5

Технически говоря, returnблок try не будет игнорироваться, если finallyблок определен, только если этот блок finally также включает в себя return.

Это сомнительное дизайнерское решение, которое, вероятно, было ошибкой в ​​ретроспективе (очень похоже на то, что ссылки по умолчанию могут быть обнуляемыми / изменяемыми и, по некоторым данным, проверенными исключениями). Во многих отношениях это поведение в точности соответствует разговорному пониманию того, что finallyозначает - «независимо от того, что происходит заранее в tryблоке, всегда выполняйте этот код». Следовательно, если вы возвращаете true из finallyблока, общий эффект всегда должен бытьreturn s , нет?

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

Киран Жужаре
источник
Сомнительно почему? Это соответствует порядку оценки во всех других контекстах.
маркиз Лорн
0

Попробуйте это: если вы хотите напечатать значение переопределения s.

finally {
    s = "override variable s";    
    System.out.println("Entry in finally Block");
    return s;
}
Ачинтя Джа
источник
1
он дает предупреждение .. наконец, блок не завершается нормально
Devendra