try-with-resources и finally

Разбираем классический вопрос-ловушку про блок finally и современный способ Java закрывать ресурсы без утечек.

try-with-resources — конструкция Java, которая автоматически закрывает ресурсы (файлы, соединения с базой, сетевые сокеты) после выполнения блока кода, даже если внутри произошло исключение.

Что выведет этот код?

Классика собеседований — вопрос про порядок вывода при исключении внутри try/catch/finally:

public class Main {
    public static void main(String[] args) {
        try {
            System.out.println("1: пробуем");
            int result = 10 / 0;
            System.out.println("2: этой строки не будет");
        } catch (ArithmeticException e) {
            System.out.println("3: поймали деление на ноль");
        } finally {
            System.out.println("4: finally выполняется всегда");
        }
        System.out.println("5: программа продолжается");
    }
}

Вывод:

1: пробуем
3: поймали деление на ноль
4: finally выполняется всегда
5: программа продолжается

Строка «2» не выводится — как только на строке 10 / 0 выбрасывается ArithmeticException, выполнение сразу прыгает в блок catch, а всё, что осталось в try после места сбоя, пропускается. После catch обязательно отрабатывает finally — и только потом код идёт дальше.

Главный секрет finally

Блок finally выполняется практически всегда — независимо от того, было исключение или нет, поймали его или нет, и даже если внутри try или catch стоит return. Именно поэтому его используют для «уборки»: закрыть файл, освободить соединение с базой, разлочить ресурс.

Вот вопрос посложнее — что выведет этот код, если внутри try есть return?

public class Main {
    public static int test() {
        try {
            System.out.println("Пробуем вернуть 1");
            return 1;
        } finally {
            System.out.println("finally всё равно выполнится");
        }
    }

    public static void main(String[] args) {
        int value = test();
        System.out.println("Получили: " + value);
    }
}

Вывод:

Пробуем вернуть 1
finally всё равно выполнится
Получили: 1

Java подготавливает значение 1 к возврату, но перед тем как реально «выйти» из метода, обязательно выполняет finally. Есть только один способ, которым можно «обойти» finally: аварийное завершение всей JVM через System.exit(), либо крах самой JVM (например, отключение питания). В обычной логике программы finally сработает всегда.

Проблема ручного закрытия ресурсов

До Java 7 закрывать ресурсы приходилось вручную именно через finally, и код выглядел так:

import java.io.FileReader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        FileReader reader = null;
        try {
            reader = new FileReader("data.txt");
            System.out.println("Читаем файл");
        } catch (IOException e) {
            System.out.println("Ошибка чтения");
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.out.println("Не смогли закрыть файл");
                }
            }
        }
    }
}

Обратите внимание: закрытие ресурса само может выбросить исключение, поэтому приходится оборачивать даже close() в ещё один try/catch. Такой код легко испортить — забыть проверку на null, забыть закрыть один из нескольких ресурсов, перепутать порядок закрытия.

Как это решает try-with-resources

Начиная с Java 7, появилась конструкция try-with-resources, которая делает всё это автоматически:

import java.io.FileReader;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        try (FileReader reader = new FileReader("data.txt")) {
            System.out.println("Читаем файл");
        } catch (IOException e) {
            System.out.println("Ошибка чтения или закрытия");
        }
    }
}

Ресурс объявляется прямо в скобках после try. Как только блок try завершается — успешно или с исключением — Java сама вызывает у ресурса метод close(), причём делает это раньше, чем передаст управление в catch или finally. Никакого ручного if (reader != null), никакого вложенного try/catch под закрытие.

Как это работает под капотом: AutoCloseable

«Магия» тут в интерфейсе AutoCloseable, у которого есть ровно один метод — close(). Любой класс, который реализует этот интерфейс, можно использовать в try-with-resources. FileReader, соединения с базой данных, сетевые сокеты — все они уже реализуют этот интерфейс за вас. Можно сделать и свой класс:

public class Connection implements AutoCloseable {
    public void query() {
        System.out.println("Выполняем запрос");
    }

    @Override
    public void close() {
        System.out.println("Соединение закрыто");
    }
}

public class Main {
    public static void main(String[] args) {
        try (Connection conn = new Connection()) {
            conn.query();
        }
    }
}

Вывод:

Выполняем запрос
Соединение закрыто

Можно объявить сразу несколько ресурсов через точку с запятой: try (Resource a = ...; Resource b = ...). Java закроет их в обратном порядке — тот, что открыт последним, закроется первым. Это логично: если второй ресурс зависит от первого, его нужно закрыть раньше.

Частые ошибки на собеседовании

  • Считают, что finally может не выполниться при обычном исключении. Не выполнится он только при System.exit() или крахе JVM.
  • Путаются в порядке при return внутри try и finally — если внутри finally тоже есть return, он «перебьёт» результат из try (это плохая практика, но её любят спрашивать отдельным вопросом).
  • Не знают, что try-with-resources требует именно интерфейс AutoCloseable, а не просто наличие метода close() у класса.
  • Забывают, что ресурсы в try-with-resources закрываются в порядке, обратном объявлению.

Итоги-шпаргалка

  • finally выполняется практически всегда — даже при исключении и даже при return внутри try.
  • try-with-resources (Java 7+) автоматически вызывает close() у ресурса, убирая необходимость в ручном finally.
  • Ресурс должен реализовывать интерфейс AutoCloseable — тогда его можно объявить прямо в скобках после try.
  • При нескольких ресурсах закрытие идёт в обратном порядке: последний открытый — первый закрытый.
Проверьте себя
1. В каком случае блок finally НЕ выполнится?
AЕсли исключение поймано в catch
BЕсли внутри try есть return
CЕсли вызван System.exit()
DЕсли исключение не было поймано ни одним catch
2. Что нужно классу, чтобы его можно было использовать в конструкции try-with-resources?
AЛюбой метод с именем close
BРеализация интерфейса AutoCloseable
CНаследование от класса Exception
DАннотация @Resource