Dlaczego Java nie zezwala na generyczne podklasy Throwable?

146

Według Java Language Sepecification , 3. edycja:

Jest to błąd czasu kompilacji, jeśli klasa ogólna jest bezpośrednią lub pośrednią podklasą klasy Throwable.

Chciałbym zrozumieć, dlaczego podjęto taką decyzję. Co jest nie tak z ogólnymi wyjątkami?

(O ile wiem, typy generyczne są po prostu cukrem składniowym w czasie kompilacji i zostaną przetłumaczone na Objecttakie .classpliki w plikach, więc efektywne zadeklarowanie klasy ogólnej jest tak, jakby wszystko w niej było Object. Popraw mnie, jeśli się mylę .)

Hosam Aly
źródło
1
Argumenty typu ogólnego są zastępowane górną granicą, którą domyślnie jest Object. Jeśli masz coś takiego jak List <? rozszerza A>, a następnie A jest używane w plikach klas.
Torsten Marek
Dziękuję @Torsten. Nie myślałem wcześniej o tym przypadku.
Hosam Aly
2
To dobre pytanie do wywiadu.
skaffman
@TorstenMarek: Jeśli ktoś dzwoni myList.get(i), oczywiście getnadal zwraca Object. Czy kompilator wstawia rzutowanie do A, aby uchwycić część ograniczenia w czasie wykonywania? Jeśli nie, OP ma rację, że ostatecznie sprowadza się do Objects w czasie wykonywania. (Plik klasy z pewnością zawiera metadane dotyczące A, ale to tylko metadane AFAIK.)
Mihai Danila

Odpowiedzi:

155

Jak powiedział Mark, typy nie są reifable, co jest problemem w następującym przypadku:

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

Oba SomeException<Integer>i SomeException<String>są kasowane do tego samego typu, nie ma możliwości, aby maszyna JVM rozróżniła wystąpienia wyjątków, a zatem nie ma możliwości określenia, który catchblok powinien zostać wykonany.

Torsten Marek
źródło
3
ale co oznacza „reifiable”?
aberrant 80
61
Zatem reguła nie powinna brzmieć „typy generyczne nie mogą tworzyć podklasy Throwable”, ale zamiast tego „klauzule catch muszą zawsze używać typów surowych”.
Archie
3
Mogliby po prostu zabronić używania razem dwóch bloków catch tego samego typu. Aby użycie samego SomeExc <Integer> było legalne, tylko używanie razem SomeExc <Integer> i SomeExc <String> byłoby nielegalne. To nie sprawiłoby żadnych problemów, czy nie?
Viliam Búr
3
Och, teraz rozumiem. Moje rozwiązanie spowodowałoby problemy z RuntimeExceptions, których nie trzeba deklarować. Więc jeśli SomeExc jest podklasą RuntimeException, mógłbym zgłosić i jawnie złapać SomeExc <Integer>, ale być może inna funkcja po cichu rzuca SomeExc <String>, a mój blok catch dla SomeExc <Integer> przypadkowo to złapie.
Viliam Búr
4
@ SuperJedi224 - Nie. Robi to dobrze - biorąc pod uwagę ograniczenie, że leki generyczne muszą być kompatybilne wstecz.
Stephen C,
14

Oto prosty przykład użycia wyjątku:

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

Treść instrukcji TRy zgłasza wyjątek z daną wartością, który jest przechwytywany przez klauzulę catch.

Natomiast poniższa definicja nowego wyjątku jest zabroniona, ponieważ tworzy sparametryzowany typ:

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

Próba skompilowania powyższego zgłasza błąd:

% 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

To ograniczenie jest rozsądne, ponieważ prawie każda próba przechwycenia takiego wyjątku musi się nie powieść, ponieważ typu nie można ponowić. Można by się spodziewać, że typowe użycie wyjątku będzie wyglądać następująco:

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

Jest to niedozwolone, ponieważ typu w klauzuli catch nie można ponowić. W chwili pisania tego tekstu kompilator Sun zgłasza kaskadę błędów składniowych w takim przypadku:

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

Ponieważ wyjątki nie mogą być parametryczne, składnia jest ograniczona, więc typ musi być zapisany jako identyfikator, bez następującego parametru.

