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

Что такое исключение

Чему вы научитесь

Чему вы научитесь:

  • читать сообщение об исключении и находить по трассировке стека строку, где случилась ошибка;
  • ловить исключения в try и catch и выполнять обязательный код в finally;
  • отличать проверяемые исключения от непроверяемых и бросать свои через throw;
  • закрывать ресурсы автоматически через try-with-resources.

Представьте офисное здание, где работа спускается сверху вниз. Директор на верхнем этаже поручает задачу отделу этажом ниже. Отдел передаёт часть работы в мастерскую ещё ниже и ждёт, пока та вернёт результат.

Вдруг в мастерской срабатывает пожарная сигнализация. Если там есть человек, который знает, что делать, он отвечает на тревогу, и работа идёт дальше. Если такого нет, мастерская бросает работу, а сигнал звучит этажом выше — у того, кто её поручил. Так этаж за этажом, вверх по лестнице, пока кто-нибудь не ответит. Не ответил даже директор — здание эвакуируют целиком.

Программа на Java устроена так же. Метод main поручает работу другим методам, а те вызывают следующие. Когда метод не может закончить работу, срабатывает сигнализация — исключение. Вы уже встречали такие тревоги:

  • ArithmeticException — целое деление на ноль;
  • ArrayIndexOutOfBoundsException — индекс за границей массива;
  • NullPointerException — обращение к полю или методу через null;
  • ClassCastException — приведение к классу, которым объект не является;
  • NumberFormatException — Integer.parseInt получил строку, которая не целое число.

Каждый раз программа компилировалась, а при запуске останавливалась: на тревогу никто не отвечал, и «эвакуировали» всю программу. В этой теме вы научитесь отвечать на тревогу. Но сначала разберём, что именно сообщает Java, когда программа останавливается.

Исключение — это объект

Возьмём программу из трёх этажей: main вызывает report, а report — метод average, который делит сумму на количество.

JAVA
public class Main {
    static int average(int sum, int n) {
        return sum / n;
    }

    static void report(int sum, int n) {
        System.out.println("Average: " + average(sum, n));
    }

    public static void main(String[] args) {
        report(10, 2);
        report(10, 0);
        System.out.println("Done");
    }
}

Запустим её:

TEXT
Average: 5
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.average(Main.java:3)
	at Main.report(Main.java:7)
	at Main.main(Main.java:12)

Давайте разберём, что произошло:

  1. report(10, 2) вызывает average(10, 2), получает 5 и печатает Average: 5.
  2. report(10, 0) вызывает average(10, 0), и у деления 10 / 0 нет результата.
  3. JVM создаёт объект класса ArithmeticException и бросает его. Метод average завершается сразу, без return.
  4. report так и не печатает свою строку, а main не доходит до Done: программа останавливается.

Исключение — больше, чем текст на экране: это объект. У него есть класс, например ArithmeticException, и сообщение — здесь / by zero. Бросить исключение — значит создать такой объект и прервать обычный ход программы.

Первая строка: что случилось

Первая строка сообщения отвечает на вопрос «что случилось»:

  • Exception in thread "main" — исключение случилось в главном потоке программы, в том, что выполняет main;
  • java.lang.ArithmeticException — полное имя класса исключения: пакет java.lang и сам класс;
  • / by zero — сообщение с подробностями: делили на ноль.

Часто всё понятно уже по имени класса: ArithmeticException — ошибка в арифметике, NullPointerException — где-то оказался null.

Трассировка стека: где случилось

Строки, которые начинаются с at, называют трассировкой стека. Каждая из них — один незавершённый вызов из стека вызовов, о котором шла речь в теме о методах. Читайте трассировку сверху вниз:

  1. at Main.average(Main.java:3) — класс Main, метод average, файл Main.java, строка 3. Здесь исключение бросили.
  2. at Main.report(Main.java:7) — average вызвали из метода report, из строки 7.
  3. at Main.main(Main.java:12) — а report вызвали из main, из строки 12.

Трассировка похожа на журнал пожарной сигнализации. Первым в нём записан этаж, где сработал датчик, ниже — каждый этаж на пути вверх, последним — директор main.

На собеседовании иногда показывают трассировку и спрашивают, где ошибка. Начинайте с первой строки at: там исключение бросили, а строки ниже показывают, кто кого вызвал.

Здесь картинка со зданием неточна. Настоящая сирена слышна во всём доме, а исключение идёт только по цепочке незавершённых вызовов. Первый вызов report(10, 2) к этому моменту уже вернулся, поэтому в трассировке его нет.

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

Трассировка начинается не с вашего кода, и новички ищут ошибку внутри самой Java:

JAVA
String text = "12a";
int n = Integer.parseInt(text); // ✗
System.out.println(n + 1);
TEXT
Exception in thread "main" java.lang.NumberFormatException: For input string: "12a"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at java.base/java.lang.Integer.parseInt(Integer.java:662)
	at Main.main(Main.java:4)

Первые три строки at ведут внутрь стандартной библиотеки: на это указывает приставка java.base/. Ошибки в библиотеке нет. Метод parseInt честно сообщил, что строка "12a" — не целое число, и бросил исключение изнутри себя.

Ищите первую строку со своим классом: at Main.main(Main.java:4). Это ваш вызов parseInt — в файле перед этим фрагментом стоят ещё две строки, заголовки класса и метода main. Исправлять нужно именно вызов: передавать в parseInt только целое число.

Главное

  • Исключение — объект с классом и сообщением; JVM бросает его, когда программа не может продолжить работу.
  • Брошенное исключение завершает метод, а за ним каждый вызвавший метод; если на тревогу никто не ответил, программа останавливается.
  • Первая строка сообщения называет класс исключения и сообщение, а строки at — это трассировка стека.
  • Трассировку читают сверху: первая строка at — место, где бросили исключение, а свою ошибку ищут в первой строке at со своим классом.