Почему Java не допускает общие подклассы Throwable?


146

Согласно спецификации языка Java , 3-е издание:

Это ошибка времени компиляции, если универсальный класс является прямым или косвенным подклассом Throwable.

Я хочу понять, почему это решение было принято. Что не так с общими исключениями?

(Насколько я знаю, дженерики - это просто синтаксический сахар во время компиляции, и они в Objectлюбом случае будут преобразованы в .classфайлы, поэтому объявление обобщения дженериков эффективно, как если бы все в нем было Object. Пожалуйста, исправьте меня, если я ошибаюсь .)


1
Аргументы универсального типа заменяются верхней границей, которая по умолчанию является Object. Если у вас есть что-то вроде List <? расширяет A>, затем A используется в файлах классов.
Торстен Марек

Спасибо @Torsten. Я не думал об этом случае раньше.
Хосам Али

2
Это хороший вопрос для интервью.
Скаффман

@TorstenMarek: Если кто-то вызывает myList.get(i), очевидно, getвсе еще возвращает Object. Вставляет ли компилятор приведение к A, чтобы захватить некоторые ограничения во время выполнения? Если нет, то OP прав, что в итоге он сводится к Objects во время выполнения. (Файл класса, безусловно, содержит метаданные A, но это только метаданные AFAIK.)
Михай Данила

Ответы:


155

Как сказал Марк, типы не являются reifiable, что является проблемой в следующем случае:

try {
   doSomeStuff();
} catch (SomeException<Integer> e) {
   // ignore that
} catch (SomeException<String> e) {
   crashAndBurn()
}

Оба SomeException<Integer>и SomeException<String>стерты до одного и того же типа, у JVM нет способа различить экземпляры исключений и, следовательно, нет способа определить, какой catchблок должен быть выполнен.


3
но что значит «переоснащение»?
aberrant80

61
Таким образом, правило не должно быть «универсальные типы не могут наследовать Throwable», а вместо этого «предложения catch всегда должны использовать необработанные типы».
Арчи

3
Они могут просто запретить использование двух блоков catch одного типа вместе. Так что использование SomeExc <Integer> само по себе было бы законным, только использование SomeExc <Integer> и SomeExc <String> вместе было бы недопустимым. Это не составило бы проблем, или так?
Вильям Бур

3
О, теперь я понял. Мое решение может вызвать проблемы с исключениями RuntimeException, которые не нужно объявлять. Поэтому, если SomeExc является подклассом RuntimeException, я мог бы бросить и явно перехватить SomeExc <Integer>, но, может быть, какая-то другая функция молча выбрасывает SomeExc <String>, и мой блок catch для SomeExc <Integer> тоже случайно его перехватит.
Вильям Бур

4
@ SuperJedi224 - Нет. Это делает их правильно - учитывая ограничение, что дженерики должны быть обратно совместимы.
Стивен С.

14

Вот простой пример того, как использовать исключение:

class IntegerExceptionTest {
  public static void main(String[] args) {
    try {
      throw new IntegerException(42);
    } catch (IntegerException e) {
      assert e.getValue() == 42;
    }
  }
}

Тело оператора TRy выдает исключение с заданным значением, которое перехватывается предложением catch.

Напротив, следующее определение нового исключения запрещено, поскольку оно создает параметризованный тип:

class ParametricException<T> extends Exception {  // compile-time error
  private final T value;
  public ParametricException(T value) { this.value = value; }
  public T getValue() { return value; }
}

Попытка скомпилировать вышесказанное сообщает об ошибке:

% javac ParametricException.java
ParametricException.java:1: a generic class may not extend
java.lang.Throwable
class ParametricException<T> extends Exception {  // compile-time error
                                     ^
1 error

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

class ParametricExceptionTest {
  public static void main(String[] args) {
    try {
      throw new ParametricException<Integer>(42);
    } catch (ParametricException<Integer> e) {  // compile-time error
      assert e.getValue()==42;
    }
  }
}

Это недопустимо, потому что тип в предложении catch не является пригодным для повторного использования. На момент написания этой статьи компилятор Sun сообщал о каскаде синтаксических ошибок в таком случае:

% javac ParametricExceptionTest.java
ParametricExceptionTest.java:5: <identifier> expected
    } catch (ParametricException<Integer> e) {
                                ^
ParametricExceptionTest.java:8: ')' expected
  }
  ^