IAdapter
źródło
2
Co masz na myśli, mówiąc „reifiable”? „reifiable” nie jest słowem.
ForYourOwnGood
1
Sam nie znałem tego słowa, ale szybkie wyszukiwanie w Google dało
Hosam Aly
13

Dzieje się tak zasadniczo dlatego, że został zaprojektowany w zły sposób.

Ten problem uniemożliwia czysty abstrakcyjny projekt, np.

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

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

Fakt, że klauzula catch zawiodłaby dla typów ogólnych, nie jest reifikowana, nie jest dla tego usprawiedliwieniem. Kompilator może po prostu zabronić konkretnych typów ogólnych, które rozszerzają Throwable lub nie zezwalać na typy ogólne wewnątrz klauzul catch.

Michele Sollecito
źródło
+1. moja odpowiedź - stackoverflow.com/questions/30759692/…
ZhongYu
1
Jedynym sposobem, w jaki mogliby to lepiej zaprojektować, było uczynienie niekompatybilnego kodu klienta przez około 10 lat. To była uzasadniona decyzja biznesowa. Projekt był poprawny ... biorąc pod uwagę kontekst .
Stephen C,
1
Jak więc złapiesz ten wyjątek? Jedynym sposobem, który by to zadziałał, jest złapanie surowego typu EntityNotFoundException. Ale to uczyniłoby generyczne bezużytecznymi.
Frans
4

Typy generyczne są sprawdzane w czasie kompilacji pod kątem poprawności typu. Informacje o typie ogólnym są następnie usuwane w procesie zwanym wymazywaniem typu . Na przykład List<Integer>zostanie przekonwertowany na typ nieogólny List.

Ze względu na wymazywanie typu nie można określić parametrów typu w czasie wykonywania.

Załóżmy, że możesz rozszerzyć w Throwableten sposób:

public class GenericException<T> extends Throwable

Rozważmy teraz następujący kod:

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

Ze względu na wymazywanie typu środowisko wykonawcze nie będzie wiedział, który blok catch wykonać.

Dlatego jest to błąd czasu kompilacji, jeśli klasa ogólna jest bezpośrednią lub pośrednią podklasą Throwable.

Źródło: problemy z wymazywaniem typu

prześc
źródło
Dzięki. To ta sama odpowiedź, którą podał Torsten .
Hosam Aly
Nie, nie jest. Odpowiedź Torstena nie pomogła mi, ponieważ nie wyjaśniła, czym jest wymazywanie / reifikacja typu.
Good Night Nerd Pride
2

Spodziewałbym się, że to dlatego, że nie ma możliwości zagwarantowania parametryzacji. Rozważ następujący kod:

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

Jak zauważyłeś, parametryzacja to tylko cukier syntaktyczny. Jednak kompilator próbuje zapewnić spójność parametryzacji we wszystkich odwołaniach do obiektu w zakresie kompilacji. W przypadku wyjątku kompilator nie ma możliwości zagwarantowania, że ​​MyException jest wyrzucany tylko z zakresu, który przetwarza.

kdgregory
źródło
Tak, ale dlaczego w takim razie nie jest oznaczony jako „niebezpieczny”, jak na przykład w przypadku rzutów?
eljenso
Ponieważ za pomocą rzutowania mówisz kompilatorowi „Wiem, że ta ścieżka wykonania daje oczekiwany wynik”. Z wyjątkiem nie możesz powiedzieć (dla wszystkich możliwych wyjątków) „Wiem, gdzie to zostało rzucone”. Ale, jak powiedziałem powyżej, to przypuszczenie; Mnie tam nie było.
kdgregory
„Wiem, że ta ścieżka wykonania daje oczekiwany rezultat”. Nie wiesz, masz taką nadzieję. Dlatego generyczne i obniżone są statycznie niebezpieczne, ale mimo to są dozwolone. Głosowałem za odpowiedzią Torstena, ponieważ tam widzę problem. Tutaj nie mam.
eljenso
Jeśli nie wiesz, że obiekt jest określonego typu, nie powinieneś go rzucać. Cała idea rzutowania polega na tym, że masz więcej wiedzy niż kompilator i czyni tę wiedzę jawnie częścią kodu.
kdgregory
Tak, i tutaj możesz mieć więcej wiedzy niż kompilator, ponieważ chcesz wykonać niesprawdzoną konwersję z MyException na MyException <Foo>. Może "wiesz", że będzie to MyException <Foo>.
eljenso