Sneaky Throws – Exceptions aus dem Hinterhalt

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.

Schreibe einen Kommentar