Страницы

Поиск по вопросам

воскресенье, 8 декабря 2019 г.

Как работает finally в java?

#java #исключения


Есть код:

private static int f (){
    try {
        return 1;
    }
    finally {
        return 2;
    }
}

public static final void main(String[] args) {
    System.out.println(f());
}

OUTPUT: 2


Кажется самоочевидным, что после того, как компилятор наткнулся на return 1, он должен
закончить метод, и вывести единицу, ведь выполнение метода идёт построчно. Но компилятор
ведёт себя как-то странно, и вместо того чтобы вернуться с единицей он попадает в секцию
finally.

Вопрос: А как на самом деле "видит" код компилятор? Может быть он воспринимает блок
try как вызов из блока finally? Типа такого:

int finallyCompilator(){
int tryCompilator(); 
return 2; //Здесь код метода finally
}

tryCompilator(){
return 1;  //Здесь код метода try
}


Но даже если так, то непонятно, как "увидит" компилятор блок catch, если таковой будет? 
    


Ответы

Ответ 1



Всё намного проще, происходит подмена return блока Чтобы это увидеть на деле, скомпилируем ваш код в bytecode и откроем его например через Intellij IDEA и увидим следующую картину: public class Main { public Main() { } private static int f() { try { boolean var0 = true; return 2; } finally { ; } } public static final void main(String[] var0) { System.out.println(f()); } } Можем заметить, что при компиляции блок return из try был заменён на return из finally UPD: Давайте сделаем ситуацию интереснее и скомпилируем такую функцию private static int f (){ try { System.exit(0); return 1; } finally { System.out.println("smth"); return 2; } } Её bytecode будет таким: private static int f() { try { System.exit(0); boolean var0 = true; } finally { System.out.println("smth"); return 2; } } Тут мы видим, что return просто удалился из блока try, а System.exit(0) естественно выбросит из приложения быстрее, чем сработает блок finally.

Ответ 2



Краткий ответ: компилятор копирует блок finally перед каждым return и в каждый блок catch. Подробнее ниже. На вопрос почему finally должен вызываться после return ответ дается в вопросе: Выполняется ли finally если в try return? Что касается реализации, то в дополнение к исследованию @Komdosh я попробовал поискать требования к компиляции finally в спецификации виртуальной машины Java. В главе 3.13 описывается механизм компиляции finally с помощью инструкций JSR и RET. Инструкции и примеры реализации устарели, тем не менее из описания можно приблизительно понять стандартный механизм компиляции finally: The jsr instruction pushes the address of the following instruction (return at index 7) onto the operand stack before jumping. The astore_2 instruction that is the jump target stores the address on the operand stack into local variable 2. The code for the finally block (in this case the aload_0 and invokevirtual instructions) is run. Assuming execution of that code completes normally, the ret instruction retrieves the address from local variable 2 and resumes execution at that address. The return instruction is executed, and tryFinally returns normally. A try statement with a finally clause is compiled to have a special exception handler, one that can handle any exception thrown within the try statement. If tryItOut throws an exception, the exception table for tryFinally is searched for an appropriate exception handler. The special handler is found, causing execution to continue at index 8. The astore_1 instruction at index 8 stores the thrown value into local variable 1. The following jsr instruction does a subroutine call to the code for the finally block. Assuming that code returns normally, the aload_1 instruction at index 12 pushes the thrown value back onto the operand stack, and the following athrow instruction rethrows the value. Соответственно, интерпретация finally в блоке try-catch-finally должна быть эквивалентна следующей: весь блок оборачивается специальным обработчиком исключений, который сохраняет исключение, не обработанное catch (если таковые есть) в локальную переменную, выполняет прыжок/переход (JSR) к блоку finally, затем выбрасывает исключение; перед каждым return выполняется переход к блоку finally. Инструкция JSR использовалась ранее для экономии инструкций для блоков finally. Но class-файлы, начиная с версии 51 (Java 7) инструкцию не поддерживают. В документации JSR сказано, что инструкция использовалась до 6 версии для поддержки finally In Oracle's implementation of a compiler for the Java programming language prior to Java SE 6, the jsr instruction was used with the ret instruction in the implementation of the finally clause. С определенной версии компилятор Sun, затем Oracle, вместо использования переходов стал копировать и встраивать блоки finally из-за чего пропала необходимость в инструкции. Учитывая вышесказанное следующий блок кода: try { if(isSomething()) { return foo(); } return bar(); } catch(FooException e) { handle(e); } finally { /** блок finally **/ } , компилятор преобразовывает в блок эквивалентный следующему: try { if(isSomething()) { T temp1 = foo(); /* копия блока finally */ return temp1; } T temp2 = bar(); /* копия блока finally */ return temp2; } catch(FooException e) { handle(e); /* копия блока finally */ } catch(Throwable t) { /* копия блока finally */ throw t; } Проверка байткода, сгенерированного javac для Oracle JDK 8 подтверждает что блок finally копируется несколько раз (invokevirtual #10 — метод, который вызывается только в блоке finally, полный код): Code: 0: aload_0 1: invokevirtual #8 // Method isSomething:()Z 4: ifeq 17 7: aload_0 8: invokevirtual #9 // Method foo:()I 11: istore_1 12: aload_0 13: invokevirtual #10 // Копия finally 1 16: ireturn 17: aload_0 18: invokevirtual #11 // Method bar:()I 21: istore_1 22: aload_0 23: invokevirtual #10 // Копия finally 2 26: ireturn 27: astore_1 28: aload_0 29: aload_1 30: invokevirtual #13 // Method handle:(LMain$FooException;)V 33: aload_0 34: invokevirtual #10 // Копия finally 3 37: ireturn 38: astore_2 39: aload_0 40: invokevirtual #10 // Копия finally 4 43: ireturn В результате оптимизации компилятор может сократить часть инструкций, но подход кардинально измениться не должен. Стоит еще раз заметить, что я не нашел в спецификации строгих требований к байт-коду, который должен генерировать компилятор для finally. Насколько я понимаю, другая реализация компилятора могла бы использовать для данных целей инструкцию goto, хотя выигрыш оказался бы незначительным. Специальных конструкций для finally на уровне байт-кода в современных версиях Java нет. Релевантные ссылки : Компиляция Try/Catch/Finally для JVM на Хабрахабре. Why does the Java Compiler copy finally Blocks What Java compilers use the jsr instruction, and what for?

Ответ 3



Блок finally выполняется всегда. Исключение try { System.exit(0); } "Может быть он воспринимает блок try как вызов из блока finally"? Нет, просто finally выполняется после try, если не произошло исключения. Если произошло, то catch, потом finally.

Комментариев нет:

Отправить комментарий