Wo der Unterschied liegt

Extreme Java: „try-with-resource“ in Plain Java

Dr. Heinz Kabutz

© Ksw Photographer | Shutterstock.com

Vor ein paar Monaten hat ein Teilnehmer unseres Kurses „Refactoring to Java 8 Streams“ diese Frage gestellt: Warum fügt try-with-resource keine unterdrückte NullPointerException hinzu, wenn die in try() deklarierte Ressource null ist? Eine interessante Frage. Als ich nach einer Antwort gesucht habe, ist mir etwas klar geworden.

Treffen Sie Dr. Heinz Kabutz auf dem Extreme Java Camp!
Das Extreme Java Camp besteht aus zwei Intensivseminaren, die umfassendes und aktuellstes Know-how zu fortgeschrittenen Java-Themen und zu Java Concurrency Performance vermitteln. Es ist ein einzigartiges Hands-on-Training, in dem auch die erfahrensten Java-Profis intensiv angeregt und gefordert werden.Infos unter www.extreme-java-camp.de

Ich hatte nicht wirklich eine Ahnung, wie der Java-Code aussähe, wenn ich ihn nicht mit try-with-resource vereinfachte. Ich habe mich mit javap -c durchgeboxt, bis meine manuelle und die automatische Version gleich waren.

Fangen wir mit der try-with-resource-Version an:

try (InputStream io = getNullStream()) {
  FileInputStream fi = new FileInputStream("somefile.bin");
  io.available();
}

Die Methode getNullStream() liefert null zurück. somefile.bin existiert nicht. Entsprechend habe ich mit einer FileNotFoundException mit einer unterdrückten NullPointerException gerechnet. Ich habe gedacht der generierte Bytecode sei äquivalent zu diesem Listing.

InputStream io = getNullStream();
Throwable throwable = null;
try {
  FileInputStream fi = new FileInputStream("somefile.bin");
  io.available();
} catch (Throwable t) {
  throwable = t;
  throw t;
} finally {
  if (throwable == null) {
    io.close();
  } else {
    try {
      io.close();
    } catch (Throwable th) { // NullPointerException
      throwable.addSuppressed(th);
    }
  }
}

Ich habe den Code nach oben und nach unten verschoben und einen Test eingebaut, ob io != null ist. Anschließend habe ich einen einfachen Java-Code gefunden, der dem Java 7 try-with-resource-Konstrukt entspricht.

Allerdings sollten wir im Hinterkopf behalten, dass es einen Test gibt und io null ist, bevor wir versuchen, zu schließen.

InputStream io = getNullStream();
Throwable throwable = null;
try {
  FileInputStream fi = new FileInputStream("somefile.bin");
  io.available();
} catch (Throwable t) {
  throwable = t;
  throw t;
} finally {
  if (io != null) {
    if (throwable != null) {
      try {
        io.close();
      } catch (Throwable t) {
        throwable.addSuppressed(t);
      }
    } else {
      io.close();
    }
  }
}

Nachstehendes Listing zeigt den via java -c zerlegten Bytecode. Der generierte Code sieht in Java 7, 8, 9 und 10 identisch aus.

   0: invokestatic  #4   // getNullStream:InputStream;
   3: astore_0
   4: aconst_null
   5: astore_1
   6: new           #5   // class java/io/FileInputStream
   9: dup
  10: ldc           #6   // String somefile.bin
  12: invokespecial #7   // FileInputStream.""(String)
  15: astore_2
  16: aload_0
  17: invokevirtual #8   // InputStream.available()
  20: pop
  21: aload_0
  22: ifnull        90
  25: aload_1
  26: ifnull        45
  29: aload_0
  30: invokevirtual #9   // InputStream.close()
  33: goto          90
  36: astore_2
  37: aload_1
  38: aload_2
  39: invokevirtual #11  // Throwable.addSuppressed(Throwable)
  42: goto          90
  45: aload_0
  46: invokevirtual #9   // InputStream.close()
  49: goto          90
  52: astore_2
  53: aload_2
  54: astore_1
  55: aload_2
  56: athrow
  57: astore_3
  58: aload_0
  59: ifnull        88
  62: aload_1
  63: ifnull        84
  66: aload_0
  67: invokevirtual #9   // java/io/InputStream.close()
  70: goto          88
  73: astore        4
  75: aload_1
  76: aload         4
  78: invokevirtual #11  // Throwable.addSuppressed(Throwable)
  81: goto          88
  84: aload_0
  85: invokevirtual #9   // InputStream.close()
  88: aload_3
  89: athrow
  90: return
Exception table:
   from    to  target type
      29    33    36   Class java/lang/Throwable
       6    21    52   Class java/lang/Throwable
       6    21    57   any
      66    70    73   Class java/lang/Throwable
      52    58    57   any

Unsere vier Zeilen unschuldigen Codes sind explodiert und auf 90 Zeilen angewachsen. Wir sollten entsprechend darauf achten, kurze Methoden zu schreiben, um es den HotSpot-Profilern zu erleichtern, unseren Code zu optimieren. Die Profiler zählen nicht die Java-Codezeilen, sondern die Anzahl der Zeilen Bytecode, die unsere Methoden benötigen. Das ist ziemlich viel.
Noch schlimmer wird die Sache, wenn wir vorherigen Java-Code neu schreiben, um die FileInput-Creation als Ressource hinzuzufügen:

