Исключения и странное наследование

255
03 апреля 2019, 05:50

Всем привет! Заметил такую вещь:

Как все мы прекрасно знаем существует 2 вида Exception:

  1. Checked
  2. Unchecked

Вопрос: Почему RuntimeException (unchecked), наследуется от класса Exception, который является (checked)?

UPGRADE: Исключения необходимо явно объявлять с помощью оператора throws, объявляются все исключения, кроме RuntimeException и Error потому что они UNCHECKED? Как понять явно объявлять?

Answer 1

Короткий ответ

Для того, чтобы конструкция:

catch(Exception e) {
    ...
}

отлавливала и RuntimeException.

Длинный ответ

@Grundy уже объяснил, что RuntimeException явно прописан в спецификации и то, что данная иерархия была выбрана авторами Java и только они смогут достоверно объяснить, почему был выбран именно такой вариант, а не какой-либо другой.

Я попробую объяснить почему выбранная иерархия имеет смысл.

Error и Exception

Начнем издалека. Есть базовый класс Throwable. Все методы, связанные с выбрасыванием исключений, определены в нем. Его наследники, что Error, что Exception, не объявляют никаких специфичных методов. По сути это один и тот же класс продублированный с разными названиями.

Такая иерархия явно отделяет фатальные системные ошибки (Error) от нефатальных исключений (Exception). В теории, разработчик должен обрабатывать только исключения, а при возникновении ошибок, программа должна прекращать работу. Например, такой блок try:

try {
    //какой-то код
} catch(Exception e) {
    //обрабатываем исключение
}

Нормально обработает NullPointerException и продолжит работу. Но если возникнет OutOfMemoryError, то исполнение прекратится и ошибка будет выброшена на уровень выше.

Проверяемые исключения

Разработчики Java приняли решение, довольно спорное, что код должен явно обрабатывать все исключения, которые могут возникнуть. Т.е. если код обращается к методу, который может выбросить исключение, то его нужно либо явно обработать с помощью блока try-catch:

try {
    //метод, который может выбросить IOException
    throwingMethod();
} catch(IOException e) {
    //явно обрабатывается
}

либо явно объявить в вызывающем методе с помощью throws:

//Так мы объявляем всему миру, что наш метод опасный
//и может выбросить исключение.
//Любой код, который вызовет метод, должен будет либо 
//обработать исключение, либо передать дальше.
void myMethod() throws IOException {
    throwingMethod();
}

Если не сделать ни того, ни другого, то код не скомпилируется.

Error — не проверяется, т.к., во-первых, Error не имеет смысла обрабатывать в большинстве случаев, и, во-вторых, ошибки обычно не относятся к какому-то конкретному методу (переполнение памяти может возникнуть в любой момент).

Непроверяемые исключения

Отдельные виды исключений потенциально очень широко распространены. Например, NullPointerException потенциально может возникнуть при вызове почти любого нестатичного метода:

void method(MyClass obj) {
    //здесь может быть NPE
    obj.method();
    //и здесь может быть NPE
    obj.getField().method();
    //и здесь
    showMessage(obj.toString());
}

Явная обработка NullPointerException привела бы к очень неудобному коду, с огромным количеством try catch и throws. Поэтому для таких исключений пришли к компромису и объявили класс RuntimeException.

(Вопрос о том, какие исключения должны наследоваться от RuntimeException — довольно спорный. Есть рекомендации использовать непроверяемые исключения для программных ошибок, но на практике выбор может оказаться сложным)

RuntimeException наследуется от Exception потому-что непроверяемое исключение это все еще исключение, а не ошибка: его можно обработать и он всегда зависит от кода, а не от сторонных, системных, факторов. И если разработчик решит обрабатывать все исключения:

catch(Exception e) {
    //обработка
}

, то должны обрабатываться и возникшие RuntimeException.

Можно ли было сделать по-другому? Да, но есть нюансы. Рассмотрим варианты.

Вариант 1. Разные классы-наследники для проверяемых и непроверяемых исключений.

Можно было бы создать отдельные базовые классы для проверяемых и непроверяемых исключений:

                        Exception
        RuntimeException        CheckedException

Но в этом случае возникает вопрос: должен ли быть проверяемым Exception и его наследники? Если да, то класс CheckedException теряет смысл и получаем существующую иерархию. Если нет, то теряет смысл RuntimeException. Тогда получаем второй вариант

Вариант 2. Проверяемые исключения наследуются от непроверяемых

                        Exception (unchecked)
                        CheckedException  (checked)

Такая иерархия вполне допустима, но разработчики Java решили, что базовый класс должен быть проверяемым. Это соответствует выбранной философии: исключения должны быть проверяемыми по-умолчанию и непроверяемыми только если их повсеместная проверка вызывает затруднения. С этой философией не все согласны, но имеем, что имеем.

Итог

Почему RuntimeException (unchecked), наследуется от класса Exception, который является (checked)?

По следующим причинам:

  • Проверяемость тех или иных классов явно прописана в спецификации и не связана с какими-либо свойствами классов-исключений. Наследование имеет чисто организационный/иллюстративный характер и не связано с наследованием свойств/методов.

  • Проверяемое исключение — это исключение, а не ошибка, и должно обрабатываться как исключение.

  • Разработчики Java приняли решение, что базовый класс исключений должен быть проверяемым.

Answer 2

Это явно определено в спецификации, section 11.1.1:

RuntimeException and all its subclasses are, collectively, the runtime exception classes.

The unchecked exception classes are the runtime exception classes and the error classes.

The checked exception classes are all exception classes other than the unchecked exception classes. That is, the checked exception classes are all subclasses of Throwable other than RuntimeException and its subclasses and Error and its subclasses.

Выше в спецификации явно указывается, что checked exceptions – это все подклассы Throwable за исключением RuntimeException, Error и их подклассов.

То есть RuntimeException и Error – это просто особые случаи для компилятора.

Перевод ответа @JonSkeet

Answer 3

RuntimeException и Error можно не проверять явно, т.е. они не должны быть обёрнуты в try-catch или методы не должны содержать throws для компиляции, в то время как обычные Exception обязаны обрабатываться с помощью try-catch или передаваться через throws.

void neverCompiled(){
  throw new Exception();
}
void compiled(){
 try{
  throw new Exception();
 } catch(Exception e){}
}
void compiled(){
  throw new RuntimeException();
}
class Complex {
    void compiled1() throws Exception {
      throw new Exception();
    }
    void compiled2() {
      try{
        compiled1();
      } catch(Exception e){}
    }
}
READ ALSO
Vuex getter не передает значение в computed свойство

Vuex getter не передает значение в computed свойство

столкнулся с проблемой что геттер для vuex не передает значение

143
Редактирования изображения node.js для бота Discord

Редактирования изображения node.js для бота Discord

Вобщем, у меня есть бот на Discord, мне нужно что бы при заходе в группу этот бот кидал картинку(с зарание подготовленым фоном) с ником человека(+-...

157
Ожидание localhost

Ожидание localhost

Вечер добрый, работаю на Express имеется init для запуска:

196
Как закрыть видео при нажати кнопки назад в браузере?

Как закрыть видео при нажати кнопки назад в браузере?

Можно как-то закрыть видео при нажати кнопки назад в браузере, но чтобы не выйти из страницы?

186