1 Neues in Java 7
»Jede Lösung eines Problems ist ein neues Problem.«
– Johann Wolfgang von Goethe (1749–1832)
1.1 Sprachänderungen
Die folgenden Abschnitte stellen Sprachänderungen aus Java 7 vor.
1.1.1 Zahlen im Binärsystem schreiben
Die Literale für Ganzzahlen lassen sich in vier unterschiedlichen Stellenwertsystemen angeben. Das natürlichste ist das Dezimalsystem (auch Zehnersystem genannt), bei dem die Literale aus den Ziffern »0« bis »9« bestehen. Zusätzlich existieren Oktal- und Hexadezimalsysteme, und seit Java 7 die Binärschreibweise. Bis auf Dezimalzahlen beginnen die Zahlen in anderen Formaten mit einem besonderen Präfix.
Präfix | Stellenwertsystem | Basis | Darstellung von 1 |
0b oder 0B |
Binär |
2 |
0b1 oder 0B1 |
0 |
Oktal |
8 |
01 |
Kein |
Dezimal |
10 |
1 |
0x oder 0X |
Hexadezimal |
16 |
0x1 oder 0X1 |
1.1.2 Unterstriche bei Ganzzahlen
Um eine Anzahl von Millisekunden in Tage zu konvertieren, muss einfach eine Division vorgenommen werden. Um Millisekunden in Sekunden umzurechnen, brauchen wir eine Division durch 1000, von Sekunden auf Minuten eine Division durch 60, von Minuten auf Stunden eine Division durch 60, und die Stunden auf Tage bringt die letzte Division durch 24. Schreiben wir das auf:
long millis = 10 * 24*60*60*1000L;
long days = millis / 86400000L;
System.out.println( days ); // 10
Eine Sache fällt bei der Zahl 86400000 auf: Besonders gut lesbar ist sie nicht. Die eine Lösung ist, es erst gar nicht zu so einer Zahl kommen zu lassen und sie wie in der ersten Zeile durch eine Reihe von Multiplikationen aufzubauen – mehr Laufzeit kostet das nicht, da dieser konstante Ausdruck zur Übersetzungszeit feststeht.
Die zweite Variante ist eine neue Schreibweise, die Java 7 einführt: Unterstriche in Zahlen. Anstatt ein numerisches Literal als 86400000 zu schreiben, ist in Java 7 auch Folgendes erlaubt:
long millis = 10 * 86_400_000L;
long days = millis / 86_400_000L;
System.out.println( days ); // 10
Die Unterstriche machen die 1000er-Blöcke gut sichtbar. Hilfreich ist die Schreibweise auch bei Literalen in Binär- und Hexdarstellung, da die Unterstriche hier ebenfalls Blöcke absetzen können.[1](Bei Umrechnungen zwischen Stunden, Minuten und so weiter hilft auch die Klasse TimeUnit mit einigen statischen toXXX()-Methoden.)
Beispiel |
Unterstriche verdeutlichen Blöcke bei Binär- und Hexadezimalzahlen. int i = 0b01101001_01001101_11100101_01011110; |
Der Unterstrich darf in jedem Literal stehen, zwei aufeinanderfolgende Unterstriche sind aber nicht erlaubt.
1.1.3 switch (String)
Seit Java 7 sind switch-Anweisungen auf String-Objekten möglich.
String input = javax.swing.JOptionPane.showInputDialog( "Eingabe" );
switch ( input.toLowerCase() )
{
case "kekse":
System.out.println( "Ich mag Keeeekse" );
break;
case "kuchen":
System.out.println( "Ich mag Kuchen" );
break;
case "scholokade":
case "lakritze":
System.out.println( "Hm. lecker" );
break;
default:
System.out.printf( "Kann man %s essen?", input );
}
Obwohl Zeichenkettenvergleiche nun möglich sind, fallen Überprüfungen auf reguläre Ausdrücke leider heraus, die insbesondere Skriptsprachen anbieten.
Wie auch beim switch mit Ganzzahlen können die Zeichenketten beim String-case-Zweig aus finalen (also nicht änderbaren) Variablen stammen. Ist etwa String KEKSE = "kekse"; vordefiniert, ist case KEKSE: erlaubt.
1.1.4 Zusammenfassen gleicher catch-Blöcke mit dem multi-catch
Greift ein Programm auf Teile zurück, die scheitern können, so ergeben sich in komplexeren Abläufen schnell Situationen, in denen unterschiedliche Ausnahmen auftreten können. Entwickler sollten versuchen, den Programmcode in einem try-Block durchzuschreiben, und dann in catch-Blöcken auf alle möglichen Fehler zu reagieren, die den Block vom korrekten Durchlaufen abgehalten haben.
Oftmals kommt es zu dem Phänomen, dass die aufgerufenen Programmteile unterschiedliche Ausnahmetypen auslösen, aber die Behandlung der Fehler gleich aussieht. Um Quellcodeduplizierung zu vermeiden, sollte der Programmcode zusammengefasst werden. Nehmen wir an, die Behandlung der Ausnahmen SQLException und IOException soll gleich sein. Wir haben schon gesehen, dass ein catch(Exception e) keine gute Lösung ist und nie im Programmcode vorkommen sollte, denn dann würden auch andere Ausnahmen mitgefangen. Zum Glück gibt es in Java 7 eine elegante Lösung.
Multi-catch
Java 7 führt eine neue Schreibweise für catch-Anweisungen ein, um mehrere Ausnahmen auf einmal aufzufangen; sie heißt multi-catch. In der abgewandelten Variante von catch steht dann nicht mehr nur eine Ausnahme, sondern eine Sammlung von Ausnahmen, die ein »|« trennt. Der Schrägstrich ist schon als Oder-Operator bekannt und wurde daher auch hier eingesetzt, denn die Ausnahmen sind ja auch als eine Oder-Verknüpfung zu verstehen. Die allgemeine Syntax ist:
try
{
...
}
catch ( E1 | E2 | ... | En exception )
Die Variable exception ist implizit final.
Um das multi-catch zu demonstrieren, nehmen wir ein Programm an, das eine Farbtabelle einliest. Die Datei besteht aus mehreren Zeilen, wobei in jeder Zeile die erste Zahl einen Index repräsentiert und die zweite Zahl den hexadezimalen RGB-Farbwert.
Listing 1.1: basiscolors.txt
0 000000
1 ff0000
8 00ff00
9 ffff00
Eine eigene Methode readColorTable() soll die Datei einlesen und ein int-Feld der Größe 256 als Rückgabe liefern, wobei an den in der Datei angegebeen Positionen jeweils die Farbwerte eingetragen sind. Nicht belegte Positionen bleiben 0. Gibt es einen Ladefehler, soll die Rückgabe null sein und die Methode eine Meldung auf dem Fehlerausgabekanal ausgeben.
Das Einlesen soll die Scanner-Klasse übernehmen. Bei der Verarbeitung der Daten und der Füllung des Feldes sind diverse Fehler denkbar:
- IOException: Die Datei ist nicht vorhanden, oder während des Einlesens kommt es zu Problemen.
- InputMismatchException: Der Index oder die Hexadezimalzahl sind keine Zahlen (einmal zur Basis 10, und dann zur Basis 16). Den Fehlertyp löst der Scanner aus.
- ArrayIndexOutOfBoundsException: Der Index liegt nicht im Bereich von 0 bis 255.
Während der erste Fehler beim Dateisystem zu suchen ist, sind die zwei unteren Fehler – unabhängig davon, dass sie ungeprüfte Ausnahmen sind – auf ein fehlerhaftes Format zurückzuführen. Die Behandlung soll immer gleich aussehen und kann daher gut in einem multi-catch zusammengefasst werden. Daraus folgt:
Listing 1.2: ReadColorTable.java
import java.io.*;
import java.util.Scanner;
public class ReadColorTable
{
private static int[] readColorTable( String filename )
{
Scanner input;
int[] colors = new int[ 256 ];
try
{
input = new Scanner( new File(filename) );
while ( input.hasNextLine() )
{
int index = input.nextInt();
int rgb = input.nextInt( 16 );
colors[ index ] = rgb;
}
return colors;
}
catch ( IOException e )
{
System.err.printf( "Dateioperationen fehlgeschlagen%n%s%n", e );
}
catch ( InputMismatchException | ArrayIndexOutOfBoundsException e )
{
System.err.printf( "Datenformat falsch%n%s%n", e );
}
finally
{
input.close();
}
return null;
}
public static void main( String[] args )
{
readColorTable( "basiscolors.txt" );
}
}
Multi-catch-Blöcke sind also eine Abkürzung, und der Bytecode sieht genauso aus wie mehrere gesetzte catch-Blöcke, also wie:
catch ( InputMismatchException e )
{
System.err.printf( "Datenformat falsch%n%s%n", e );
}
catch ( ArrayIndexOutOfBoundsException e )
{
System.err.printf( "Datenformat falsch%n%s%n", e );
}
Multi-catch-Blöcke sind nur eine Abkürzung, daher teilen sie auch die Eigenschaften der normalen catch-Blöcke. Der Compiler führt die gleichen Prüfungen wie bisher durch, also ob etwa die genannten Ausnahmen im try-Block überhaupt ausgelöst werden können. Nur das, was in der durch »|« getrennten Liste aufgezählt ist, wird behandelt; unser Programm fängt zum Beispiel nicht generisch alle RuntimeExceptions ab. Und genauso dürfen die in catch oder multi-catch genannten Ausnahmen nicht in einem anderen (multi)-catch auftauchen.
Neben den Standard-Tests kommen neue Überprüfungen hinzu, ob etwa die exakt gleiche Exception zweimal in der Liste ist oder ob es Widersprüche durch Mengenbeziehungen gibt.
Hinweis |
Der folgende multi-catch ist falsch: try Der javac-Compiler meldet einen Fehler der Art »Alternatives in a multi-catch statement cannot
be related by subclassing« und bricht ab. Mengenprüfungen führt der Compiler auch ohne multi-catch durch, und Folgendes ist ebenfalls falsch: try { new RandomAccessFile("", ""); } Während allerdings eine Umsortierung der Zeilen die Fehler korrigiert, spielt die Reihenfolge bei multi-catch keine Rolle. |
1.1.5 Präzisiertes rethrows
Die Notwendigkeit, Ausnahmen über einen Basistyp zu fangen, ist mit dem Einzug vom multi-catch gesunken. Doch für gewisse Programmteile ist es immer noch praktisch, alle Fehler eines gewissen Typs aufzufangen. Wir können auch so weit in der Ausnahmehierarchie nach oben laufen, um alle Fehler aufzufangen – dann haben wir es mit einem try { ... } catch(Throwable t){ ... } zu tun. Ein multi-catch ist für geprüfte Ausnahmen besonders gut, aber bei ungeprüften Ausnahmen ist eben nicht immer klar, was als Fehler denn so ausgelöst wird, und ein catch(Throwable t) hat den Vorteil, dass es alles wegfischt.
Problemstellung
Werden Ausnahmen über einen Basistyp gefangen und wird diese Ausnahme mit throw weitergeleitet, dann ist es naheliegend, dass der aufgefangene Typ genau der Typ ist, der auch bei throws in der Methodensignatur stehen muss.
Stellen wir uns vor, ein Programmblock nimmt einen Screenshot und speichert ihn in einer Datei. Kommt es beim Abspeichern zu einem Fehler, soll das, was vielleicht schon in die Datei geschrieben wurde, gelöscht werden; die Regel ist also: Entweder steht der Screenshot komplett in der Datei oder es gibt gar keine Datei. Die Methode kann so aussehen, wobei die Ausnahmen an den Aufrufer weitergegeben werden sollen:
public static void saveScreenshot( String filename )
throws AWTException, IOException
{
try
{
Rectangle r = new Rectangle( Toolkit.getDefaultToolkit().getScreenSize() );
BufferedImage screenshot = new Robot().createScreenCapture( r );
ImageIO.write( screenshot, "png", new File( filename ) );
}
catch ( AWTException e )
{
throw e;
}
catch ( IOException e )
{
new File( filename ).delete();
throw e;
}
}
Mit den beiden catch-Blöcken sind wir genau auf die Ausnahmen eingegangen, die createScreenCapture() und write() auslösen. Das ist richtig, aber löschen wir wirklich immer die Dateireste, wenn es Probleme beim Schreiben gibt? Richtig ist, dass wir immer dann die Datei löschen, wenn es zu einer IOException kommt. Aber was passiert, wenn die Implementierung eine RuntimeException auslöst? Dann wird die Datei nicht gelöscht, aber das ist gefragt! Das scheint einfach gefixt, denn statt
catch ( IOException e )
{
new File( filename ).delete();
throw e;
}
schreiben wir:
catch ( Throwable e )
{
new File( filename ).delete();
throw e;
}
Doch können wir das Problem so lösen? Der Typ Throwable passt doch gar nicht mehr mit dem deklarierten Typ IOException in der Methodensignatur zusammen:
public static void saveScreenshot( String filename )
throws AWTException /*1*/, IOException /*2*/
{
...
catch ( AWTException /*1*/ e )
{
throw e;
}
catch ( Throwable /*?*/ e )
{
new File( filename ).delete();
throw e;
}
}
Die erste catch-Klausel fängt AWTException und leitet es weiter. Damit wird saveScreenshot() zum möglichen Auslöser von AWTException und die Ausnahme muss mit throws an die Signatur. Wenn nun ein catch-Block jedes Throwable auffängt und diesen Throwable-Fehler weiterleitet, ist zu erwarten, dass an der Signatur auch Throwable stehen muss und IOException nicht reicht. Das war auch bis Java 6 so, aber in Java 7 kam eine Anpassung.
Neu seit Java 7: eine präzisere Typprüfung
In Java 7 hat der Compiler eine kleine Veränderung erfahren, von einer unpräziseren zu einer präziseren Typanalyse: Immer dann, wenn in einem catch-Block ein throw stattfindet, ermittelt der Compiler die im try-Block tatsächlich aufgetretenen geprüften Exception-Typen und schenkt dem im catch genannten Typ für das rethrow im Prinzip keine Beachtung. Statt dem gefangenen Typ wird der Compiler den durch die Codeanalyse gefundenen Typ beim rethrow melden.
Der Compiler erlaubt nur dann das präzise rethrow, wenn die catch-Variable nicht verändert wird. Zwar ist eine Veränderung einer nicht-finalen catch-Variablen wie auch unter Java 1.0 erlaubt, doch wenn die Variable belegt wird, schaltet der Compiler von der präzisen in die unpräzise Erkennung zurück. Führen wir etwa die folgende Zuweisung ein, so funktioniert das Ganze schon nicht mehr:
catch ( Throwable e )
{
new File( filename ).delete();
e = new IllegalStateException();
throw e;
}
Die Zuweisung führt zu dem Compilerhinweis, dass jetzt auch Throwable mit in die throws-Klausel muss.
Stilfrage |
Die catch-Variable kann für die präzisere Typprüfung den Modifizierer final tragen, muss das aber nicht tun. Immer dann, wenn es keine Veränderung an der Variablen gibt, wird der Compiler sie als final betrachten und eine präzisere Typprüfung durchführen – daher nennt sich das auch effektiv final. Die Java Language Specification rät vom final-Modifizierer aus Stilgründen ab. Ab Java 7 ist es das Standardverhalten, und daher ist es Quatsch, überall ein final dazuzuschreiben, um die präzisere Typprüfung zu dokumentieren. |
Migrationsdetail |
Da der Compiler nun mehr Typwissen hat, stellt sich die Frage, ob alter Programmode mit dem neuen präziseren Verhalten vielleicht ungültig werden könnte. Theoretisch ist das möglich, aber die Sprachdesigner haben in über 9 Millionen Zeilen Code[2](Die Zahl stammt aus der FOSDEM 2011-Präsentation »Project Coin: Language Evolution in the Open«.) von unterschiedlichen Projekten keine Probleme gefunden. Prinzipiell könnte der Compiler jetzt unerreichbaren Code finden, der vorher versteckt blieb. Ein kleines Beispiel, was vor Java 7 compiliert, aber ab Java 7 nicht mehr: try Die Variable e in catch (IOException e) ist effektiv final, und der Compiler führt die präzisere Typerkennung durch. Er findet heraus, dass der wahre rethrow-Typ nicht IOException, sondern FileNotFoundException ist. Wenn dieser Typ dann mit throw e weitergeleitet wird, kann ihn catch(MalformedURLException) nicht auffangen. |
Unter Java 6 war das etwas anders, denn hier wusste der Compiler nur, dass e irgendeine IOException ist, und es hätte ja durchaus die IOException-Unterklasse MalformedURLException sein können. (Warum MalformedURLException aber eine Unterklasse von IOException ist, steht auf einem ganz anderen Blatt.) |
1.1.6 Automatisches Ressourcen-Management (try mit Ressourcen)
Java hat eine automatische Garbage-Collection (GC), sodass nicht mehr referenzierte Objekte erkannt und ihr Speicher automatisch freigegeben wird. Nun bezieht sich die GC aber ausschließlich auf Speicher, doch es gibt viele weitere Ressourcen:
- Dateisystem-Ressourcen von Dateien
- Netzwerkressourcen wie Socket-Verbindungen
- Datenbankverbindungen
- nativ gebundene Ressourcen vom Grafiksubsystem
- 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 der try-catch-finally drei Unfeinheiten hat:
- Sollte eine Variable in finally zugänglich sein, muss sie außerhalb des try-Blocks deklariert werden, was ihr eine höhere 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.
1.1.7 try mit Ressourcen
Um das Schließen von Ressourcen zu vereinfachen, wurde in Java 7 eine besondere Form der try-Anweisung eingeführt, die try mit Ressourcen genannt werden. Mit ihm lassen sich Ressource-Typen, die die Schnittstelle java.lang.AutoCloseable implementieren, automatisch schließen. Ein-/Ausgabeklassen wie Scanner, InputStream und Writer, 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: Aus einer Datei soll mit einem Scanner die erste Zeile gelesen und ausgegeben werden. Nach dem Lesen soll der Scanner geschlossen werden. Die linke Seite der folgenden Tabelle nutzt die spezielle Syntax, die rechte Seite ist im Prinzip die Übersetzung, allerdings noch etwas vereinfacht, wie wir später genauer sehen werden.
try mit Ressourcen | Vereinfachte ausgeschriebene Implementierung |
InputStream in = |
InputStream in = |
Üblicherweise folgt nach dem Schlüsselwort try ein Block, doch try-mit-Ressourcen nutzt eine eigene spezielle Syntax.
- Nach dem try folgt statt dem direkten {}-Block eine Anweisung in runden Klammern, und dann erst der {}-Block, also try (...) {...} statt try {...}.
- In den runden Klammern findet man eine lokale Variablendeklaration (die Variable ist automatisch final) mit einer Zuweisung. Die Ressourcenvariable muss vom Typ AutoCloseable sein. Rechts vom Gleichheitszeichen steht ein Ausdruck, etwa ein Konstruktor- oder Methodenaufruf. Der Typ des Ausdrucks muss natürlich auch intanceof AutoCloseable sein.
Die in dem try deklarierte lokale AutoCloseable-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 einem Fehler kam. Der Compiler fügt alle nötigen Prüfungen ein.
1.1.8 Ausnahmen vom close() bleiben bestehen
Unser Scanner-Beispiel hat eine Besonderheit, denn keine der Methoden löst eine geprüfte Ausnahme aus – weder getSystemResourceAsStream(), new Scanner(InputStream), nextLine() noch das close(), was try-mit-Ressourcen automatisch aufruft. Anders ist es, wenn die Ressource ein InputStream ist, denn dort deklariert die close()-Methode eine IOException. Die muss daher auch behandelt werden, wie es das folgende Beispiel zeigt:
Listing 1.3: TryWithResourcesReadsLine, readFirstLine()
static String readFirstLine( File file )
{
try ( BufferedReader br = new BufferedReader(new FileReader(file) ) )
{
return br.readLine();
}
catch ( IOException e ) { e.printStackTrace(); return null; }
}
Wenn try-mit-Ressourcen verwendet wird, bleibt die Ausnahme bestehen; es zaubert die Ausnahmen beim catch also nicht weg.
Hinweis |
Löst close() eine geprüfte Ausnahme aus, und wird diese nicht behandelt, so kommt es zum Compilerfehler. Die close()-Methode vom BufferedReader löst zum Beispiel eine IOException aus, sodass sich die folgende Methode nicht übersetzen lässt: void no() Der Ausdruck new BufferedReader(null) benötigt keine Behandlung, denn der Konstruktor löst keine Ausnahme aus. Einzig der nicht behandelte Fehler von close() führt zu »exception thrown from implicit call to close() on resource variable 'r'«. |
1.1.9 Die Schnittstelle AutoCloseable
Die ARM-Anweisung schließt Ressourcen vom Typ AutoCloseable. Daher wird es Zeit, sich diese Schnittstelle etwas genauer anzuschauen:
package java.lang;
public interface AutoCloseable
{
void close() throws Exception;
}
Anders als das übliche close() ist die Ausnahme deutlich allgemeiner mit Exception angegeben; die Ein-/Ausgabe-Klassen lösen beim Misslingen immer eine IOException aus, aber jede Klasse hat eigene Ausnahmetypen:
Typ | Signatur |
java.io.Scanner |
close() // ohne Ausnahme |
javax.sound.sampled.Line |
close() // ohne Ausnahme |
java.io.FileInputStream |
close() throws IOException |
java.sql.Connection |
close() throws SQLException |
Eine Unterklasse darf die Ausnahme ja auch weglassen, das machen Klassen wie der Scanner, der keine Ausnahme weiterleitet, sondern sie intern schluckt – wenn es Ausnahmen gab, liefert sie die Scanner-Methode ioException().
AutoCloseable und Closeable
Auf den ersten Blick einleuchtend wäre es, die schon existierende Schnittstelle Closeable als Typ zu nutzen. Doch das hätte Nachteile: Die close()-Methode ist mit einem throws IOException deklariert, was bei einer allgemeinen automatischen Ressourcen-Freigabe unpassend ist, wenn etwa ein Grafikobjekt bei der Freigabe eine IOException auslöst. Vielmehr ist der Weg anders herum: Closeable erweitert AutoCloseable, denn das Schließen von Ein-/Ausgabe-Ressourcen ist eine besondere Art, allgemeine Ressourcen zu schließen.
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 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 freigeben werden könnte, oder Grafik-Anwendungen, bei denen der Grafik-Kontext wieder freigegeben werden muss, gehören nicht dazu.
1.1.10 Mehrere Ressourcen nutzen
Unsere beiden Beispiele zeigten die Nutzung eines Ressource-Typs. Es sind aber auch mehrere Typen möglich, die dann mit einem Semikolon getrennt werden:
try ( InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dest) )
{ ... }
Hinweis |
Die Trennung erledigt ein Semikolon, und jedes Segment kann einen unterschiedlichen Typ deklarieren, etwa InputStream/OutputStream. Die Ressourcen-Typen 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 |
Wenn es beim Anlegen in der Kette zu einem Fehler kommt, wird nur das geschlossen, was auch aufgemacht wurde. Wenn es also bei der ersten Initialisierung von in1 schon zu einer Ausnahme kommt, wird die Belegung von in2 erst gar nicht begonnen und daher auch nicht geschlossen. (Intern setzt der Compiler das als geschachtelte try-catch-finally-Blöcke um.)
Beispiel |
Am Schluss der Ressourcensammlung kann – muss aber nicht – ein Semikolon stehen, so wie auch bei Feldinitialisierungen zum Schluss ein Komma stehen kann: int[] array = { 1, 2, }; Ob das stilvoll ist, muss jeder selbst entscheiden; in der Insel steht kein unnützes Zeichen. |
1.1.11 Unterdrückte Ausnahmen *
Aufmerksame Leser haben bestimmt schon ein Detail wahrgenommen: Im Text steht »vereinfachte ausgeschriebene Implementierung«, was vermuten lässt, dass es ganz so einfach doch nicht ist. Das stimmt, denn es können zwei Ausnahmen auftauchen, die einiges an Sonderbehandlung benötigen:
- 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 alleine 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 von close() vielleicht immer verdeckt werden und immer nur die vom try-Block zum Anwender kommen?
- Wenn beide Ausnahmen 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.[3](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 »Haupt-Ausnahme«, und die close()-Ausnahme kommt Huckepack als Extra-Information mit oben drauf.
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.
Listing 1.4: SuppressedClosed.java
public class SuppressedClosed
{
public static void main( String[] args )
{
class NotCloseable implements AutoCloseable
{
@Override public void close()
{
throw new UnsupportedOperationException( "close() mag ich nicht" );
}
}
try ( NotCloseable res = new NotCloseable() ) {
throw new NullPointerException();
}
}
}
Das Programm löst also im close() und im try-Block eine Ausnahme aus. Das Resultat ist:
Exception in thread "main" java.lang.NullPointerException
at SuppressedClosed.main(SuppressedClosed.java:14)
Suppressed: java.lang.UnsupportedOperationException: close() mag ich nicht
at SuppressedClosed$1NotCloseable.close(SuppressedClosed.java:9)
at SuppressedClosed.main(SuppressedClosed.java:15)
Die interessante Zeile beginnt mit »Suppressed:«, denn dort ist die close()-Ausnahme referenziert. An den Aufrufer kommt die spannende Ausnahme vom misslungenen try-Block 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 SuppressedClosed$1NotCloseable.close(SuppressedClosed.java:9)
at SuppressedClosed.main(SuppressedClosed.java:15)
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 SuppressedClosed.main(SuppressedClosed.java:15)
Suppressed: java.lang.UnsupportedOperationException: close() mag ich nicht
at SuppressedClosed$1NotCloseable.close(SuppressedClosed.java:9)
at SuppressedClosed.main(SuppressedClosed.java:16)
Suppressed: java.lang.UnsupportedOperationException: close() mag ich nicht
at SuppressedClosed$1NotCloseable.close(SuppressedClosed.java:9)
at SuppressedClosed.main(SuppressedClosed.java:16)
Jede unterdrückte close()-Ausnahme taucht auf.
Umsetzung |
Im Kapitel über 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, sind in der Basisklasse Throwable seit Java 7 zwei Methoden hinzugekommen:
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 sie auch printStackTrace() nicht mehr an.
Blick über den Tellerrand |
In C++ gibt es Dekonstruktoren, 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, 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 bei Betreten eines Blockes __enter__() aufruft und beim Verlassen die Methode __exit__(). Das ist insofern interessant, als dass hier zwei Methoden zur Verfügung stehen. Bei Java ist es nur close() beim Verlassen des Blockes, aber es gibt keine Methode zum Betreten eines Blockes; so etwas muss beim Anlegen der Ressource erledigt werden. |
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.