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


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.