Всегда ли выполняется finally в Java? Полный разбор с кодом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Блок 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 без

Блоги

Рекомендаций по книгам