9.6 try mit Ressourcen (automatisches Ressourcen-Management)
Java hat eine automatische Speicherbereinigung, die nicht mehr referenzierte Objekte erkennt und ihren Speicher automatisch freigibt. Nun bezieht sich der Garbage-Collector aber ausschließlich auf Speicher, doch es gibt viele weitere Ressourcen:
-
Dateisystemressourcen von Dateien
-
Netzwerkressourcen wie Socket-Verbindungen
-
Datenbankverbindungen
-
nativ gebundene Ressourcen des Grafiksubsystems
-
Synchronisationsobjekte
Auch hier gilt es, nach getaner Arbeit aufzuräumen und Ressourcen freizugeben, etwa Dateien und Datenbankverbindungen zu schließen.
Mit dem try-catch-finally-Konstrukt haben wir gesehen, wie Ressourcen freizugeben sind. Doch es lässt sich auch ablesen, dass relativ viel Quellcode geschrieben werden muss und dass try-catch-finally drei Unfeinheiten hat:
-
Soll eine Variable in finally zugänglich sein, muss sie außerhalb des try-Blocks deklariert werden, was ihr eine längere Sichtbarkeit als nötig gibt.
-
Das Schließen der Ressourcen bringt oft ein zusätzliches try-catch mit sich.
-
Eine im finally ausgelöste Ausnahme (etwa beim close()) überdeckt die im try-Block ausgelöste Ausnahme.
9.6.1 try mit Ressourcen
Um das Schließen von Ressourcen zu vereinfachen, gibt es eine besondere Form der try-Anweisung, die try mit Ressourcen heißt. Mit diesem Sprachkonstrukt lassen sich Ressourcentypen automatisch schließen, die die Schnittstelle java.lang.AutoCloseable implementieren. Ein-/Ausgabeklassen wie Scanner, InputStream, Writer, RandomAccessFile implementieren diese Schnittstelle und können direkt verwendet werden. Weil try mit Ressourcen dem Automatic Resource Management dient, heißt der spezielle try-Block auch ARM-Block.
Gehen wir in die Praxis: Im früheren Programm ReadGifSizeWithTryFinally mussten wir mit viel Code das RandomAccessFile schließen. Das try mit Ressourcen macht es einfacher:
public static void main( String[] args ) {
try ( RandomAccessFile raf = new RandomAccessFile( "duke.gif", "r" ) ) {
raf.seek( 6 );
System.out.printf( "%s x %s Pixel%n", raf.read() + raf.read() * 256,
raf.read() + raf.read() * 256 );
}
catch ( IOException e ) {
System.err.println( "Ein-/Ausgabefehler" );
}
}
Bisher haben wir nach dem Schlüsselwort try direkt einen Block gesetzt, doch try mit Ressourcen nutzt eine eigene erweiterte Syntax: Nach dem try folgt statt des direkten {}-Blocks eine Ressourcenspezifikation in runden Klammern, und erst dann folgt der {}-Block, also try (…) {…} statt try {…}.
Die Ressourcenspezifikation besteht aus einem Satz runder Klammern und einer Liste von Ressourcen. Die Ressourcen sind finale Variablen vom Typ AutoCloseable, und genau diese werden später automatisch geschlossen. Die Variablen können mit einem Gleichheitszeichen direkt mit einem Ausdruck initialisiert werden, etwa mit einem Konstruktor- oder Methodenaufruf. Vor Java 9 war die Initialisierung zwingend. Der Einsatz von var ist erlaubt.
Die in dem try deklarierte lokale Variable ist nur in dem Block gültig und wird automatisch freigegeben, gleichgültig, ob der ARM-Block korrekt durchlaufen wurde oder ob es bei der Abarbeitung zu einer Ausnahme kam. Der Compiler fügt alle nötigen Prüfungen ein.
[»] Hinweis
Wird auf einer Variablen, die vom Typ AutoCloseable ist, nicht close() aufgerufen oder wird sie nicht in einem try mit Ressourcen eingesetzt, gibt der Java-Compiler eine Warnung aus. Die Warnung verschwindet mit @SuppressWarnings("resource").
9.6.2 Die Schnittstelle AutoCloseable
Das try mit Ressourcen schließt Ressourcen, die auf den Typ AutoCloseable »passen«. Daher wird es Zeit, sich diese Schnittstelle etwas genauer anzuschauen:
package java.lang;
public interface AutoCloseable {
void close() throws Exception;
}
Ausnahmen
Auffällig ist bei close(), dass eine sehr allgemeine Exception ausgelöst werden kann. Zum Vergleich: Die Ein-/Ausgabe-Klassen lösen beim Misslingen eine IOException aus, aber unterschiedliche Klassen können bei close() unterschiedliche Ausnahmen auslösen oder auch überhaupt nichts auslösen.
Typ |
Signatur |
---|---|
java.io.Scanner |
close() // ohne Ausnahme |
javax.sound.sampled.Line |
close() // ohne Ausnahme |
java.io.RandomAccessFile |
close() throws IOException |
java.sql.Connection |
close() throws SQLException |
Implementiert eine Klasse die Schnittstelle, darf die Ausnahme bei throws weglassen werden. Das machen Klassen wie der Scanner, der bei den Methoden keine Ausnahme weiterleitet, sondern Ausnahmen intern abfängt und die letzte Ausnahme später über die Methode ioException() zugänglich macht.
9.6.3 Ausnahmen vom close()
Wenn try mit Ressourcen verwendet wird, bleibt die eine deklarierte Ausnahme bei close() bestehen; es zaubert die möglichen ausgelösten Ausnahmen nicht weg, und entweder muss ein fangendes catch her oder die Ausnahme muss nach oben weitergeleitet werden.
Es ist natürlich etwas merkwürdig, dass der Compiler einen Fehler im Code meldet, der nicht sichtbar ist. Aber daran müssen wir uns gewöhnen, denn im Bytecode ist die Schließanweisung ja vorhanden. Löst close() eine geprüfte Ausnahme aus und wird diese nicht behandelt, so kommt es zum Compilerfehler, egal, ob uns der Compiler die Anweisung generiert, oder wir das Schließen explizit hineinsetzen.
[zB] Beispiel
Die close()-Methode vom BufferedReader löst eine IOException aus, sodass sich die folgende Methode nicht übersetzen lässt:
void no() {
try ( Reader r = new BufferedReader(null) ) { } // Compilerfehler
}
Der Ausdruck new BufferedReader(null) benötigt keine Behandlung, denn der Konstruktor löst keine Ausnahme aus. Einzig die nicht behandelte Ausnahme von close() führt zu »exception thrown from implicit call to close() on resource variable 'r'«.
Der Vorteil vom try mit Ressourcen ist, dass das zugehörige catch die Ausnahme vom close() mit abfangen kann. Nehmen wir:
try ( RandomAccessFile raf = new RandomAccessFile( "duke.gif", "r" ) ) {
raf.seek( 6 );
}
catch ( IOException e ) {
System.err.println( "Ein-/Ausgabefehler" );
}
Bei try mit Ressourcen setzt der Compiler selbstständig in den Bytecode ein close(), was im Beispiel eine IOException auslöst – das catch fängt es mit auf.
Scanner zum Beispiel löst bei close() keine Ausnahme aus, daher muss auch kein catch-Block folgen.
InputStream in = ClassLoader.getSystemResourceAsStream( "EastOfJava.txt" );
try ( Scanner res = new Scanner( in ) ) {
System.out.println( res.nextLine() );
}
Ein catch-Block ist nicht zwingend, denn keine der Methoden löst eine geprüfte Ausnahme aus – weder getSystemResourceAsStream(…), new Scanner(InputStream), nextLine() noch das close(), das try mit Ressourcen automatisch aufruft. Anders ist es, wenn die Ressource ein RandomAccessFile oder klassischer Datenstrom (InputStream/OutputStream/Reader/Writer) ist, denn dort deklariert die close()-Methode eine IOException.
9.6.4 Typen, die AutoCloseable und Closeable sind
In der Java-Bibliothek gibt es eine zweite Schnittstelle Closeable, die von der Geschichte etwas älter ist. Doch die close()-Methode deklariert ein throws IOException, was bei einer allgemeinen automatischen Ressourcenfreigabe unpassend ist, wenn etwa ein Grafikobjekt bei der Freigabe eine IOException auslöst. Vielmehr ist der Weg andersherum: Closeable erweitert AutoCloseable, denn das Schließen von Ein-/Ausgabe-Ressourcen ist eine besondere Art, allgemeine Ressourcen zu schließen. So ist die Schnittstelle realisiert:
package java.io;
import java.io.IOException;
public interface Closeable extends AutoCloseable {
void close() throws IOException;
}
Wer ist AutoCloseable?
Da alle Klassen, die Closeable implementieren, auch automatisch vom Typ AutoCloseable sind, kommen schon einige Typen zusammen. Im Wesentlichen sind es aber Klassen aus dem java.io-Paket, wie RandomAccessFile, Channel-, Reader-, Writer-Implementierungen, FileLock, XMLDecoder und noch ein paar Exoten wie URLClassLoader, ImageOutputStream. Auch Typen aus dem java.sql-Paket gehören zu den Nutznießern. Klassen aus dem Bereich Threading, wo etwa ein Lock wieder freigegeben werden könnte, oder Grafikanwendungen, bei denen der Grafikkontext wieder freigegeben werden muss, gehören nicht dazu.
[»] Hinweis
Es gibt Ströme, die müssen offen bleiben. Das gilt etwa für System.in, einen vom System bereitgestellten InputStream. Auch wenn dieser in einen Scanner verpackt wird, wird ein close() auf dem Scanner zu einem close() auf dem InputStream, und ein erneutes Lesen aus System.in wird mit einem »java.io.IOException: Stream closed« quittiert. Ein try ( Scanner in = new Scanner(System.in) ) { … } ist also keine gute Idee.
[+] Tipp *
Es ist mit einem Trick möglich, auch Exemplare in einem try mit Ressourcen zu nutzen, die nicht vom Typ AutoCloseable sind. Ein Lambda-Ausdruck oder eine Methodenreferenz lassen sich nutzen, um eine beliebige Methode als close()-Methode einzusetzen. Ein ReentrantLock zum Beispiel ist eine Implementierung eines Lock, um bei nebenläufigen Zugriffen einen Bereich abzuschließen. lock() beginnt den Bereich, unlock() gibt ihn wieder frei. Das unlock() lässt sich über einen Lambda-Ausdruck als close()-Methode verkaufen.
ReentrantLock lock = new ReentrantLock();
try ( AutoCloseable unlock = lock::unlock ) { // oder () -> {lock.unlock();}
lock.lock();
}
System.out.println( lock.isLocked() ); // false
Ob dieser »Trick« sinnvoll ist oder nicht, ist eine andere Frage. Das try mit Ressourcen setzt auf jeden Fall das unlock() in einen internen finally-Block, der über die Konstruktion eingespart wird. Allerdings wird üblicherweise die Ressource im try-mit-Ressourcen-Block auch erst deklariert, was hier vorher gemacht werden muss. Außerdem ist die Variable unlock unnütz. Daher ist die Relevanz eher niedrig.
9.6.5 Mehrere Ressourcen nutzen
Die vorherigen Beispiele zeigten die Nutzung genau einer Ressource. Es sind aber auch mehrere Typen möglich, die ein Semikolon trennt:
try ( InputStream in = Files.newInputStream( srcPath );
OutputStream out = Files.newOutputStream( destPath ) ) {
...
}
Weiterhin ist es erlaubt, dass die nachfolgende Ressource auf die vorherige Ressource verweist, sodass geschachtelte Ströme möglich sind:
try ( InputStream fis = Files.newInputStream( srcPath );
InputStream bis = new BufferedInputStream( fis ) ) {
...
}
Dieses Modell findet oft bei Dekoratoren Anwendung.
[»] Hinweis
Die Trennung erledigt ein Semikolon, und jedes Segment kann einen unterschiedlichen Typ deklarieren, etwa InputStream/OutputStream. Die Ressourcentypen müssen also nicht gleich sein, und auch wenn sie es sind, muss der Typ immer neu geschrieben werden, also etwa:
try ( InputStream in1 = ...; InputStream in2 = ... )
Es ist ungültig, Folgendes zu schreiben:
try ( InputStream in1 = ..., in2 = ... ) // Compilerfehler
Am Schluss der Ressourcensammlung kann – muss aber nicht – ein Semikolon stehen, so wie auch bei Array-Initialisierungen zum Schluss ein Komma stehen kann:
int[] array = { 1, 2, };
// ^ Komma optional
try ( InputStream in = Files.newInputStream( path ); ) { ... }
// ^ Semikolon optional
try ( InputStream in = Files.newInputStream( src );
OutputStream out = Files.newOutputStream( dest ); ) { ... }
// ^ Semikolon optional
Ob das stilvoll ist, muss jeder selbst entscheiden – in der »Insel« steht kein unnützes Zeichen.
Reihenfolge beim Schließen
Die Ressourcen werden in der umgekehrten Reihenfolge geschlossen, wie sie geöffnet wurden.
[zB] Beispiel
Zunächst wird in, dann out initialisiert. Zum Schluss wird out, danach in geschlossen.
try ( InputStream in = Files.newInputStream( srcPath );
OutputStream out = Files.newOutputStream( destPath ) ) {
...
}
Kommt es beim Anlegen in der Kette zu einer Ausnahme, wird nur die Ressource geschlossen, die geöffnet wurde. Wenn es also in dem gerade genannten Beispiel bei der ersten Initialisierung von in knallt, wird die Belegung von out erst gar nicht begonnen und daher auch nicht geschlossen. (Intern setzt der Compiler das als geschachtelte try-catch-finally-Blöcke um.)
9.6.6 try mit Ressourcen auf null-Ressourcen
Dass immer zum Abschluss eines try-mit-Ressourcen-Blocks ein close() aufgerufen wird, ist nicht ganz korrekt. Es gibt nur dann einen Schließversuch, wenn die Ressource ungleich null ist.
[zB] Beispiel
Der Codebaustein compiliert und führt zu einer Konsolenausgabe:
try ( Scanner scanner1 = null; Scanner scanner2 = null ) {
System.out.println( "Ok" );
}
Bei Konstruktoren ist ein Objekt ja immer gegeben, aber es gibt auch Fabrikaufrufe, bei denen vielleicht null herauskommen kann. Für diese Fälle ist es ganz praktisch, dass try mit Ressourcen dann nichts macht, um eine NullPointerException beim close() zu vermeiden.
9.6.7 Unterdrückte Ausnahmen *
Der Compiler setzt zwar bei einem try mit Ressourcen einen Aufruf von close() automatisch in einen finally-Block, doch ganz so einfach ist es dann doch nicht. Es können zwei Ausnahmen auftauchen, die einiges an Sonderbehandlung benötigen:
-
nur Ausnahme im try-Block, an sich unproblematisch
-
Ausnahme beim close(), auch an sich unproblematisch. Aber es gibt mehrere close()-Aufrufe, wenn nicht nur eine Ressource verwendet wurde: Ungünstig.
-
Die Steigerung: Ausnahme im try-Block und dann auch noch Ausnahme(n) beim close(). Das ist ein echtes Problem!
Eine Ausnahme allein ist kein Problem, aber zwei Ausnahmen auf einmal bilden ein großes Problem, da ein Programmblock nur genau eine Ausnahme melden kann und nicht eine Sequenz von Ausnahmen. Daher sind verschiedene Fragen zu klären, falls der try-Block und close() beide eine Ausnahme auslösen:
-
Welche Ausnahme ist wichtiger? Die Ausnahme im try-Block oder die vom close()?
-
Wenn es zu zwei Ausnahmen kommt: Soll die vom close() vielleicht immer verdeckt werden und immer nur die vom try-Block zum Anwender kommen?
-
Wenn beide Ausnahmen gleich wichtig sind, wie sollen sie gemeldet werden?
Wie haben sich die Java-Ingenieure entschieden? Eine Ausnahme bei close() darf bei einem gleichzeitigen Auftreten einer Exception im try-Block auf keinen Fall verschwinden.[ 192 ](In einem frühen Prototyp war dies tatsächlich der Fall – die Ausnahme wurde komplett geschluckt. ) Wie also beide Ausnahmen melden? Hier gibt es einen Trick: Da die Ausnahme im try-Block wichtiger ist, ist sie die »Hauptausnahme«, und die close()-Ausnahme kommt Huckepack als Extrainformation mit obendrauf.
Dieses Verhalten soll das nächste Beispiel zeigen. Um die Ausnahmen besser steuern zu können, soll eine eigene AutoCloseable-Implementierung eine Ausnahme in close() auslösen.
public class NotCloseable implements AutoCloseable {
@Override public void close() {
throw new UnsupportedOperationException( "close() mag ich nicht" ); //
}
}
Die Klasse NotCloseable wollen wir als Ressource nutzen:
public class SuppressedClosed {
public static void main( String[] args ) {
try ( NotCloseable res = new NotCloseable() ) {
throw new NullPointerException(); //
}
}
}
NotCloseable löst im close() eine Ausnahme aus, die beim try mit Ressourcen ankommt. Ohne catch bricht die JVM den Thread ab, und das Resultat auf der Konsole ist:
Exception in thread "main" java.lang.NullPointerException
at com.tutego.insel.exception.SuppressedClosed.main(SuppressedClosed.java:6)
Suppressed: java.lang.UnsupportedOperationException: close() mag ich nicht
at com.tutego.insel.exception.NotCloseable.close(NotCloseable.java:4)
at com.tutego.insel.exception.SuppressedClosed.main(SuppressedClosed.java:7)
Die Hauptausnahme ist die NullPointerException. Die interessante Zeile beginnt mit Suppressed:, denn dort ist die close()-Ausnahme referenziert. An den Aufrufer kommt die spannende Ausnahme des misslungenen try-Blocks aber nicht direkt von close(), sondern verpackt in der Hauptausnahme und muss extra erfragt werden.
Zum Vergleich: Kommentieren wir throw new NullPointerException() aus, gibt es nur noch die close()-Ausnahme, und es folgt auf der Konsole:
Exception in thread "main" java.lang.UnsupportedOperationException: close() mag ich nicht
at com.tutego.insel.exception.NotCloseable.close(NotCloseable.java:4)
at com.tutego.insel.exception.SuppressedClosed.main(SuppressedClosed.java:7)
Die Ausnahme ist also nicht irgendwo anders untergebracht, sondern die »Hauptausnahme«. Eine Steigerung ist, dass es mehr als eine Ausnahme beim Schließen geben kann. Simulieren wir auch dies wieder an einem Beispiel, indem wir unser Beispiel um eine Zeile ergänzen:
try ( NotCloseable res1 = new NotCloseable();
NotCloseable res2 = new NotCloseable() ) {
throw new NullPointerException();
}
Aufgerufen führt dies zu:
Exception in thread "main" java.lang.NullPointerException
at com.tutego.insel.exception.SuppressedClosed2.main(SuppressedClosed2.java:7)
Suppressed: java.lang.UnsupportedOperationException: close() mag ich nicht
at com.tutego.insel.exception.NotCloseable.close(NotCloseable.java:4)
at com.tutego.insel.exception.SuppressedClosed2.main(SuppressedClosed2.java:8)
Suppressed: java.lang.UnsupportedOperationException: close() mag ich nicht
at com.tutego.insel.exception.NotCloseable.close(NotCloseable.java:4)
at com.tutego.insel.exception.SuppressedClosed2.main(SuppressedClosed2.java:8)
Jede unterdrückte close()-Ausnahme taucht auf.
[»] Umsetzung
In Abschnitt 9.4, »Abschlussbehandlung mit finally«, wurde das Verhalten vorgestellt, dass eine Ausnahme im finally eine Ausnahme im try-Block unterdrückt. Der Compiler setzt bei der Umsetzung vom try mit Ressourcen das close() in einen finally-Block. Ausnahmen im finally-Block sollen eine mögliche Hauptausnahme aber nicht schlucken. Daher fängt die Umsetzung vom Compiler jede mögliche Ausnahme im try-Block ab sowie die close()-Ausnahme und hängt diese Schließausnahme, falls vorhanden, an die Hauptausnahme.
Spezielle Methoden in Throwable *
Damit eine normale Exception die unterdrückten close()-Ausnahmen Huckepack nehmen kann, gibt es in der Basisklasse Throwable zwei besondere Methoden:
final class java.lang.Throwable
-
final Throwable[] getSuppressed()
Liefert alle unterdrückten Ausnahmen. Die printStackTrace(…)-Methode zeigt alle unterdrückten Ausnahmen und greift auf getSuppressed() zurück. Für Anwender wird es selten Anwendungsfälle für diese Methode geben. -
final void addSuppressed(Throwable exception)
Fügt eine neue unterdrückte Ausnahme hinzu. In der Regel ruft der finally-Block vom try mit Ressourcen die Methode auf, doch wir können auch selbst die Methode nutzen, wenn wir mehr als eine Ausnahme melden wollen. Die Java-Bibliothek selbst nutzt das bisher nur an sehr wenigen Stellen.
Neben den beiden Methoden gibt es einen protected-Konstruktor, der bestimmt, ob es überhaupt unterdrückte Ausnahmen geben soll oder ob sie nicht vielleicht komplett geschluckt werden. Wenn, dann zeigt auch printStackTrace(…) sie nicht mehr an.
[»] Blick über den Tellerrand
In C++ gibt es Destruktoren, die beliebige Anweisungen ausführen, wenn ein Objekt freigegeben wird. Hier lässt sich auch das Schließen von Ressourcen realisieren. C# nutzt statt try das spezielle Schlüsselwort using, mit Typen, die die Schnittstelle IDisposable implementieren, und zwar mit einer Methode Dispose() statt close() (in Java sollte die Schnittstelle ursprünglich auch Disposable statt nun AutoCloseable heißen). In Python 2.5 wurde ein context management protocol mit dem Schlüsselwort with realisiert, sodass Python automatisch beim Betreten eines Blocks __enter__() aufruft und beim Verlassen die Methode __exit__(). Das ist insofern interessant, als hier zwei Methoden zur Verfügung stehen. Bei Java ist es nur close() beim Verlassen des Blocks, aber es gibt keine Methode zum Betreten eines Blocks. So etwas muss beim Anlegen der Ressource erledigt werden.