Почему перехват проверенных исключений разрешен для кода, который не генерирует исключения?

В Java методы, которые выдают проверенные исключения ( Exception или его подтипы - IOException, InterruptedException и т. Д.), Должны объявлять оператор throws:

public abstract int read() throws IOException;

Методы, которые не объявляют throws оператор не может генерировать проверенные исключения.

public int read() { // does not compile
    throw new IOException();
}
// Error: unreported exception java.io.IOException; must be caught or declared to be thrown

Но перехват проверенных исключений в безопасных методах все еще разрешен в Java:

public void safeMethod() { System.out.println("I'm safe"); }

public void test() { // method guarantees not to throw checked exceptions
    try {
        safeMethod();
    } catch (Exception e) { // catching checked exception java.lang.Exception
        throw e; // so I can throw... a checked Exception?
    }
}

Вообще-то, нет. Это немного забавно: компилятор знает, что e не является проверенным исключением, и позволяет отбросить его. Вещи даже немного смешны, этот код не компилируется:

public void test() { // guarantees not to throw checked exceptions
    try {
        safeMethod();
    } catch (Exception e) {        
        throw (Exception) e; // seriously?
    }
}
// Error: unreported exception java.lang.Exception; must be caught or declared to be thrown

Первый фрагмент был мотивом для вопроса.

Компилятор знает, что проверенные исключения не могут быть выброшены в безопасный метод - поэтому, возможно, он должен позволять перехватывать только непроверенные исключения?


Возвращаясь к основному вопросу - есть ли причины для реализации перехвата проверенных исключений таким образом? Это просто недостаток в дизайне или я упускаю некоторые важные факторы - возможно, обратную несовместимость? Что может пойти не так, если только RuntimeException было разрешено быть пойманным в этом сценарии? Примеры очень ценятся.

3 ответа

Решение

Цитирование спецификации языка Java, §11.2.3:

Это ошибка времени компиляции, если предложение catch может перехватить проверенный класс исключений E1, и это не тот случай, когда блок try, соответствующий предложению catch, может выбросить проверенный класс исключений, который является подклассом или суперклассом E1, если только E1 не является Исключение или суперкласс Исключения.

Я предполагаю, что это правило возникло задолго до Java 7, где не было многократных ловушек. Поэтому, если у вас был try блок, который может генерировать множество исключений, самый простой способ поймать все - это поймать общий суперкласс (в худшем случае Exception, или же Throwable если хочешь поймать Error а также).

Обратите внимание, что вы не можете поймать тип исключения, который совершенно не связан с тем, что на самом деле выдается - в вашем примере, перехват любого подкласса Throwable это не RuntimeException будет ошибка:

try {
    System.out.println("hello");
} catch (IOException e) {  // compilation error
    e.printStackTrace();
}


Редактирование OP: Основная часть ответа заключается в том, что примеры вопросов работают только для класса исключений. Обычно отлов проверенных исключений не допускается в случайных местах кода. Извините, если я кого-то запутал, используя эти примеры.

В Java 7 введена более инклюзивная проверка типов исключений.

Однако в Java SE 7 вы можете указать типы исключений FirstException и SecondException в предложении throws в объявлении метода rethrowException. Компилятор Java SE 7 может определить, что исключение, генерируемое оператором throw e, должно быть получено из блока try, и единственными исключениями, генерируемыми блоком try, могут быть FirstException и SecondException.

Этот отрывок говорит о try блок который конкретно кидает FirstException а также SecondException; хотя catch броски блока Exception, метод должен только объявить, что он бросает FirstException а также SecondExceptionне Exception:

public void rethrowException(String exceptionName)
 throws FirstException, SecondException {
   try {
     // ...
   }
   catch (Exception e) {
     throw e;
   }
 }

Это означает, что компилятор может обнаружить, что выбрасываются только возможные типы исключений test являются Errorс или RuntimeExceptions, ни один из которых не должен быть пойман. Когда ты throw e;, это может сказать, даже когда статический тип Exception, что это не должно быть объявлено или повторно поймано.

Но когда вы бросаете его Exceptionэто обходит эту логику. Теперь компилятор воспринимает это как обычный Exception который должен быть пойман или объявлен.

Основная причина добавления этой логики в компилятор состояла в том, чтобы позволить программисту указывать только определенные подтипы в throws пункт при свержении генерала Exception ловить эти конкретные подтипы. Тем не менее, в этом случае это позволяет поймать общий Exception и не должны объявлять какие-либо исключения в throws предложение, потому что никакие определенные типы, которые могут быть выброшены, не являются проверенными исключениями.

Проблема здесь заключается в том, что проверенные / непроверенные ограничения исключений влияют на то, что вашему коду разрешено генерировать, а не на то, что ему разрешено ловить. Хотя вы все еще можете поймать любой тип Exceptionединственные, кого вам разрешено снова бросать, это непроверенные. (Вот почему преобразование вашего непроверенного исключения в проверенное исключение нарушает ваш код.)

Поймать непроверенное исключение с Exception допустимо, потому что непроверенные исключения (ака RuntimeExceptions) является подклассом исключения, и он следует стандартным правилам полиморфизма; это не превращает пойманное исключение в ExceptionБудучи хранителем String в Object не превращает String в Object, Полиморфизм означает, что переменная, которая может содержать Object может содержать все, что происходит от Object (такой как String). Точно так же, как Exception суперкласс всех типов исключений, переменная типа Exception может содержать любой класс, производный от Exceptionбез превращения объекта в Exception, Учти это:

import java.lang.*;
// ...
public String iReturnAString() { return "Consider this!"; }
// ...
Object o = iReturnAString();

Несмотря на то, что тип переменной Object, o до сих пор хранит Stringне так ли? Аналогично, в вашем коде:

try {
    safeMethod();
} catch (Exception e) { // catching checked exception
    throw e; // so I can throw... a checked Exception?
}

Что это означает на самом деле "поймать что-нибудь совместимое с классом Exception (т.е. Exception и все, что происходит от него)."Подобная логика используется и в других языках; например, в C++, перехватывая std::exception также будет ловить std::runtime_error, std::logic_error, std::bad_allocлюбые правильно определенные пользовательские исключения и т. д., поскольку все они происходят из std::exception,

tl;dr: Вы не ловите проверенные исключения, вы ловите любые исключения. Исключение становится проверенным, только если вы преобразуете его в проверенный тип исключения.

Другие вопросы по тегам