Die Einführung der Generics in Java 8 hat zur einigen eigentümlichen Konsequenzen geführt. Die meisten dieser Konsequenzen fußen auf dem Konzept der Type Erasure. Die Generics sind nur Bestandteil der Java Sprache und werden demzufolge nur vom Java Compiler beachtet. Der compilierte Code kennt, salopp gesprochen, keine generische Typen sondern operiert auf dem vermuteten Basistypen der Generics.
Eine Konsequenz aus der Type Erasure ist u.a., dass Instanzen vom falschen Typ in einer generischen Liste landen können. Dies ist zwar mit einem Trick verbunden, aber der ist leicht gefunden, denn Generics werden auch heute noch vielfach falsch verwendet.
Eine weitere groteske Konsequenz aus der Type Erause bietet das Werfen von generischen Exceptions. Die folgende Methode bietet die Möglichkeit beliebige Exceptions zu werfen.
public static <E extends Exception> void sneakyThrow(Exception e) throws E { throw (E) e; }
Der Parameter vom Typ Exception
wird auf den generischen Type E
gecastet und dann geworfen. Wird diese Methode in der folgenden Test Methode mit einer IOException
aufgerufen, dann wird eine IOException
geworfen.
@Test void testSneakyThrow() { sneakyThrow(new IOException()); }
Der aufmerksame Leser wird es sofort bemerkt haben. Obwohl die IOException
eine Checked Exception ist, wird sie weder gefangen noch in einer throws
Klausel angegeben. Sie fliegt völlig unbemerkt bis zur nächsten Stelle an der Exception
oder Throwable
gefangen werden.
Ein anderes Beispiel, dass noch etwas harmloser aussieht, zeigt das gleiche Verhalten.
private interface Holder { <T, E extends Throwable> T hold(String name) throws E; } private static class FileHolder implements Holder { @Override public File hold(String name) throws IOException { File file = new File(name); if (!file.exists()) { throw new IOException(); } return file; } } @Test void fileHolder() throws IOException { FileHolder holder = new FileHolder(); holder.hold("test"); } @Test void fileHolderByHolder() { Holder holder = new FileHolder(); holder.hold("test"); }
Der FileHolder
implementiert die generische Methode von Holder
und wirft dabei die IOException
. Verwendet der Unit Test den Typ FileHolder
, dann muss eine throws
Klausel angegeben werden, verwendet der Unit Test den Typ Holder, dann kann die Klausel entfallen.
Ursache für dieses eigenartige Verhalten ist, dass der Java Compiler bei der Type Erasure den generischen Exception Typ E
durch die RuntimeException
ersetzt. Außerdem ist es nur der Java Compiler, der sich für throws
Klauseln und Checked Exceptions interessiert. Der Java Virtual Machine, auf der später der erzeugte Byte Code läuft, sind diese Konzepte völlig egal.
Die Bibliothek Lombok stellt eigens die Annotation @SneakyThrows
bereit um Checked Exceptions auch an ungünstigen Stellen werfen zu können. Im Beitrag Fehlerbehandlung aus der Hölle wird bereits erklärt, warum das Werfen und Fangen von Exception
zu unterlassen ist. Zum einen können sich beim Fangen von Exception
neue Exceptions einschleichen, die an dieser Stelle nicht behandelt werden sollten oder Dank der globalen Fehlerbehandlung nicht einmal vom Entwickler bemerkt werden. Zum anderen sorgt das Werfen von Exception
, dass an immer mehr Stellen Exception
gefangen oder geworfen wird.
Die @SneakyThrows
Annotation macht das alles noch viel schlimmer, weil nun an vielen Stellen nicht einmal mehr sicher ist, ob Checked Exceptions geworfen werden.