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