Может ли переопределяющий метод объявить более широкое проверяемое исключение, чем метод родителя?
Нет. Переопределяющий метод не может объявлять более широкое или дополнительное проверяемое исключение, чем метод родителя. Он может не объявлять такое исключение вовсе, объявить то же исключение или его подкласс; непроверяемые исключения этим ограничением не связаны.
Проверяемые исключения появились как способ сделать потенциально обрабатываемые ошибки частью контракта метода. Вызывающий код должен знать о таких сбоях на этапе компиляции и явно обработать их либо передать дальше.
Ограничение для переопределения поддерживает принцип подстановки: объект подкласса должен оставаться заменяемым объектом базового типа. Код, работающий с базовым типом, не должен внезапно требовать обработки нового проверяемого исключения только потому, что фактически вызван метод подкласса.
Представим код, который хранит объект в переменной типа родителя. Компилятор проверяет вызов по контракту родительского метода, поэтому он не ожидает проверяемых исключений, которых в этом контракте нет.
Если разрешить дочернему методу добавлять более широкое проверяемое исключение, вызывающий код мог бы получить необработанную проверяемую ошибку, хотя при работе с типом родителя компилятор не требовал ни catch, ни throws. Это нарушило бы безопасность статической проверки.
При переопределении разрешены следующие варианты:
RuntimeException или Error.Более широкое исключение запрещено. Например, если родительский метод объявляет IOException, переопределяющий метод может объявить FileNotFoundException, но не Exception и не SQLException.
Здесь FileNotFoundException является подклассом IOException, поэтому контракт не расширяется. Вызывающий код, рассчитанный на Parent, уже обязан учитывать IOException, а значит, более узкое исключение не создаёт нового обязательства.
Правило применяется также при реализации метода интерфейса. При этом оно относится именно к проверяемым исключениям: объявление RuntimeException или собственного наследника RuntimeException формально не расширяет обязательства компилятора.
Важно отличать переопределение от перегрузки. Для перегрузки создаётся другой метод, и его собственный список throws не является способом переопределить контракт исходного метода. Также компилятор проверяет доступный статический тип ссылки: фактический класс объекта не меняет требований к обработке проверяемых исключений на месте вызова.
В библиотеке есть интерфейс клиента с методом чтения данных, объявляющим IOException. Команда реализует этот интерфейс через HTTP-транспорт, где библиотека HTTP-клиента выбрасывает собственное проверяемое исключение, не являющееся подклассом IOException.
Первый вариант — добавить это исключение в объявление метода реализации. Он не скомпилируется, поскольку реализация расширяет контракт интерфейса. Второй вариант — обернуть транспортное исключение в IOException; это сохраняет контракт, но может потерять тип исходной ошибки, если причина не будет корректно сохранена.
Третий вариант — преобразовать ошибку в собственное непроверяемое исключение. Это не требует изменения сигнатуры, но переносит ответственность за обработку на соглашения приложения и может скрыть ожидаемую ошибку от компилятора.
Обычно выбирают оборачивание в исключение, предусмотренное контрактом, сохраняя исходную причину. Так вызывающий код работает с единым типом ошибки, а диагностика не теряется; при этом деталям конкретного транспорта не позволяют просочиться в публичный API.
1. Может ли переопределяющий метод вообще не указывать throws, если родитель его указывает?
Да. Отсутствие throws означает, что данная реализация не сообщает о проверяемом исключении через свою сигнатуру. Ссылку на объект подкласса можно использовать там, где ожидается родитель, потому что вызывающий код всё равно ориентируется на контракт родителя.
Это не запрещает методу фактически выбросить непроверяемое исключение. Проверяемое исключение он может выбросить только если оно обработано внутри метода либо преобразовано в допустимый тип; иначе компилятор потребует изменить реализацию.
2. Почему разрешено объявить подкласс исключения, но не его родителя?
Объявление подкласса сужает множество возможных исключений. Если вызывающий код умеет обработать родительский тип, он сможет обработать и любой его подкласс.
Объявление родителя расширяет множество вариантов. Например, код, ожидающий FileNotFoundException, не обязан уметь обрабатывать произвольный IOException, поэтому такой контракт нельзя незаметно расширить при переопределении.
3. Что изменится, если новое исключение наследуется от RuntimeException?
Ограничение на проверяемые исключения не применяется, поэтому переопределяющий метод может объявить или выбросить такой тип. Компилятор не требует от вызывающего кода обязательного catch или throws.
Однако это не делает ошибку безопасной или несущественной. Непроверяемое исключение может привести к сбою операции во время выполнения, поэтому решение следует принимать по семантике ошибки, а не только для обхода ограничения сигнатуры.