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:
1
2
3
4
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
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:
1
2
3
4
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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:
1
2
3
4
5
6
7
8
9
10
11
|
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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:
1
2
3
4
|
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.
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