W3docs

Исключения в Java

Обзор исключений в Java — что это такое, иерархия исключений и почему важна обработка ошибок.

Исключение — это то, что происходит, когда программа сталкивается с ситуацией, которую не может обработать на текущем пути выполнения: файл не найден, деление числа на ноль, индекс массива выходит за границы. Вместо того чтобы вернуть неверный результат или молча повредить состояние, Java останавливает текущий метод, создаёт объект исключения с описанием произошедшего и начинает поиск кода, способного с этим справиться. Изучение механизма исключений — это изучение того, как Java сообщает вам, что пошло не так, где и что с этим можно сделать.

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

Исключение — это обычный Java-объект. Точнее, экземпляр класса, унаследованного от java.lang.Throwable. Когда что-то идёт не так, JVM (или ваш собственный код) создаёт один из таких объектов и бросает его. С этого момента программа следует по иному потоку управления — пока либо блок catch не примет исключение, либо поток не завершится.

Объект содержит информацию: тип (класс — NullPointerException, IOException и т. д.), сообщение (описание на человеческом языке) и трассировку стека (цепочку вызовов, зафиксированную в момент создания исключения). Когда вы читаете исключение в консоли, вы видите именно эти три составляющих.

Exception in thread "main" java.lang.ArithmeticException: / by zero
  at calc.Money.divide(Money.java:42)
  at calc.App.main(App.java:11)

Первая строка — тип и сообщение. Строки с отступом — трассировка стека, самый последний вызов идёт первым.

Почему исключения, а не коды ошибок

Старые языки — классический пример C — сигнализируют об ошибках кодами возврата: каждая функция возвращает int, вы его проверяете, и если значение отрицательное — что-то пошло не так. У этого подхода есть два недостатка:

  1. Возвращаемое значение можно проигнорировать. Вызывающий код, забывший проверить его, не увидит немедленного сбоя, и ошибка проявится позже в неочевидном месте.
  2. Путь ошибки засоряет основной путь. Каждая строка реальной работы оборачивается в if (err) return err;.

Исключения меняют это с точностью до наоборот. По умолчанию необработанное исключение останавливает выполнение, и это заметно. Вы явно выбираете, где его обрабатывать — там, где у вас есть стратегия. Основной путь остаётся чистым; путь восстановления находится в отдельном блоке.

Три вида проблем

Java делит всё, что унаследовано от Throwable, на три категории, и это различие важно, поскольку язык обращается с ними по-разному:

  • Error — проблемы самой JVM. OutOfMemoryError, StackOverflowError. В обычном коде их не перехватывают. Как правило, ничего полезного сделать нельзя.
  • RuntimeException (подкласс Exception) — программные ошибки, проявляющиеся во время выполнения. NullPointerException, IndexOutOfBoundsException, ClassCastException. Компилятор не требует их обработки, поскольку в корректном коде они не должны возникать.
  • Проверяемые Exception (всё остальное под Exception) — восстанавливаемые сбои, которые программа должна предусматривать. IOException, SQLException. Компилятор требует либо перехватить их, либо объявить в конструкции throws метода.

Граница между «программной ошибкой» (непроверяемое исключение) и «ожидаемым сбоем» (проверяемое исключение) — одно из самых спорных проектных решений Java. Мы вернёмся к этому в Java: проверяемые и непроверяемые исключения.

Как работают бросок и перехват

Когда выполняется throw, JVM:

  1. Разматывает стек — текущий метод аварийно завершается, затем его вызывающий, и так далее.
  2. На каждом кадре проверяет наличие блока try { ... } catch (SomeType e) { ... }, в котором catch соответствует типу исключения (или его супертипу).
  3. Первый подходящий catch побеждает. Управление переходит туда, разматывание стека прекращается, и выполнение возобновляется в блоке catch.
  4. Если ничего не подходит, поток завершается. В однопоточной программе это означает, что JVM выводит трассировку стека и завершает работу.

Вот почему выброшенное исключение может пройти через множество методов, прежде чем будет перехвачено — каждый непойманный throw поднимается ещё на один кадр.

Первый пример

Чтобы столкнуться с исключением, не нужно писать throw самому — Java выбрасывает их сама, когда что-то идёт не так. Вот простейший пример: деление целого числа на ноль. JVM создаёт ArithmeticException за вас.

java— editable, runs on the server

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

Что охватывает эта часть книги

Оставшиеся главы этой части — это рабочий словарь обработки исключений в Java, по одной теме за раз:

  • Структура try/catch/finally и назначение каждого блока.
  • Несколько блоков catch и сокращение multi-catch.
  • try-with-resources для всего, что требует закрытия.
  • Самостоятельный выброс исключений через throw и их объявление через throws.
  • Проверяемые и непроверяемые: какое использовать и когда.
  • Полная иерархия классов.
  • Создание собственных типов исключений для вашей предметной области.
  • Принципы, отличающие хорошую обработку исключений от защитного шума.

Читайте по порядку — каждая глава предполагает знание предыдущей.

Что дальше

Обработка исключений начинается с самой фундаментальной конструкции: блока try/catch. Перейдите к Java try...catch.

Практика

Практика
Метод `loadConfig()` читает файл и выбрасывает `IOException` при сбое. Вызывающий код оборачивает вызов в `try { loadConfig(); } catch (RuntimeException e) { ... }`. Операция ввода-вывода завершается ошибкой. Что произойдёт?
Метод `loadConfig()` читает файл и выбрасывает `IOException` при сбое. Вызывающий код оборачивает вызов в `try { loadConfig(); } catch (RuntimeException e) { ... }`. Операция ввода-вывода завершается ошибкой. Что произойдёт?
Was this page helpful?