ParametricExceptionTest.java:9: '}' expected
}
 ^
3 errors

Поскольку исключения не могут быть параметрическими, синтаксис ограничен, поэтому тип должен быть записан как идентификатор без следующего параметра.


2
Что вы имеете в виду, когда говорите «reifiable»? «reifiable» - это не слово.
ForYourOwnGood

1
Я сам не знал этого слова, но быстрый поиск в Google дал
Хосам Али


13

По сути, потому что он был спроектирован плохо.

Эта проблема предотвращает чистый абстрактный дизайн, например,

public interface Repository<ID, E extends Entity<ID>> {

    E getById(ID id) throws EntityNotFoundException<E, ID>;
}

Тот факт, что предложение catch для дженериков не выполнено, не является оправданием. Компилятор может просто запретить конкретные обобщенные типы, которые расширяют Throwable или запрещать обобщенные типы внутри предложений catch.



1
Единственный способ, которым они могли бы спроектировать его лучше, это сделать ~ 10 лет кода клиента несовместимым. Это было жизнеспособное деловое решение. Дизайн был правильным ... учитывая контекст .
Стивен С

1
Так как вы поймаете это исключение? Единственный способ, который сработает, - поймать необработанный тип EntityNotFoundException. Но это сделает дженерики бесполезными.
Франс

4

Обобщения проверяются во время компиляции на корректность типов. Информация об общем типе затем удаляется в процессе, называемом стиранием типа . Например, List<Integer>будет преобразован в неуниверсальный тип List.

Из-за стирания типа параметры типа не могут быть определены во время выполнения.

Предположим, вам разрешено расширяться Throwableследующим образом:

public class GenericException<T> extends Throwable

Теперь давайте рассмотрим следующий код:

try {
    throw new GenericException<Integer>();
}
catch(GenericException<Integer> e) {
    System.err.println("Integer");
}
catch(GenericException<String> e) {
    System.err.println("String");
}

Из-за стирания типа среда выполнения не будет знать, какой блок catch выполнить.

Поэтому это ошибка времени компиляции, если универсальный класс является прямым или косвенным подклассом Throwable.

Источник: Проблемы с стиранием типов


Спасибо. Это тот же ответ, который дал Торстен .
Хосам Али

Нет, это не так. Ответ Торстена мне не помог, потому что он не объяснял, что такое стирание типа / reification.
Спокойной ночи Nerd Pride

2

Я ожидаю, что это потому, что нет способа гарантировать параметризацию. Рассмотрим следующий код:

try
{
    doSomethingThatCanThrow();
}
catch (MyException<Foo> e)
{
    // handle it
}

Как вы заметили, параметризация - это просто синтаксический сахар. Тем не менее, компилятор пытается обеспечить согласованность параметров во всех ссылках на объект в области компиляции. В случае исключения компилятор не может гарантировать, что MyException выбрасывается только из области, которую он обрабатывает.


Да, но почему тогда он не помечается как «небезопасный», как, например, при приведении типов?
eljenso

Потому что при приведении вы говорите компилятору: «Я знаю, что этот путь выполнения дает ожидаемый результат». За исключением, вы не можете сказать (для всех возможных исключений) «Я знаю, где это было брошено». Но, как я уже сказал выше, это предположение; Меня там не было
kdgregory

«Я знаю, что этот путь выполнения дает ожидаемый результат». Вы не знаете, вы надеетесь на это. Вот почему дженерики и откаты статически небезопасны, но, тем не менее, разрешены. Я проголосовал за ответ Торстена, потому что там я вижу проблему. Здесь я не
eljenso

Если вы не знаете, что объект относится к определенному типу, вам не следует его приводить. Идея приведения в том, что у вас больше знаний, чем у компилятора, и вы делаете эти знания явным образом частью кода.
kdgregory

Да, и здесь у вас может быть больше знаний, чем у компилятора, поскольку вы хотите выполнить неконтролируемое преобразование из MyException в MyException <Foo>. Может быть, вы «знаете», что это будет MyException <Foo>.
eljenso
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.