Исключения при наследовании в Java: переопределение методов и конструкторы - Вопросы
Всего: 5 вопросов
1. Какие исключения может объявить в throws переопределяющий метод в Java и зачем нужно это ограничение?
Какие исключения может объявить в throws переопределяющий метод в Java и зачем нужно это ограничение?
Переопределяющий метод может объявить те же проверяемые (checked) исключения, что и метод суперкласса, их подклассы или не объявлять ничего; непроверяемые исключения можно добавлять любые. Если у родителя void start() throws IOException, то в подклассе допустимы throws IOException, throws FileNotFoundException, отсутствие throws и throws IllegalArgumentException, а throws Exception (более общий тип) и throws SQLException (не связанный тип) дают ошибку overridden method does not throw Exception. Правило защищает полиморфный код: метод, который вызывает obj.start() через ссылку на суперкласс и перехватывает только IOException, не должен получить необработанное проверяемое исключение от наследника. Подкласс может обещать меньше, чем родитель, но не больше.
2. Какие исключения должен объявить конструктор подкласса, если конструктор суперкласса объявляет throws, и можно ли перехватить их вокруг super()?
Какие исключения должен объявить конструктор подкласса, если конструктор суперкласса объявляет throws, и можно ли перехватить их вокруг super()?
Конструктор подкласса всегда вызывает конструктор суперкласса, явно через super(...) или неявно, поэтому обязан объявить все его проверяемые исключения тем же типом или суперклассом. Для SuperClass() throws IOException компилируются SubClass() throws IOException, throws Exception и throws IOException, SQLException: добавлять новые проверяемые исключения конструктору можно. Не компилируются throws FileNotFoundException (подкласс не покрывает все случаи), конструктор без throws и отсутствие конструктора: unreported exception IOException in default constructor. Перехватить исключение нельзя: super(...) не может стоять внутри try, в том числе в Java 25 с Flexible Constructor Bodies (JEP 513). Выход — вызвать другой конструктор суперкласса или создавать объект через фабричный метод.
3. Почему в Runnable.run() нельзя объявить throws IOException и как обработать IOException внутри run()?
Почему в Runnable.run() нельзя объявить throws IOException и как обработать IOException внутри run()?
Правило переопределения действует и при реализации методов интерфейса: метод класса не может объявить проверяемое исключение, которого нет в throws метода интерфейса. Runnable.run() не объявляет ничего, поэтому public void run() throws IOException даёт ошибку run() in FileTask cannot implement run() in Runnable; overridden method does not throw IOException. То же относится к Comparator.compare(), Object.toString() и Consumer.accept() в лямбдах. Исключение обрабатывают на месте или оборачивают в непроверяемое с сохранением причины: catch (IOException e) { throw new UncheckedIOException(e); } (класс есть с Java 8), для других типов — throw new RuntimeException(e). Пустой catch не подходит: ошибка пропадёт бесследно.
4. Зачем убирать throws Exception в реализации AutoCloseable.close()?
Зачем убирать throws Exception в реализации AutoCloseable.close()?
В интерфейсе метод объявлен как void close() throws Exception, а try-with-resources вызывает close() автоматически, поэтому все исключения из его throws должен обработать окружающий код. Если реализация оставит close() throws Exception, блок try (DbSession s = new DbSession()) { ... } без catch не скомпилируется: unreported exception Exception; must be caught or declared to be thrown. Переопределяющему методу разрешено сужать список исключений, поэтому если ресурс при закрытии не выбрасывает проверяемых исключений, достаточно написать public void close() без throws — и try-with-resources с этим классом перестанет требовать catch (Exception e). Можно также сузить до конкретного типа, например close() throws IOException.
5. Наследник убрал throws у переопределённого метода. Почему при вызове через ссылку на суперкласс компилятор всё равно требует try-catch?
Наследник убрал throws у переопределённого метода. Почему при вызове через ссылку на суперкласс компилятор всё равно требует try-catch?
Проверка throws — работа компилятора, а не JVM: какие исключения нужно обработать, он определяет по объявленному (статическому) типу переменной, а не по реальному объекту. Если Parser.parse() объявлен с throws IOException, а StrictParser.parse() переопределён без throws, то strict.parse("a") через ссылку StrictParser компилируется без try-catch, а тот же объект через Parser p = strict требует catch (IOException e). Во время выполнения в обоих случаях вызывается метод реального объекта. Поэтому сужать throws в подклассе полезно: код, работающий с конкретным классом, избавляется от лишних try-catch.