try и catch
В прошлом уроке на тревогу не отвечал никто, и программа останавливалась. Теперь посадим на этаж человека, который знает, что делать. Для этого в Java есть блоки try и catch:
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");
}
}Программа печатает четыре строки и доходит до конца:
4
Skip: / by zero
3
DoneБлоки try и catch
Из чего состоит эта конструкция:
try { … }— код, в котором может случиться исключение: этаж, за которым следят;catch (ArithmeticException e) { … }— что делать, если вtryбросили исключение этого класса: человек, который отвечает на тревогу;e— переменная, в которой лежит сам объект исключения;e.getMessage()возвращает его сообщение.
Поймать исключение — значит выполнить подходящий catch вместо того, чтобы остановить программу. Давайте разберём запуск по шагам:
nравно3:12 / 3печатает4, иcatchпропускается.nравно0: деление бросаетArithmeticException. Остатокtryне выполняется, управление переходит вcatch, и он печатаетSkip: / by zero.- Цикл идёт дальше: при
n, равном4, печатается3. - После цикла печатается
Done: программа работает до конца.
После catch программа продолжает работу со строки, которая идёт за всей конструкцией try/catch. В прерванный try она уже не возвращается.
Путь исключения по стеку
А если исключение бросили глубоко во вложенном вызове? Возьмём программу с методами report и average, разделим строку в report на две и поставим try только в main. Правило такое:
- Метод, в котором бросили исключение, сразу завершается: без
returnи без оставшихся строк. - Исключение приходит в вызвавший метод, в строку с вызовом.
- Если эта строка внутри
tryс подходящимcatch, выполняетсяcatch. - Если нет, вызвавший метод тоже завершается, и поиск идёт к тому, кто вызвал его.
Это и есть тревога, которая звучит этаж за этажом. Нажимайте «Следующий шаг» и следите за столбцом рамок вызовов. Здание в нём перевёрнуто: последний вызов лежит сверху, как тарелка в стопке, а main — в самом низу. Исключение снимает рамки одну за другой, пока не найдёт catch.
1public class Main {2 static int average(int sum, int n) {3 return sum / n;4 }5 6 static void report(int sum, int n) {7 int avg = average(sum, n);8 System.out.println(avg);9 }10 11 public static void main(String[] args) {12 try {13 report(10, 0);14 System.out.println("Done");15 } catch (ArithmeticException e) {16 System.out.println("No data");17 }18 System.out.println("End");19 }20}
main входит в try и вызывает report(10, 0).
Обратите внимание: переменная avg так и не получила значения, а строка Done не напечаталась. Каждый метод на пути исключения закончил работу на той строке, где его застало исключение.
На собеседовании это спрашивают так: что происходит, когда бросается исключение? Ответ: Java ищет подходящий catch, переходя от метода к вызвавшему его методу, и завершает каждый метод на этом пути. Если catch не нашёлся даже в main, программа останавливается с трассировкой стека.
Несколько блоков catch
У одного try может быть несколько catch, по одному на класс исключения:
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 через |:
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 для наследника:
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");
}error: exception ArrayIndexOutOfBoundsException has already been caughtПрограмма не компилируется. Первый catch ловит любое Exception, а значит, и ArrayIndexOutOfBoundsException. До второго catch очередь не дойдёт никогда, и компилятор считает такой код ошибкой. Поменяйте блоки местами: сначала ArrayIndexOutOfBoundsException, потом Exception.
Главное
- Если в
tryбросили исключение, остатокtryпропускается, выполняется подходящийcatch, а за ним код после всей конструкции. - Исключение завершает каждый метод на своём пути, пока не найдёт
tryс подходящимcatch. - Из нескольких
catchвыполняется первый подходящий, поэтомуcatchдля наследника пишут раньше, чем для родителя. catch (A | B e)ловит исключения двух классов одним блоком.