Перейти к содержимому
Теория · Исключения · урок 2 из 12

try и catch

В прошлом уроке на тревогу не отвечал никто, и программа останавливалась. Теперь посадим на этаж человека, который знает, что делать. Для этого в Java есть блоки try и catch:

JAVA
public class Main {
    public static void main(String[] args) {
        int[] boxes = {3, 0, 4};
        for (int n : boxes) {
            try {
                System.out.println(12 / n);
            } catch (ArithmeticException e) {
                System.out.println("Skip: " + e.getMessage());
            }
        }
        System.out.println("Done");
    }
}

Программа печатает четыре строки и доходит до конца:

TEXT
4
Skip: / by zero
3
Done

Блоки try и catch

Из чего состоит эта конструкция:

  • try { … } — код, в котором может случиться исключение: этаж, за которым следят;
  • catch (ArithmeticException e) { … } — что делать, если в try бросили исключение этого класса: человек, который отвечает на тревогу;
  • e — переменная, в которой лежит сам объект исключения; e.getMessage() возвращает его сообщение.

Поймать исключение — значит выполнить подходящий catch вместо того, чтобы остановить программу. Давайте разберём запуск по шагам:

  1. n равно 3: 12 / 3 печатает 4, и catch пропускается.
  2. n равно 0: деление бросает ArithmeticException. Остаток try не выполняется, управление переходит в catch, и он печатает Skip: / by zero.
  3. Цикл идёт дальше: при n, равном 4, печатается 3.
  4. После цикла печатается Done: программа работает до конца.

После catch программа продолжает работу со строки, которая идёт за всей конструкцией try/catch. В прерванный try она уже не возвращается.

Путь исключения по стеку

А если исключение бросили глубоко во вложенном вызове? Возьмём программу с методами report и average, разделим строку в report на две и поставим try только в main. Правило такое:

  1. Метод, в котором бросили исключение, сразу завершается: без return и без оставшихся строк.
  2. Исключение приходит в вызвавший метод, в строку с вызовом.
  3. Если эта строка внутри try с подходящим catch, выполняется catch.
  4. Если нет, вызвавший метод тоже завершается, и поиск идёт к тому, кто вызвал его.

Это и есть тревога, которая звучит этаж за этажом. Нажимайте «Следующий шаг» и следите за столбцом рамок вызовов. Здание в нём перевёрнуто: последний вызов лежит сверху, как тарелка в стопке, а main — в самом низу. Исключение снимает рамки одну за другой, пока не найдёт catch.

ВживуюИсключение ищет свой catchшаг 1 из 8 · рамок: 1

main входит в try и вызывает report(10, 0).

Рамки снимаются, пока не найдётся try с подходящим catch

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

На собеседовании это спрашивают так: что происходит, когда бросается исключение? Ответ: Java ищет подходящий catch, переходя от метода к вызвавшему его методу, и завершает каждый метод на этом пути. Если catch не нашёлся даже в main, программа останавливается с трассировкой стека.

Несколько блоков catch

У одного try может быть несколько catch, по одному на класс исключения:

JAVA
int[] boxes = {3, 0};
for (int i = 0; i <= 2; i++) {
    try {
        System.out.println(12 / boxes[i]); // 4
    } catch (ArithmeticException e) {
        System.out.println("Empty box " + i); // Empty box 1
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("No box " + i); // No box 2
    }
}

Java проверяет блоки catch сверху вниз и выполняет первый подходящий. Срабатывает только один catch:

  • при i, равном 1, деление на ноль ловит первый catch;
  • при i, равном 2, ячейки нет, и ArrayIndexOutOfBoundsException ловит второй.

«Подходящий» значит, что класс исключения совпадает с классом в catch или наследует его. Классы исключений — обычные классы с родителями и наследниками. ArithmeticException, NullPointerException и другие знакомые вам исключения — наследники класса Exception, поэтому catch (Exception e) ловит любое из них.

Здесь картинка со зданием неточна. В здании на тревогу ответит любой, кто её услышал. А catch отвечает только на исключения своего класса и его наследников, остальные проходят мимо него дальше.

Отсюда правило порядка: сначала catch для наследника, потом для родителя. В обратном порядке программа не скомпилируется — это разобрано в разделе «Частая ошибка». Порядок catch любят спрашивать на собеседованиях.

Один catch на несколько классов

Если ответ на разные исключения одинаковый, их классы перечисляют в одном catch через |:

JAVA
String[] inputs = {"6", "x", "0"};
for (String s : inputs) {
    try {
        System.out.println(12 / Integer.parseInt(s));
    } catch (NumberFormatException | ArithmeticException e) {
        System.out.println("Bad input: " + s);
    }
}

Строку "x" нельзя прочитать как число, и parseInt бросает NumberFormatException. Строку "0" прочитать можно, но на ноль делить нельзя — это ArithmeticException. Исключения разные, а ответ один, поэтому программа печатает 2, Bad input: x и Bad input: 0.

Через | нельзя объединить родителя с наследником: на catch (ArithmeticException | Exception e) компилятор ответит Alternatives in a multi-catch statement cannot be related by subclassing. Exception и так ловит оба класса.

Частая ошибка

catch для родителя поставили выше catch для наследника:

JAVA
int[] boxes = {3, 0};
try {
    System.out.println(12 / boxes[2]);
} catch (Exception e) {
    System.out.println("Something failed");
} catch (ArrayIndexOutOfBoundsException e) { // ✗
    System.out.println("No such box");
}
TEXT
error: exception ArrayIndexOutOfBoundsException has already been caught

Программа не компилируется. Первый catch ловит любое Exception, а значит, и ArrayIndexOutOfBoundsException. До второго catch очередь не дойдёт никогда, и компилятор считает такой код ошибкой. Поменяйте блоки местами: сначала ArrayIndexOutOfBoundsException, потом Exception.

Главное

  • Если в try бросили исключение, остаток try пропускается, выполняется подходящий catch, а за ним код после всей конструкции.
  • Исключение завершает каждый метод на своём пути, пока не найдёт try с подходящим catch.
  • Из нескольких catch выполняется первый подходящий, поэтому catch для наследника пишут раньше, чем для родителя.
  • catch (A | B e) ловит исключения двух классов одним блоком.