Menu

Исключения в Java: ошибки, трассировки стека и выбрасывание

Что такое исключение в Java, как читать трассировку стека, различие между проверяемыми и непроверяемыми исключениями, иерархия исключений и как выбрасывать свои собственные.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

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

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

Если никто его не обработает, исключение достигает вершины вашей программы, JVM печатает трассировку стека, и процесс завершается с ненулевым статусом.

Обратите внимание, что "after" никогда не печатается. В тот миг, когда вычисляется numbers[5], выбрасывается ArrayIndexOutOfBoundsException, и оставшаяся часть main отбрасывается.

Чтение трассировки стека

Когда исключение остаётся необработанным, вы получаете вывод вроде такого. Он выглядит пугающе, но это всего лишь список:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Main.divide(Main.java:8)
	at Main.main(Main.java:4)

Читайте сверху вниз:

  • Первая строка — это тип (ArithmeticException) и сообщение (/ by zero).
  • Каждая строка at ... — это кадр стека. Верхний — это место, где исключение действительно было выброшено: divide на строке 8. Под ним вызывающий, main на строке 4.

Верхняя строка at — это то, куда вы смотрите первым делом. Кадры под ней отвечают на вопрос «как мы сюда попали?».

Запустите это, и трассировка укажет прямо на строку a / b, а затем покажет main как вызывающего. Эта цепочка вызовов — самый полезный инструмент отладки, который Java даёт вам бесплатно.

Иерархия исключений

Каждое исключение — это объект, и все они происходят от Throwable. Две важные ветви:

  • Error — серьёзные проблемы, которые поднимает JVM, например OutOfMemoryError или StackOverflowError. Их вы, как правило, не перехватываете.
  • Exception — проблемы, которые ваша программа может разумно предвидеть и обработать. Внутри неё находится RuntimeException — родитель повседневных багов вроде NullPointerException.
Throwable
├── Error                 (don't catch: OutOfMemoryError, StackOverflowError)
└── Exception
    ├── IOException        (checked)
    ├── SQLException       (checked)
    └── RuntimeException   (unchecked)
        ├── NullPointerException
        ├── ArithmeticException
        └── ArrayIndexOutOfBoundsException

Поскольку это настоящие классы, исключение может нести сообщение, и вы можете расспросить его о нём самом:

getMessage() возвращает текст после двоеточия в трассировке стека; getClass().getSimpleName() даёт вам тип исключения по имени.

Проверяемые и непроверяемые

Это различие сбивает новичков с толку чаще всего.

  • Непроверяемые исключения наследуют RuntimeException. Обычно они означают баг в вашем коде — неожиданный null, неверный индекс, деление на ноль. Компилятор не заставляет вас их обрабатывать.
  • Проверяемые исключения наследуют Exception, но не RuntimeException (например, IOException). Они представляют условия вне вашего контроля — отсутствующий файл, оборвавшееся сетевое соединение. Компилятор заставляет вас либо перехватить их, либо объявить.

Если метод может выбросить проверяемое исключение, он должен указать это через throws, и каждый вызывающий обязан с ним разобраться:

Уберите throws Exception из main, и код даже не скомпилируется — это компилятор обеспечивает соблюдение контракта проверяемых исключений. Непроверяемые исключения этого никогда не требуют.

Выбрасывание собственных исключений

Вы не только реагируете на исключения — вы можете их и поднимать. Используйте throw с новым объектом исключения, чтобы сигнализировать, что аргумент или состояние недопустимы. Это гораздо лучше, чем возвращать магическое значение вроде -1 и надеяться, что вызывающий его проверит.

Всегда включайте сообщение, объясняющее, что было не так, и в идеале — недопустимое значение: ваше будущее «я», читающее трассировку стека, скажет вам спасибо. IllegalArgumentException и IllegalStateException — два исключения, к которым вы будете обращаться чаще всего при проверке входных данных.

Частые подводные камни

  • Проглатывание исключений. Перехват исключения без каких-либо действий (пустой блок) скрывает баги. Как минимум залогируйте его; обычно следует обработать его или пробросить дальше.
  • Слишком широкий перехват Exception. Перехват базового Exception (или, хуже того, Throwable) может замаскировать проблемы, которые вы не собирались обрабатывать. Перехватывайте конкретный ожидаемый тип.
  • Чтение трассировки снизу вверх. Самый значимый кадр — это верхняя строка at, а не нижняя. Начинайте с неё.
  • Путаница между Error и Exception. Не пытайтесь восстановиться после OutOfMemoryError или StackOverflowError; вместо этого устраните первопричину.

Далее: try-catch

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

Часто задаваемые вопросы

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

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

В чём разница между проверяемым и непроверяемым исключением в Java?

Проверяемые исключения (подклассы Exception, но не RuntimeException, например IOException) должны быть либо перехвачены, либо объявлены через throws — компилятор это требует. Непроверяемые исключения (подклассы RuntimeException, например NullPointerException, ArrayIndexOutOfBoundsException) обычно сигнализируют об ошибках программирования и не требуют объявления. Error (например OutOfMemoryError) также непроверяемое и, как правило, не предназначено для перехвата.

Как читать трассировку стека в Java?

Читайте сверху вниз. Первая строка называет тип исключения и сообщение (например, java.lang.ArithmeticException: / by zero). Каждая строка at ... ниже — это кадр стека, показывающий метод, файл и номер строки, начиная с места, где исключение было выброшено, и далее назад через вызывающие методы. Верхняя строка at почти всегда является тем местом, куда нужно смотреть в первую очередь.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