Что такое исключение
Исключение — это способ 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 почти всегда является тем местом, куда нужно смотреть в первую очередь.