Dr. Heinz Kabutz Selbstständig

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.

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.

Ich hatte nicht wirklich eine Ahnung, wie der Java-Code aussähe, wenn ich ihn nicht mit try-with-resource aufhübschte. 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 Listing 1.

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, io null ist, bevor wir versuchen, zu schließen (Listing 2).

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

Listing 3 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 neunzig 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-Kreation 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. Ist es nicht eine Wonne, das nicht selbst von Hand schreiben zu müssen?

Den vollständigen Artikel lesen Sie in der Ausgabe:

Java Magazin 10.18 - "Neue Schule der Softwarearchitektur"

Alle Infos zum Heft
579857654„try-with-resource” in Plain Java
X
- Gib Deinen Standort ein -
- or -