try (InputStream io = getNullStream();
     FileInputStream fi = new FileInputStream("somefile.bin")) {
  io.available();
}

Diese subtile Änderung bedeutet, dass wir fi schließen müssen, bevor wir versuchen io zu schließen. Der dazugehörige plain Java-Code erzeugt 170 Zeilen Bytecode und sieht jetzt so aus:

InputStream io = getNullStream();
Throwable throwable1 = null;
try {
  FileInputStream fi = new FileInputStream("somefile.bin");
  Throwable throwable2 = null;
  try {
    io.available();
  } catch (Throwable t) {
    throwable2 = t;
    throw t;
  } finally {
    if (fi != null) {
      if (throwable2 != null) {
        try {
          fi.close();
        } catch (Throwable t) {
          throwable2.addSuppressed(t);
        }
      } else {
        fi.close();
      }
    }
  }
} catch (Throwable t) {
  throwable1 = t;
  throw t;
} finally {
  if (io != null) {
    if (throwable1 != null) {
      try {
        io.close();
      } catch (Throwable t) {
        throwable1.addSuppressed(t);
      }
    } else {
      io.close();
    }
  }
}

Was Java 9 verändert hat

Meine initiale Aufgabe lautete, herauszufinden, wie der einfache Java-Code aussähe. Währenddessen ist mir aufgefallen, dass sich die Dinge seit Java 9 etwas geändert haben. Unsere erste nicht verschachtelte try-with-Ressource bleibt die selbe. Die zweite mit verschiedenen in try() definierten Ressourcen enthält nun eine synthetische Methode, um das Schließen zu verwalten.

InputStream io = getNullStream();
Throwable throwable1 = null;
try {
  FileInputStream fi = new FileInputStream("somefile.bin");
  Throwable throwable2 = null;
  try {
    io.available();
  } catch (Throwable t) {
    throwable2 = t;
    throw t;
  } finally {
    $closeResource(throwable2, fi);
  }
} catch (Throwable t) {
  throwable1 = t;
  throw t;
} finally {
  if (io != null) {
    $closeResource(throwable1, io);
  }
}

Wie von Zauberhand erscheint Methode $closeResource(Throwable, Autocloseable) in unserer Klasse und sieht so aus:

private static void $closeResource(Throwable t, AutoCloseable a) {
  if (t != null) {
    try {
      a.close();
    } catch(Throwable t2) {
      throwable.addSuppressed(t2);
    }
  } else {
    a.close();
  }
}

Es überrascht, dass die Methode $closeResource() keine Ausnahmen deklariert, obwohl a.close() eindeutig eine Exception werfen könnte. Ein weiterer Beweis dafür, dass das Konzept der checked exception eine Kompilierzeitprüfung ist und nicht während der Ausführung stattfindet. Ich zögere, „zur Laufzeit“ zu sagen, da der Name RuntimeException von java.lang.Runtime stammt, wie in der repräsentativen Klasse der Java-Laufzeitumgebung. Während der Ausführung gibt es keinen Unterschied zwischen den Typen von Throwable-Unterklassen, ob sie unchecked sind (Unterklassen von Error oder RuntimeException) oder checked sind (alle anderen Throwables). Warum funktioniert das? Nun, die Methode $closeResource() wird der Klasse synthetisch hinzugefügt, so dass der normale javac-Compiler sie nicht kompilieren muss. Magie.

Eine weitere Java-9-Änderung in try-with-resource

Es gibt eine weitere kleine Änderung seit Java 9 in Bezug auf try-with-resource. Der Code in Listing 6 wird nicht in Java 8 kompiliert.

import java.io.*;

public class Java9TryWithResource {
  public static void main(String... args) throws IOException {
    printClose(new FileInputStream("Java9TryWithResource.java"));
  }

  private static void printClose(InputStream in)throws IOException {
    try (in; // <-- Compiler error prior to Java 9
         BufferedReader reader = new BufferedReader(
             new InputStreamReader(in)
         )) {
      String line;
      while ((line = reader.readLine()) != null) {
        System.out.println(line);
      }
    }
  }
}

Vor Java 9 hätten wir try() so schreiben müssen:

try (InputStream temp = in;
     BufferedReader reader = new BufferedReader(
         new InputStreamReader(in)
     )) {

Ich habe noch nicht viel Verwendung für diese Funktion gefunden. Ein Wort der Warnung: In der aktuellen Version von IntelliJ 2018.1.2 laufen die Dinge schrecklich schief, wenn Sie versuchen, zu den try-with-resource-Blöcken zu migrieren. Frühere Versionen waren nicht so zerbrechlich wenn ich mich recht entsinne. Wie immer, wenn Sie Code migrieren oder refaktorisieren, seien Sie sehr vorsichtig, denn die Tools transformieren unseren Code nicht immer mit semantischer oder gar syntaktischer Äquivalenz.

Verwandte Themen:

Geschrieben von
Dr. Heinz Kabutz
Dr. Heinz Kabutz
Dr. Heinz Kabutz ist regelmäßiger Referent auf allen wichtigen Java-Konferenzen mit den Schwerpunkten Java Concurrency und Performance. Kabutz schreibt den beliebten „The Java Specialists’ Newsletter“, der von Zehntausenden von begeisterten Fans in über 140 Ländern gelesen wird.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: