Блок finally в Java: мифы, реальность и подводные камни
Каждый Java-разработчик, от новичка до сеньора, рано или поздно сталкивается с блоком finally. В учебниках пишут: «Этот блок выполняется всегда, независимо от того, возникло исключение или нет». Звучит как железобетонная гарантия, правда? Но давайте заглянем за кулисы.
На StackOverflow вопрос «Does a finally block always get executed in Java?» собрал тысячи просмотров и сотни комментариев. Опытные программисты спорят, новички теряются, а реальные кейсы показывают: «всегда» — это не абсолютная истина. В этой статье мы разберем все сценарии, напишем реальный код и выясним, когда finally может вас подвести.
Как работает finally? Базовый пример
Для начала — классика. Блок finally используется для освобождения ресурсов: закрытия файлов, соединений с БД, сокетов. Он выполняется после try (и catch, если было исключение), но до того, как управление покинет метод.
public class FinallyDemo {
public static void main(String[] args) {
try {
System.out.println("Внутри try");
int result = 10 / 2; // Всё хорошо
} catch (ArithmeticException e) {
System.out.println("Поймано исключение: " + e.getMessage());
} finally {
System.out.println("Блок finally выполнен!");
}
System.out.println("Программа продолжается...");
}
}
Вывод:
Внутри try
Блок finally выполнен!
Программа продолжается...
Всё предсказуемо. Но что произойдёт, если в try возникнет исключение, которое не будет поймано?
public class FinallyWithUncaught {
public static void main(String[] args) {
try {
System.out.println("Попытка деления на ноль...");
int result = 10 / 0; // Исключение!
} finally {
System.out.println("Finally выполняется ДО того, как исключение улетит наверх");
}
// Эта строка уже не выполнится
System.out.println("Эта строка не будет напечатана");
}
}
Вывод удивит новичков:
Попытка деления на ноль...
Finally выполняется ДО того, как исключение улетит наверх
Exception in thread "main" java.lang.ArithmeticException: / by zero
at FinallyWithUncaught.main(FinallyWithUncaught.java:5)
Блок finally выполнился, хотя исключение не было обработано. Именно это поведение и породило миф о «всегда». Но давайте копнём глубже.
Когда finally НЕ выполняется? Реальные сценарии
Есть как минимум три ситуации, когда finally может не выполниться. Запомните их — это спасёт вам часы отладки.
1. System.exit() — экстренное завершение JVM
Самый известный случай. Если в блоке try вызвать System.exit(0), JVM завершится мгновенно, и finally не выполнится.
public class FinallyAndExit {
public static void main(String[] args) {
try {
System.out.println("Начинаем работу...");
System.exit(0); // JVM завершается здесь
} finally {
// Этот код НЕ выполнится!
System.out.println("Вы думали, я выполнюсь? А вот и нет!");
}
}
}
Вывод:
Начинаем работу...
Почему так? Потому что System.exit() останавливает виртуальную машину на уровне ОС. Это не исключение — это приказ «умереть». Блок finally просто не успевает сработать.
System.exit() в коде, перестаньте. Это дурной тон. Для завершения программы используйте возврат из main() или выбрасывание исключения.
2. Бесконечный цикл или зависание потока
Если выполнение программы застревает в блоке try (например, из-за бесконечного цикла или ожидания блокировки), до finally дело никогда не дойдёт.
public class FinallyAndInfiniteLoop {
public static void main(String[] args) {
try {
System.out.println("Входим в бесконечный цикл...");
while (true) {
// Делаем что-то вечно
}
} finally {
System.out.println("Сюда мы никогда не попадём");
}
}
}
Тривиально? Да. Но на практике такое случается, когда поток зависает из-за deadlock'а или бесконечной рекурсии.
3. Аварийное завершение JVM (OutOfMemoryError, StackOverflowError)
Некоторые критические ошибки (Error, а не Exception) могут привести к тому, что JVM просто не успеет выполнить finally. Например, OutOfMemoryError может убить поток до того, как блок finally будет вызван.
public class FinallyAndOOM {
public static void main(String[] args) {
try {
System.out.println("Пытаемся выделить гигантский массив...");
int[] hugeArray = new int[Integer.MAX_VALUE]; // OutOfMemoryError
} finally {
// Может выполниться, а может и нет — зависит от реализации JVM
System.out.println("Finally? Возможно, нет...");
}
}
}
Обратите внимание: поведение не гарантировано. В некоторых реализациях JVM блок finally выполнится, в других — нет. Никогда не полагайтесь на finally для спасения от OutOfMemoryError.
Спорные моменты: return в finally и потеря исключений
А теперь — вишенка на торте. Блок finally может не только не выполниться, но и испортить логику программы, если в нём есть return.
Что будет, если в finally написать return?
public class FinallyReturn {
public static void main(String[] args) {
System.out.println("Результат: " + getValue());
}
static int getValue() {
try {
return 42; // Кажется, вернёт 42
} finally {
return 100; // Но finally переопределяет возвращаемое значение!
}
}
}
Вывод:
Результат: 100
Да, именно так. finally выполняется после return из try, и если в finally тоже есть return, он перезаписывает предыдущее возвращаемое значение. Более того, исключение, возникшее в try, может быть «проглочено» блоком finally.
static int getValueWithException() {
try {
throw new RuntimeException("Ошибка в try");
} finally {
return 200; // Исключение теряется!
}
}
Вызов getValueWithException() вернёт 200 без