Из-за чего добавление нового проверяемого исключения в сигнатуру переопределяемого метода может нарушить компиляцию реализации потомка?
Реализация потомка не может объявить проверяемое исключение, которое шире или не входит в набор проверяемых исключений, разрешённых методом родителя. Поэтому добавление такого исключения в переопределяемый метод может сделать исходный код потомка некорректным.
Потомок может не объявлять исключения, объявлять то же исключение или его подкласс. Непроверяемые исключения — RuntimeException и её подклассы — этим ограничением не связаны.
Механизм проверяемых исключений появился как способ сделать потенциально восстанавливаемые ошибки частью контракта метода. Компилятор требует, чтобы вызывающий код обработал такое исключение или явно передал его выше по стеку вызовов.
Правила для переопределения сохраняют совместимость контракта родителя. Код, работающий со ссылкой на родительский тип, должен быть защищён от исключений, которые разрешены этим типом, но не обязан заранее обрабатывать новые, более широкие исключения, добавленные только реализацией потомка.
Представим, что родительский метод объявляет IOException, а переопределяющий его метод в потомке добавляет Exception. Это недопустимо: Exception охватывает не только IOException, но и другие проверяемые исключения.
Если бы такое расширение разрешалось, вызов через ссылку родительского типа мог бы фактически выбросить исключение, отсутствующее в контракте родителя. Поэтому компилятор отклоняет реализацию потомка, а не перекладывает риск на вызывающий код.
При проверке переопределения компилятор сравнивает объявления методов. Для каждого проверяемого исключения в методе потомка должно существовать совместимое исключение в методе родителя: оно должно быть тем же классом или его подклассом.
Допустимы следующие варианты:
IOException;FileNotFoundException, если родитель объявляет IOException;IllegalStateException.Недопустимо объявлять более широкое проверяемое исключение, например Exception вместо IOException, или другое независимое проверяемое исключение, например SQLException.
Сигнатура метода в JVM не включает список throws, однако проверка правил переопределения выполняется компилятором Java. Поэтому уже скомпилированные классы могут не столкнуться с ошибкой связывания только из-за изменения throws, но исходный код при повторной компиляции или вызовы через обновлённый тип могут потребовать корректировки.
Практический компромисс таков: добавление проверяемого исключения в публичный метод делает контракт более строгим для вызывающего кода и может вызвать каскад изменений. Для API важно заранее выбирать устойчивую и понятную иерархию исключений, а не добавлять новые проверяемые исключения без оценки всех реализаций и клиентов.
В библиотеке есть базовый сервис загрузки данных, от которого унаследованы реализации для файла, сети и базы данных. Если в базовый метод добавить SQLException, файловая реализация не сможет просто объявить это исключение: оно не относится к контракту прежнего метода и не является его подтипом.
Рассматривались три варианта. Добавить в родительский метод широкое Exception проще для отдельных реализаций, но это ухудшает обработку ошибок и заставляет клиентов перехватывать слишком широкий тип. Добавить конкретное исключение в базовый контракт точнее, но может нарушить существующие реализации и вызвать изменения у клиентов. Перенести специфическую ошибку во внутреннее исключение или обернуть её в исключение, предусмотренное общим контрактом, сохраняет единый API, но может потребовать настройки причинного исключения и документации.
Выбран вариант с отдельным общим исключением уровня домена, которое допустимо объявить в базовом контракте. В результате все реализации сохраняют корректное переопределение, а вызывающий код обрабатывает единый смысловой тип ошибки вместо технических деталей конкретного хранилища.
Да. Метод потомка может не объявлять проверяемое исключение, даже если метод родителя его объявляет. Это безопасно: вызывающий код, ориентирующийся на тип родителя, всё равно обязан быть готов к исключению, предусмотренному контрактом родителя, а фактическая реализация выбросит не больше разрешённого набора.
Да. RuntimeException, её подклассы и Error не подчиняются ограничению списка проверяемых исключений при переопределении. Однако это не означает, что такие ошибки безопасны или не требуют документации: они могут привести к аварийному завершению операции во время выполнения.
Если родитель объявляет IOException, а потомок — FileNotFoundException, любой код, подготовленный к IOException, сможет обработать и FileNotFoundException, поскольку второе является его подклассом. Контракт не расширяется, а только уточняется для конкретной реализации, поэтому правило переопределения сохраняется.