Оператор try-catch-finally в Java
Поставьте catch (Exception e) перед catch (ArithmeticException e) — и программа даже не скомпилируется: компилятор сообщит, что ArithmeticException уже перехвачен. Порядок блоков catch, правила multi-catch и поведение finally при return — частые темы вопросов на собеседованиях по Java.
Конструкция try-catch-finally в Java — это механизм обработки исключений: в блоке try размещается код, который может выбросить исключение, блоки catch перехватывают и обрабатывают исключения указанных типов, а блок finally выполняется после try и catch практически всегда — обычно в нём освобождают ресурсы.
Синтаксис try-catch-finally в Java
Общая форма блока обработки исключений:
public class ExceptionExample1 {
public static void main(String[] args) {
try {
// блок кода, в котором отслеживаются ошибки
} catch (ExceptionType1 e) {
// обработчик исключений типа ExceptionType1
} catch (ExceptionType2 e) {
// обработчик исключений типа ExceptionType2
} finally {
// блок кода, который выполняется после завершения try и catch
}
}
} Блоки catch и finally необязательны по отдельности, но блок try не может стоять один. Допустимые комбинации:
try-catch— перехват и обработка исключения;try-finally— без обработки, но с гарантированным выполнением завершающего кода;try-catch-finally— обработка плюс завершающий код;try-with-resources(Java 7+) — может обходиться безcatchиfinally, ресурсы закрываются автоматически.
Одиночный try { ... } без catch, finally и ресурсов — ошибка компиляции.
Несколько блоков catch и их порядок
Один фрагмент кода может выбросить исключения разных типов. Для этого после try пишут два или более блока catch, каждый — для своего типа исключений.
Когда возбуждается исключение, блоки catch проверяются по порядку сверху вниз, и выполняется первый из них, чей тип совпадает с типом исключения (или является его суперклассом).
После того как выполнится один из блоков catch, остальные пропускаются, и выполнение программы продолжается с места, следующего за конструкцией try-catch.
Рассмотрим пример применения нескольких блоков catch:
public class MultiCatch {
public static void main(String[] args) {
try {
int a = args.length;
int b = 42 / a;
int[] c = new int[a];
c[a] = 6;
} catch (ArithmeticException e) {
System.out.println("Деление на ноль: " + e);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("Уже после блока try-catch-catch.");
}
} Если запустить программу без аргументов, то a = 0 и деление 42 / a выбросит ArithmeticException. Если передать один аргумент, деление пройдёт, но обращение c[1] к массиву длиной 1 выбросит ArrayIndexOutOfBoundsException. В обоих случаях после обработки выполнится последняя строка main.
При использовании нескольких блоков catch подклассы исключений должны идти раньше своих суперклассов. Блок catch с суперклассом перехватывает исключения этого класса и всех его подклассов, поэтому блок с подклассом, стоящий после него, никогда не получит управление. А недостижимый код в Java — это ошибка компиляции:
public class SuperSubCatch {
public static void main(String[] args) {
try {
int a = 0;
int b = 42 / a;
} catch (Exception e) {
System.out.println("Перехват исключений общего класса Exception.");
} /*catch (ArithmeticException e) {
// ОШИБКА компиляции: exception ArithmeticException has already been caught
System.out.println("Этот код вообще недостижим.");
}*/
}
} Ещё одно правило компилятора: нельзя писать catch для проверяемого исключения (например, IOException), которое код в блоке try выбросить не может, — это тоже ошибка компиляции. На непроверяемые исключения, а также на Exception и Throwable это правило не распространяется.
Multi-catch: несколько исключений в одном catch
Начиная с Java 7, несколько типов исключений, которые обрабатываются одинаково, можно перечислить в одном блоке catch через вертикальную черту |. Такой блок называют multi-catch.
До Java 7 приходилось дублировать обработчики:
try {
// ...
} catch (IllegalStateException e) {
System.out.println(e.getMessage());
} catch (IOException e) {
System.out.println(e.getMessage());
} catch (SQLException e) {
System.out.println(e.getMessage());
} С multi-catch тот же код записывается короче:
try {
// ...
} catch (IllegalStateException | IOException | SQLException e) {
System.out.println(e.getMessage());
} Пример использования multi-catch блока:
public class MultiCatch2 {
public static void main(String[] args) {
try {
int a = args.length;
int b = 42 / a;
int[] c = new int[a];
c[a] = 6;
} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
System.out.println("Ошибка: " + e);
}
System.out.println("Уже после блока try-catch.");
}
} Типы в multi-catch не должны быть связаны наследованием: если один тип является подклассом другого, код не скомпилируется. Например, catch (FileNotFoundException | IOException e) — ошибка, потому что FileNotFoundException наследует IOException, и достаточно указать только IOException.
Обратите внимание
Параметр multi-catch блока неявно объявлен как final: присвоить ему новое значение (e = new IOException();) нельзя — ошибка компиляции. В обычном catch с одним типом параметр переприсвоить можно, если он не объявлен как final явно.
Блок finally и конструкция try-finally
Когда генерируется исключение, выполнение метода идёт по нелинейному пути. Если файл открывается в начале метода и закрывается в конце, вряд ли кого-то устроит, что код закрытия файла будет пропущен из-за исключения. Для таких случаев и служит блок finally.
Блок finally выполняется по завершении блоков try и catch, но перед кодом, который следует за всей конструкцией.
Блок finally выполняется независимо от того, было ли выброшено исключение. Если исключение выброшено и ни один из блоков catch ему не соответствует, finally всё равно выполнится, а затем исключение пойдёт дальше по стеку вызовов. Каждый раз, когда метод собирается вернуть управление вызывающему коду из блока try-catch — через необработанное исключение или явно через оператор return, — блок finally выполняется перед выходом из метода.
В примере ниже все три метода используют конструкцию try-finally без catch: исключение в procA() не обрабатывается на месте, а перехватывается уже в main, но finally успевает выполниться.
public class FinallyUse {
// Выход из метода через исключение
public static void procA() {
try {
System.out.println("Внутри procA");
throw new RuntimeException("demo");
} finally {
System.out.println("finally для procA");
}
}
// Возврат изнутри try-блока
public static void procB() {
try {
System.out.println("Внутри procB");
return;
} finally {
System.out.println("finally для procB");
}
}
// Нормальное выполнение try-блока
public static void procC() {
try {
System.out.println("Внутри procC");
} finally {
System.out.println("finally для procC");
}
}
public static void main(String[] args) {
try {
procA();
} catch (Exception e) {
System.out.println("Исключение выброшено");
}
procB();
procC();
}
} Результат выполнения:
Внутри procA
finally для procA
Исключение выброшено
Внутри procB
finally для procB
Внутри procC
finally для procC Для закрытия файлов, соединений и потоков в современном коде вместо ручного finally обычно используют try-with-resources (Java 7+): ресурсы, реализующие AutoCloseable, закрываются автоматически, а исключения при закрытии не теряются.
Когда finally выполняется, а когда нет
| Ситуация | Выполнится ли finally | Комментарий |
|---|---|---|
| try завершился без исключений | Да | Сразу после try |
| Исключение перехвачено в catch | Да | После выполнения catch |
| Исключение не перехвачено ни одним catch | Да | Затем исключение передаётся вызывающему методу |
| Новое исключение выброшено внутри catch | Да | Затем новое исключение передаётся дальше |
return, break или continue в try/catch | Да | Перед фактическим выходом |
Вызов System.exit() в try/catch | Нет | JVM завершает работу немедленно |
| Крах JVM, принудительное завершение процесса | Нет | Например, kill -9 или отключение питания |
| Бесконечный цикл или вечная блокировка в try | Нет | Управление просто не доходит до finally |
| Daemon-поток, а JVM завершается | Не гарантируется | Daemon-потоки останавливаются без выполнения finally |
Неочевидные моменты: return в finally
Если return есть и в try, и в finally, метод вернёт значение из finally — оно перекрывает результат блока try:
public class FinallyReturn {
static int getValue() {
try {
return 1;
} finally {
return 2; // перекрывает return из try
}
}
static int lostException() {
try {
throw new RuntimeException("Ошибка");
} finally {
return 0; // исключение молча теряется
}
}
public static void main(String[] args) {
System.out.println(getValue()); // 2
System.out.println(lostException()); // 0, исключения нет
}
} Во втором методе исключение вообще «исчезает»: return в finally отменяет его распространение, и вызывающий код о проблеме не узнает.
Важно
Не пишите return, break, continue и throw внутри finally: они подменяют результат метода и могут скрыть исключение. Компилятор это разрешает, но IDE и статические анализаторы помечают такой код как ошибку проектирования.
Часто задаваемые вопросы
Есть ли в Java блок else для try-catch, как в Python?
Нет, конструкции try-catch-else в Java не существует. Код, который должен выполниться только при отсутствии исключения, просто пишут в конце блока try после потенциально опасных операций. Если же он должен выполняться всегда, его помещают в finally.
Чем multi-catch отличается от catch (Exception e)?
Multi-catch перехватывает только явно перечисленные типы, например catch (IOException | SQLException e), а остальные исключения пропускает дальше. catch (Exception e) перехватывает вообще все проверяемые и непроверяемые исключения, включая ошибки в логике программы, которые лучше не глушить.
Изменит ли finally возвращаемое значение, если поменять в нём переменную?
Для примитивов и неизменяемых объектов — нет. В методе int x = 1; try { return x; } finally { x = 10; } вернётся 1: значение для return вычисляется и запоминается до выполнения finally. Но если вернуть ссылку на изменяемый объект (например, список) и изменить его содержимое в finally, вызывающий код увидит изменения.
Что будет, если исключение выброшено внутри catch?
Остальные блоки catch той же конструкции его не перехватят — они относятся только к блоку try. Сначала выполнится finally, а затем новое исключение уйдёт вызывающему методу. Чтобы не потерять исходную причину, передавайте её в конструктор: throw new MyException("...", e).
Что лучше для закрытия ресурсов: finally или try-with-resources?
Try-with-resources. Он короче, закрывает ресурсы автоматически в обратном порядке и сохраняет исключения, возникшие при закрытии, как suppressed. Ручной finally оправдан, когда нужно выполнить завершающее действие, не связанное с AutoCloseable, например снять блокировку lock.unlock().
Practice
Reinforce this lesson with hands-on tasks, each with a level and a time estimate.
All module tasks →
Comments