Блок finally в Java
Выполняйте код очистки в Java с помощью блоков finally, которые работают всегда — независимо от того, было ли выброшено исключение.
Блок finally выполняется независимо от того, как завершился try — нормально, после пойманного исключения, после непойманного исключения или даже после раннего return. Именно в этом заключается его смысл: здесь размещают код очистки, который должен выполняться в любом случае. Закрытие файлового дескриптора, освобождение блокировки, восстановление состояния потока — всё то, чьё отсутствие оставило бы программу в худшем состоянии, чем до начала работы.
Структура
try {
// risky code
} catch (SomeException e) {
// optional — zero or more catches
} finally {
// always runs after the try (and any matching catch)
}Блок finally можно комбинировать с catch, использовать без catch (try { ... } finally { ... }) или с несколькими catch. Части свободно сочетаются между собой.
Что значит «всегда выполняется»
Блок finally выполняется, когда управление покидает try, независимо от способа выхода:
- Нормальное завершение блока
try—finallyвыполняется после последнего оператора. - Исключение, выброшенное из
try—finallyвыполняется после завершения подходящегоcatch(или, если ни один catch не подходит, непосредственно перед распространением исключения). returnвнутриtryилиcatch—finallyвыполняется до того, как return фактически вступит в силу.breakилиcontinue, покидающиеtry—finallyвыполняется перед переходом.
Единственные способы пропустить finally: сама JVM завершает работу (System.exit, отключение питания, Runtime.halt), бесконечный цикл или взаимная блокировка внутри try, или Thread.stop (устаревший именно по этой причине). Для любого кода, написанного в обычном приложении, finally является железной гарантией.
try {
return computeAnswer(); // even though there's a return here,
} finally {
cleanup(); // this runs before the method actually returns
}Для чего нужен finally
По-честному: почти всегда — очистка ресурсов. До появления try-with-resources в Java 7 стандартный шаблон выглядел так:
InputStream in = null;
try {
in = new FileInputStream(path);
// read from in...
} catch (IOException e) {
// handle
} finally {
if (in != null) {
try { in.close(); } catch (IOException ignored) { /* */ }
}
}Вложенный try/catch вокруг close() в finally — именно тот шаблонный шум, который try-with-resources был создан устранить. Мы рассмотрим это в следующей главе. Но понимание того, для чего нужен finally, делает новую конструкцию понятной.
Помимо ресурсов, finally полезен для:
- Восстановления общего состояния, изменённого на время выполнения
try— инкремент счётчика глубины, переключение флага, заменаThreadLocal. - Освобождения вручную приобретённых блокировок (
Lock.lock()→try { ... } finally { lock.unlock(); }). - Остановки таймеров или закрытия транзакций, не реализующих
AutoCloseable.
Для чего finally не предназначен
Не пишите в finally логику, производящую результаты. Блок выполняется независимо от исхода — он не знает, успешно ли завершился try. Если вы поместите commit() в finally, коммит произойдёт даже при ошибке.
И не делайте return из finally. Это один из действительно опасных уголков языка:
try {
return 1;
} finally {
return 2; // wins — the method returns 2 and the original return is lost
}return (или throw) внутри finally переопределяет любой return или исключение из try. Исключение, которое должно было распространиться, молча отбрасывается. Именно поэтому большинство линтеров помечают это как ошибку. Правило: finally выполняет очистку; finally не производит значения.
Порядок выполнения
Когда присутствуют и catch, и finally:
try { ... }
catch { ... }
finally { ... }Порядок именно такой, какой вы ожидаете: выполняется try, если он выбрасывает исключение и catch подходит — выполняется catch, затем finally выполняется после того, что отработало. Если finally затем выбрасывает исключение, это новое исключение заменяет то, что распространялось из try или catch — ещё одна причина держать finally «тихим».
Рабочий пример
Мы оснащаем небольшую «транзакцию» блоками try/catch/finally и вызываем её тремя разными способами: нормальный успех, восстанавливаемый сбой и невосстанавливаемый. Блок finally выполняется во всех трёх случаях — в этом и состоит весь смысл.
В третьем вызове doWork выбрасывает RuntimeException, которое локальный catch не перехватывает. Блок finally всё равно выполняется и выводит "release lock" до того, как исключение продолжает раскручивать стек вверх до main. Именно это свойство вы хотите от кода очистки — оно не зависит от того, успешно ли прошла обработка.
Что дальше
Шаблон «открыть ресурс, поработать с ним, закрыть в finally» настолько распространён, что Java создала для него специальный оператор. Продолжайте изучение: Java try-with-resources.