воскресенье, 23 мая 2010 г.

Java: finally + return всё стерпит

Конструкция try-catch-finally в Java. Finally выполняется всегда. Это знают все. А что если в finally стоит return, а в try или catch случился exception? 

Вот такой примерчик. Предположим у нас есть наш собственный checked my.ArithmeticException которым мы оборачиваем стандартный unchecked ArithmeticException по каким-то своим причинам. Как-то так:

  1. public int myDiv(int a, int b) throws my.ArithmeticException{
  2.     int result = 0;
  3.     try{
  4.          result = a / b;
  5.     }
  6.     catch (java.lang.ArithmeticException arex){
  7.          throw new my.ArithmeticException("Division by zero", arex);
  8.     }
  9.     finally{
  10.         return result;
  11.     }

Запускаем. Для входных параметров 12345 и 0 (деление на 0) наш exception не бросается а результат операции равен нулю.

Что же произошло? Почему отработал return а не throws?

Разберемся, как работает JVM при выходе из метода. Выход из метода - значение на стеке. И throws и return занимают её место, когда работа метода завершается. В нашем случае, в стек сначала попадает throws my.ArithmeticException, затем начинает работать finally который кладет на стек return и перетирает наш exception. Он теряется. Навсегда.

Как это может быть полезно? Да никак. Если уж очень надо чтобы метод всегда завершал работу штатно (например) , обычно пишут catch(Throwable) в котором хотя бы можно залогировать проблему перед тем как ее игнорировать. Finally + return затирает ВСЕ (да-да, и OutOfMemory тоже) exception'ы и вы никак об этом не узнаете.

Наверное единственный правильный вывод к которому я пришел - не писать return в finally.

6 комментариев:

  1. И что же произойдет если выбрасывание OutOfMemory затрется? - сегментейшен флод :-D?

    ОтветитьУдалить
  2. Врядли. Скорее всего OutOfMemory выбросится еще раз, при дальнейшем исполнении текущим потоком.

    ОтветитьУдалить
  3. Интересно в какой момент выбрасится повторно? При следующей попытки выделить память? А что если до этой попытки сборщик мусора уже освободил память?

    ОтветитьУдалить
  4. Насколько я знаю современные сборщики мусора (по крайней мере у HotSpot) помимо фоновых действий работают примерно так:
    Выделить память для нового объекта -> нет свободной памяти в new gen? -> вызвать GC -> памяти всё еще не хватает? -> вызвать FullGC -> памяти не хватает? -> OutOfMemory

    Т.е. отвечая на ваш вопрос - да. Если другой поток между первый OutOfMemory и выделением нового объекта например успеет освободить пару объектов - тогда OutOfMemory не выбросится.

    ОтветитьУдалить
  5. Спасибо за такое интерестное изложение проблемы. Но думаю многим читателям было бы интерестно увидеть собственно декомпиляцию байт кода этого примера=)

    ОтветитьУдалить
  6. У finally немного другое предназначение:
    Допустим мы открываем какое-то соединение (TCP, UDP, SQL Connection или поток вывода PrintWriter и т.п.) в finally соединение можно закрыть!

    PrintWriter out = null;
    try{
    out = response().getWriter();
    //какой то код выкидывающий исключение
    }catch(Exception e){}
    finally{
    if (out!= null)out.close();
    }

    В любом случае, поток вывода будет закрыт!

    ОтветитьУдалить