Чому зміна повернутої змінної в остаточному блоці не змінює значення повернення?


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
ви повинні поставити зворотну заяву на остаточний блок, повторити зі мною, нарешті блок завжди виконується
Хуан Антоніо Гомес Моріано

1
у спробу заблокувати повернення s
Девендра

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блоку (застосовується розділ, який починається " Якщо виконання блоку спробу завершено різко з будь-якої іншої причини R .... "). Дивіться розділ 14.17 JLS, чому returnзаява є різким припиненням блоку.

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

Якщо виконання tryблоку різко завершується з будь-якої іншої причини R [окрім викидання виключення], finallyблок виконується, і тоді є вибір:

  • Якщо finallyблок завершується нормально, то tryзаява різко завершується з причини Р.
  • Якщо finallyблок завершується різко через причину S, то tryвираз завершується різко з причини S (а причина R відкидається).

Результатом є те, що returnоператор у finallyблоці визначає повернене значення всього try-finallyоператора, а повернене значення з tryблоку відкидається. Подібна річ відбувається в try-catch-finallyоператорі, якщо tryблок кидає виняток, його спіймає catchблок, і в catchблоці, і в finallyблоці є returnзаяви.


Якщо я використовую клас StringBuilder замість String, ніж додаю якесь значення в остаточному блоці, його зміна повертається value.why?
Девендра

6
@dev - Я обговорюю це у другому абзаці своєї відповіді. У ситуації, яку ви описуєте, finallyблок не змінює те, що об'єкт повертається (the StringBuilder), але він може змінювати внутрішні об'єкти. У finallyблоці виконується перед методом фактично повертає (навіть якщо returnоператор закінчив), так що ці зміни відбуваються до того , як код виклику бачить повертається значення.
Тед Хопп

спробуйте зі списком, у вас така поведінка, як у StringBuilder.
Йогеш Праджапаті

1
@yogeshprajapati - Так. Те ж саме відноситься і до будь-яких змінним значенням повернення ( StringBuilder, List, Set, оголошення до нудоти): якщо ви міняєте вміст в finallyблоці, то ці зміни виявляються в зухвалій коді , коли метод , нарешті , завершує свою роботу.
Тед Хопп

Це говорить про те, що трапляється, і не говорить чому (було б особливо корисно, якби вказувалося на те, де це було висвітлено в JLS).
TJ Crowder

65

Тому що значення повернення ставиться на стек перед викликом нарешті.


3
Це правда, але це не стосується питання ОП щодо того, чому повернута рядок не змінилася. Це стосується незмінності рядків та посилань на об'єкти більше, ніж натискання на повернення значення повернення.
templatetypedef

Обидва питання пов'язані між собою.
Тордек

2
@templatetypedef, навіть якщо String було вимкнено, використання =не вимкне його.
Оуен

1
@Saul - Точка Оуена (що є правильним) полягала в тому, що незмінність не має нічого спільного з тим, чому finallyблок OP не впливав на значення повернення. Я думаю, що те, що шаблонtypedef, можливо, отримував (хоча це не зрозуміло), це те, що повернене значення є посиланням на незмінний об'єкт, навіть зміна коду в finallyблоці (крім використання іншого returnоператора) не може вплинути на значення, повернене з методу.
Тед Хопп

1
@templatetypedef Ні, це не так. Нічого не пов’язаного з незмінною струною. Те саме було б з будь-яким іншим типом. Це пов’язано з оцінкою виразного повернення перед тим, як увійти до остаточного блоку, інакше кажучи, екcффля "висуває значення повернення на стек".
Маркіз Лорн

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

Тут варто звернути увагу на дві речі:

  • Струни незмінні. Коли ви встановлюєте s на "переосмислити змінну s", ви задаєте s для посилання на вкладений String, не змінюючи притаманний буфер char об'єкта s, щоб змінити на "override змінної s".
  • Ви ставите посилання на s на стек, щоб повернутися до коду виклику. Після цього (коли остаточно запущений блок), зміна посилання не повинна нічого робити для поверненого значення, яке вже є у стеку.

1
тож якщо я візьму струнний буфер, ніж жало буде замінено ??
Девендра

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

чому замінений рядок s не повертається.
Девендра

2
Як Тед і Тордек вже говорили, "повернене значення ставиться на стек до того, як остаточно буде виконано"
Френк

1
Хоча це є додатковою додатковою інформацією, я не бажаю її оприлюднювати, оскільки вона не відповідає (сама по собі) на питання.
Йоахім Зауер

5

Технічно кажучи, returnблок спробу не буде ігноруватися, якщо finallyблок визначений, лише якщо цей остаточний блок також включає в себе a return.

Це сумнівне дизайнерське рішення, що, ймовірно, було помилкою в ретроспективі (подібно до того, що посилання за замовчуванням були нульовими / змінними, і, за деякими, перевіреними винятками). Багато в чому така поведінка точно відповідає розмовному розумінню того, що finallyозначає - "незалежно від того, що відбувається заздалегідь у tryблоці, завжди виконайте цей код". Отже, якщо ви повернете істину з finallyблоку, загальний ефект завжди повинен бути таким return s, ні?

Взагалі, це рідко є ідіомою, і ви повинні використовувати finallyблоки для очищення / закриття ресурсів, але рідко, якщо взагалі повертаєте значення з них.


Сумнівно чому? Це узгоджується з порядком оцінювання у всіх інших контекстах.
Маркіз Лорн

0

Спробуйте це: Якщо ви хочете роздрукувати значення заміщення s.

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

1
це попереджає .. нарешті блок не завершується нормально
Девендра
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.