Объясните механизм поиска обработчика для неперехваченного исключения по стеку вызовов Java.
Java ищет подходящий catch сначала в текущем методе, затем последовательно в вызывающих методах, поднимаясь по стеку вызовов. Обработчик подходит, если его тип является суперклассом фактического типа исключения или совпадает с ним. Если обработчик не найден, текущие вызовы завершаются с раскруткой стека, а исключение передаётся необработанному обработчику потока.
Механизм исключений решает проблему передачи ошибки через несколько уровней вызовов без возврата специальных кодов из каждого метода. Метод может сосредоточиться на своей основной задаче, а обработка ошибки выполняется на уровне, где достаточно контекста для принятия решения.
В Java эта модель дополнена разделением исключений на проверяемые и непроверяемые. Однако сам механизм поиска обработчика по стеку применяется к обоим видам исключений.
Ошибка часто возникает глубоко внутри приложения: например, при чтении файла или обращении к базе данных. Низкоуровневый метод может не знать, нужно ли повторить операцию, преобразовать ошибку в доменную или сообщить о сбое пользователю.
Если обработчик размещён слишком низко, он может скрыть важную информацию или выбрать неверную стратегию. Если обработчика нет вообще, исключение завершит текущий поток, поэтому важно понимать, какие вызовы будут прерваны при его распространении.
При возникновении исключения Java проверяет обработчики в текущем кадре стека. Если подходящего catch нет, метод не продолжает выполнение: его кадр удаляется, управление возвращается вызывающему методу, и поиск продолжается там.
Проверяется фактический тип объекта исключения во время выполнения. Например, исключение типа IllegalArgumentException может быть обработано catch для RuntimeException или Exception, но не обработчиком несвязанного типа.
Порядок поиска следующий:
catch не найден.После выбора обработчика управление переходит в него, а кадры между местом возникновения исключения и обработчиком уже считаются раскрученными. Поэтому локальные переменные и выполнение оставшихся операторов таких методов недоступны. Блоки finally, предусмотренные для покидаемых конструкций, выполняются во время выхода из них.
Объявление исключения в throws не заставляет JVM искать обработчик именно в вызывающем методе. Это ограничение проверяет компилятор для проверяемых исключений; во время выполнения действует фактический тип исключения и структура стека.
Минимальный пример:
readData не содержит подходящего обработчика, поэтому его кадр удаляется. В service подходящего обработчика также нет, и исключение доходит до main, где RuntimeException совместим с фактическим типом IllegalStateException.
В сервисе ошибка подключения к базе данных возникает в репозитории, но решение о повторной попытке зависит от бизнес-операции. Рассматривались два варианта: обработать исключение в репозитории или передать его выше.
Обработка в репозитории позволила бы централизовать работу с драйвером, но репозиторий не знал бы, безопасно ли повторять конкретную операцию. Передача исключения до сервисного слоя сохранила контекст операции, однако потребовала определить понятный контракт ошибок.
Был выбран второй вариант: репозиторий добавляет технический контекст и сохраняет исходную причину, сервис решает вопрос о повторе или преобразует ошибку в доменное исключение, а внешний слой формирует ответ клиенту. Это предотвращает преждевременное поглощение ошибки и сохраняет трассировку для диагностики.
Допустимо ли перехватить исключение в вызывающем методе, если вызванный метод сам его не обработал?
Да. Если вызов находится внутри try вызывающего метода и тип исключения совместим с типом catch, обработчик будет найден после раскрутки кадра вызванного метода. Сам вызываемый метод не обязан содержать собственный catch; для проверяемого исключения он должен лишь корректно объявить его или обработать.
Что происходит, если подходящий обработчик не найден ни в одном кадре стека?
Исключение передаётся необработанному обработчику текущего потока. По умолчанию он выводит информацию об исключении и трассировку стека, после чего поток завершается. Если это был поток с критически важной задачей, приложение может потерять эту часть работы; поэтому на границах потоков обычно устанавливают явную политику журналирования и завершения.
Формируется ли трассировка стека заново на каждом уровне распространения исключения?
Обычно нет. Трассировка связана с местом создания или заполнения исключения, а не с каждым переходом к вызывающему методу. Повторный throw того же объекта продолжает распространение того же исключения; явное создание нового исключения может изменить диагностическую картину, поэтому исходную причину следует сохранять как cause.