Warum erlaubt Java keine generischen Unterklassen von Throwable?

146

Gemäß der Java Language Sepecification , 3. Auflage:

Es ist ein Fehler zur Kompilierungszeit, wenn eine generische Klasse eine direkte oder indirekte Unterklasse von ist Throwable.

Ich möchte verstehen, warum diese Entscheidung getroffen wurde. Was ist los mit generischen Ausnahmen?

(Soweit ich weiß, handelt es sich bei Generika lediglich um syntaktischen Zucker zur Kompilierungszeit, und sie werden Objectohnehin in die .classDateien übersetzt. Eine effektive Deklaration einer generischen Klasse ist also so, als ob alles darin eine wäre Object. Bitte korrigieren Sie mich, wenn ich falsch liege .)

Hosam Aly
quelle
1
Generische Typargumente werden durch die Obergrenze ersetzt, die standardmäßig Object ist. Wenn Sie so etwas wie Liste <haben? erweitert A>, dann wird A in den Klassendateien verwendet.
Torsten Marek
Danke @Torsten. An diesen Fall habe ich vorher nicht gedacht.
Hosam Aly
2
Es ist eine gute Interviewfrage, diese.
Skaffman
@TorstenMarek: Wenn man anruft myList.get(i), gibt man natürlich getimmer noch ein zurück Object. Fügt der Compiler eine Umwandlung Ain ein, um einen Teil der Einschränkung zur Laufzeit zu erfassen? Wenn nicht, hat das OP Recht, dass es am Ende zur ObjectLaufzeit auf s hinausläuft. (Die Klassendatei enthält sicherlich Metadaten über A, aber es sind nur Metadaten AFAIK.)
Mihai Danila

Antworten:

155

Wie bereits erwähnt, sind die Typen nicht überprüfbar, was im folgenden Fall ein Problem darstellt:

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

Beide SomeException<Integer>und SomeException<String>werden für denselben Typ gelöscht. Die JVM kann die Ausnahmeinstanzen nicht unterscheiden und daher nicht festlegen, welcher catchBlock ausgeführt werden soll.

Torsten Marek
quelle
3
aber was bedeutet "reifizierbar"?
aberrant80
61
Die Regel sollte also nicht "generische Typen können Throwable nicht unterordnen" lauten, sondern "catch-Klauseln müssen immer rohe Typen verwenden".
Archie
3
Sie könnten es einfach verbieten, zwei Fangblöcke mit demselben Typ zusammen zu verwenden. Damit die Verwendung von SomeExc <Integer> allein legal wäre, wäre es illegal, nur SomeExc <Integer> und SomeExc <String> zusammen zu verwenden. Das würde keine Probleme machen, oder?
Viliam Búr
3
Oh, jetzt verstehe ich es. Meine Lösung würde Probleme mit RuntimeExceptions verursachen, die nicht deklariert werden müssen. Wenn SomeExc eine Unterklasse von RuntimeException ist, könnte ich SomeExc <Integer> auslösen und explizit abfangen, aber möglicherweise löst eine andere Funktion stillschweigend SomeExc <String> aus, und mein catch-Block für SomeExc <Integer> würde dies ebenfalls versehentlich abfangen.
Viliam Búr
4
@ SuperJedi224 - Nein , es tut sie Recht - angesichts der Einschränkung , dass Generika sein mußte rückwärts kompatibel.
Stephen C
14

Hier ist ein einfaches Beispiel für die Verwendung der Ausnahme:

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

Der Hauptteil der TRy-Anweisung löst die Ausnahme mit einem bestimmten Wert aus, der von der catch-Klausel abgefangen wird.

Im Gegensatz dazu ist die folgende Definition einer neuen Ausnahme verboten, da dadurch ein parametrisierter Typ erstellt wird:

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

Ein Versuch, die oben genannten zu kompilieren, meldet einen Fehler:

% 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

Diese Einschränkung ist sinnvoll, da fast jeder Versuch, eine solche Ausnahme abzufangen, fehlschlagen muss, da der Typ nicht überprüfbar ist. Man könnte erwarten, dass eine typische Verwendung der Ausnahme etwa die folgende ist:

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

Dies ist nicht zulässig, da der Typ in der catch-Klausel nicht überprüfbar ist. Zum Zeitpunkt dieses Schreibens meldet der Sun-Compiler in einem solchen Fall eine Kaskade von Syntaxfehlern:

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

Da Ausnahmen nicht parametrisch sein können, ist die Syntax eingeschränkt, sodass der Typ als Bezeichner ohne folgenden Parameter geschrieben werden muss.

