Avatar ·

Блок finally выполняется всегда? Ловушки JVM и System.exit()

Всем привет! Я новичок в Java и пытаюсь разобраться с обработкой исключений. Меня смущает блок `finally`. В учебниках пишут, что он выполняется всегда, независимо от того, было исключение или нет. Но я нашел несколько примеров, где это, кажется, не так. Например, вот такой код: ```java public class FinallyTest { public static void main(String[] args) { try { System.out.println("В try"); System.exit(0); } catch (Exception e) { System.out.println("В catch"); } finally { System.out.println("В finally"); } } } ``` Я ожидал, что увижу "В finally", но программа просто завершилась без этого вывода. Почему? `System.exit(0)` — это же не исключение, это нормальное завершение. И еще один пример. Если в блоке `try` есть `return`, то `finally` все равно выполнится? Я слышал, что да, но хочу убедиться. И что будет, если в `finally` тоже бросить исключение? Оно перезапишет исключение из `try`? Объясните, пожалуйста, в каких случаях `finally` НЕ выполняется, а в каких выполняется всегда. Мне нужны четкие правила и примеры, чтобы не попасть в ловушку в реальном проекте. Спасибо! <<>>

Когда блок finally выполняется, а когда — нет

Блок finally в Java предназначен для гарантированного выполнения кода, который должен быть исполнен в любом случае: закрытие файлов, освобождение ресурсов, снятие блокировок. Однако есть три исключения, когда finally не выполняется:

  • Вызов System.exit(int) — немедленно завершает JVM.
  • Аварийное завершение JVM (например, SIGKILL, OutOfMemoryError в критической фазе).
  • Бесконечный цикл или зависание в блоке try/catch.

Во всех остальных случаях — да, finally выполняется. Давайте разберем каждый случай с примерами.

Пример 1: System.exit() — блок finally не выполняется

public class ExitExample {
    public static void main(String[] args) {
        try {
            System.out.println("Попытка...");
            System.exit(0); // JVM завершается здесь
        } finally {
            System.out.println("Это НЕ будет напечатано");
        }
    }
}

Вывод: только Попытка.... JVM убивается до того, как доходит до finally.

Пример 2: return в try — finally выполняется

public class ReturnExample {
    public static int test() {
        try {
            return 1;
        } finally {
            System.out.println("Finally выполнен!");
            // return 2; // если раскомментировать — переопределит return из try
        }
    }

    public static void main(String[] args) {
        System.out.println(test()); // Вывод: "Finally выполнен!" затем "1"
    }
}

Важно: finally выполняется до возврата значения. Если в finally тоже есть return, он перезапишет возвращаемое значение из try.

Пример 3: Исключение в finally перезаписывает исключение из try

public class ExceptionOverrideExample {
    public static void main(String[] args) {
        try {
            throw new RuntimeException("Ошибка в try");
        } finally {
            throw new IllegalArgumentException("Ошибка в finally");
        }
    }
}

Выбросится IllegalArgumentException. Исключение из try будет подавлено (suppressed). Это частая ошибка — терять оригинальное исключение.

Пример 4: Исключение в catch — finally все равно выполняется

public class CatchExceptionExample {
    public static void main(String[] args) {
        try {
            int x = 1 / 0;
        } catch (ArithmeticException e) {
            System.out.println("Поймано: " + e.getMessage());
            throw new RuntimeException("Новое исключение");
        } finally {
            System.out.println("Finally выполнен даже после throw в catch");
        }
    }
}

Вывод: сначала Поймано: / by zero, затем Finally выполнен..., затем программа упадет с RuntimeException.

Рекомендации по использованию

  • Используйте try-with-resources (Java 7+) для автоматического закрытия ресурсов — это чище, чем finally.
  • В блоке finally никогда не бросайте исключения и не используйте return — это ломает логику обработки ошибок.
  • Если нужно закрыть ресурс вручную, проверяйте на null и игнорируйте исключения закрытия:
InputStream is = null;
try {
    is = new FileInputStream("file.txt");
    // работа с is
} finally {
    if (is != null) {
        try {
            is.close();
        } catch (IOException ignored) {
            // логируем, но не бросаем
        }
    }
}

Итог

Блок finally выполняется во всех случаях, кроме:

  1. System.exit()
  2. Аварийное завершение JVM
  3. Бесконечный цикл/зависание
Помните, что finally выполняется даже после return и после throw, но будьте осторожны с побочными эффектами в нем.

<<>>

Дополнение: try-with-resources как современная альтернатива

Начиная с Java 7, для работы с ресурсами, реализующими AutoCloseable, рекомендуется использовать try-with-resources. Это избавляет от ручного управления finally и делает код более безопасным.

Пример с файлом:

try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
// Ресурс br автоматически закрыт, даже если было исключение

Преимущества:

  • Код короче и читаемее.
  • Исключения, возникшие при закрытии ресурса, не перезаписывают исключение из try — они добавляются как подавленные (suppressed).
  • Можно использовать несколько ресурсов через точку с запятой:
try (InputStream is = new FileInputStream("a.txt");
     OutputStream os = new FileOutputStream("b.txt")) {
    // копирование
}

Однако помните: try-with-resources не заменяет finally для кода, который не связан с ресурсами (например, снятие блокировки, сброс счетчика). В таких случаях старый добрый finally все еще актуален.