Чому Java не допускає загальні підкласи Throwable?


146

Відповідно до специфікації мови Java , 3-е видання:

Це помилка часу компіляції, якщо загальний клас є прямим або непрямим підкласом Throwable.

Я хочу зрозуміти, чому було прийнято таке рішення. Що не так із загальними винятками?

(Наскільки я знаю, дженерики - це просто синтаксичний цукор за часом компіляції, і вони будуть переведені у Objectбудь-який .classфайл у файли, тому ефективне оголошення загального класу - це як би все в ньому Object. Будь ласка, виправте мене, якщо я помиляюся .)


1
Аргументи загального типу замінюються верхньою межею, яка за замовчуванням є Object. Якщо у вас є щось на зразок Список <? розширює A>, тоді A використовується у файлах класу.
Торстен Марек

Дякую @Torsten Я не думав про цей випадок раніше.
Хосам Алі

2
Це гарне запитання про інтерв'ю, це.
skaffman

@TorstenMarek: Якщо хтось телефонує myList.get(i), очевидно, getвсе одно повертає Object. Чи вкладає компілятор команду для Aтого, щоб захопити частину обмежень під час виконання? Якщо ні, то ОП правильно, що врешті-решт воно зводиться до Objects під час виконання. (Файл класу, безумовно, містить метадані про A, але це лише метадані AFAIK.)
Mihai Danila

Відповіді:


155

Як було сказано в марці, типи не підлягають повторенню, що є проблемою в наступному випадку:

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

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


3
але що означає "повторюваний"?
aberrant80

61
Таким чином, правило не повинно бути "загальні типи не можуть підклас Throwable", а натомість "пропозиції лову повинні завжди використовувати необроблені типи".
Арчі

3
Вони могли просто заборонити використовувати два блоки спіймання одного типу. Так що використання лише SomeExc <Integer> було б законним, лише використання SomeExc <Integer> і SomeExc <String> було б незаконним. Це не складе проблем, чи не так?
Viliam Búr

3
О, тепер я зрозумів. Моє рішення спричинило б проблеми з RuntimeExceptions, які не потрібно декларувати. Отже, якщо SomeExc є підкласом RuntimeException, я можу кинути та явно зловити SomeExc <Integer>, але, можливо, якась інша функція мовчки кидає SomeExc <String> і мій блок лову для SomeExc <Integer> також випадково спіймає це.
Viliam Búr

4
@ SuperJedi224 - Ні. Це робить їх правильно - зважаючи на обмеження, що дженерики повинні були бути сумісними назад.
Stephen C

14

Ось простий приклад використання винятку:

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

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

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

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;
    }
  }
}

Це не дозволено, оскільки тип у пункті вилову не підлягає повторенню. Під час написання цього запису компілятор 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' - це не слово.
ForYourOwnGood

1
Я сам не знав цього слова, але швидкий пошук у google отримав мені це: java.sun.com/docs/books/jls/third_edition/html/…
Aly


13

По суті це тому, що він був розроблений погано.

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

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

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

Те, що застереження про помилку не вдалося б змінити для генериків, не переосмислюється - це не привід для цього. Компілятор може просто заборонити конкретні загальні типи, які розширюють Throwable або забороняють генеричні дані в рамках застережень.


+1. моя відповідь - stackoverflow.com/questions/30759692 / ...
Zhongyu

1
Єдиний спосіб, який вони могли краще спроектувати, це зробило ~ 10 років код клієнта несумісним. Це було прийнятним бізнес-рішенням. Дизайн був правильним ... враховуючи контекст .
Stephen C

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");
}

Через стирання типу , час виконання не буде знати, який блок лову потрібно виконати.

Тому це помилка часу компіляції, якщо загальний клас є прямим або непрямим підкласом Throwable.

Джерело: Проблеми зі стиранням типу


Дякую. Це та сама відповідь, що та, яку надав Торстен .
Хосам Алі

Ні це не так. Відповідь Торстена мені не допомогла, оскільки вона не пояснила, що таке стирання / реіфікація.
Доброї ночі 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.