IAdapter
quelle
2
Was meinst du mit "reifizierbar"? 'reifizierbar' ist kein Wort.
ForYourOwnGood
1
Ich kannte das Wort selbst nicht, aber eine schnelle Suche in Google brachte mir Folgendes
Hosam Aly
13

Es ist im Wesentlichen, weil es schlecht entworfen wurde.

Dieses Problem verhindert ein sauberes abstraktes Design, z.

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

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

Die Tatsache, dass eine Fangklausel für Generika fehlschlagen würde, ist keine Entschuldigung dafür. Der Compiler kann einfach konkrete generische Typen, die Throwable erweitern, verbieten oder Generika innerhalb von catch-Klauseln nicht zulassen.

Michele Sollecito
quelle
+1. meine Antwort - stackoverflow.com/questions/30759692/…
ZhongYu
1
Die einzige Möglichkeit, es besser zu gestalten, bestand darin, ~ 10 Jahre Kundencode inkompatibel zu machen. Das war eine tragfähige Geschäftsentscheidung. Das Design war korrekt ... angesichts des Kontexts .
Stephen C
1
Wie werden Sie diese Ausnahme abfangen? Der einzige Weg, der funktionieren würde, besteht darin, den Rohtyp zu fangen EntityNotFoundException. Aber das würde die Generika unbrauchbar machen.
Frans
4

Generika werden zur Kompilierungszeit auf Typkorrektheit überprüft. Die generischen Typinformationen werden dann in einem Prozess entfernt, der als Typlöschung bezeichnet wird . Zum Beispiel List<Integer>wird auf den nicht-generischen Typen umgewandelt werden List.

Aufgrund der Typlöschung können zur Laufzeit keine Typparameter ermittelt werden.

Nehmen wir an, Sie dürfen so erweitern Throwable:

public class GenericException<T> extends Throwable

Betrachten wir nun den folgenden Code:

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

Aufgrund der Löschung des Typs weiß die Laufzeit nicht, welcher Catch-Block ausgeführt werden soll.

Daher ist es ein Fehler zur Kompilierungszeit, wenn eine generische Klasse eine direkte oder indirekte Unterklasse von Throwable ist.

Quelle: Probleme beim Löschen des Typs

outdev
quelle
Vielen Dank. Dies ist die gleiche Antwort wie die von Torsten .
Hosam Aly
Nein, ist es nicht. Torstens Antwort hat mir nicht geholfen, weil sie nicht erklärt hat, was für eine Art Löschung / Verdinglichung es ist.
Gute Nacht Nerd Pride
2

Ich würde erwarten, dass es keine Möglichkeit gibt, die Parametrisierung zu garantieren. Betrachten Sie den folgenden Code:

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

Wie Sie bemerken, ist Parametrisierung nur syntaktischer Zucker. Der Compiler versucht jedoch sicherzustellen, dass die Parametrisierung über alle Verweise auf ein Objekt im Kompilierungsbereich hinweg konsistent bleibt. Im Falle einer Ausnahme kann der Compiler nicht garantieren, dass MyException nur aus einem Bereich ausgelöst wird, den er verarbeitet.

kdgregory
quelle
Ja, aber warum wird es dann nicht als "unsicher" gekennzeichnet, wie zum Beispiel bei Casts?
Eljenso
Denn mit einer Besetzung sagen Sie dem Compiler: "Ich weiß, dass dieser Ausführungspfad das erwartete Ergebnis liefert." Mit einer Ausnahme kann man nicht sagen (für alle möglichen Ausnahmen) "Ich weiß, wohin das geworfen wurde." Aber wie ich oben sage, ist es eine Vermutung; Ich war nicht da
kdgregory
"Ich weiß, dass dieser Ausführungspfad das erwartete Ergebnis liefert." Sie wissen es nicht, Sie hoffen es. Das ist der Grund, warum Generika und Downcasts statisch unsicher sind, aber dennoch zulässig sind. Ich habe Torstens Antwort positiv bewertet, weil ich dort das Problem sehe. Hier tue ich nicht.
Eljenso
Wenn Sie nicht wissen, dass ein Objekt von einem bestimmten Typ ist, sollten Sie es nicht umwandeln. Die ganze Idee einer Besetzung ist, dass Sie mehr Wissen als der Compiler haben und dieses Wissen explizit Teil des Codes machen.
kdgregory
Ja, und hier haben Sie möglicherweise auch mehr Wissen als der Compiler, da Sie eine ungeprüfte Konvertierung von MyException zu MyException <Foo> durchführen möchten. Vielleicht "wissen" Sie, dass es sich um eine MyException <Foo> handelt.
Eljenso