Почему изменение возвращаемой переменной в блоке 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блоке, а контролируется вывод на печать?


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), изменение ссылки не должно ничего делать для возвращаемого значения в стеке.

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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.