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

280
12 октября 2017, 09:48

Есть код:

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, если таковой будет?

Answer 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.

Answer 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?
Answer 3

Блок finally выполняется всегда. Исключение

try { System.exit(0); }

"Может быть он воспринимает блок try как вызов из блока finally"? Нет, просто finally выполняется после try, если не произошло исключения. Если произошло, то catch, потом finally.

READ ALSO
Как в IntelliJ IDEA открыть декомпилированный байт-код?

Как в IntelliJ IDEA открыть декомпилированный байт-код?

Оказывается, можно сделать такую удивительную магию как:

220
Проблема при проверки на число

Проблема при проверки на число

Создал консольный калькулятор, когда ввожу число ошибка NumberFormatException

252
Что такое статический полиформизм?

Что такое статический полиформизм?

Наткнулся на такой термин - "статический полиформизм"Не знал про такое сочетание

193