Java 7: Project Coin II

Automatic Resource Management

Der Vorschlag für eine automatische Unterstützung solcher Klassen, die dynamisch angeforderte Ressourcen verwalten, stammt von Joshua Bloch und ist als ARM (Automatic Resource Management) bekannt [3], [4]. Damit wird ein Problem gelöst, das schon so alt ist wie Java selbst. In Java gibt es zwar Konstruktoren, in denen ein Objekt zu Beginn seiner Lebenszeit Ressourcen beschaffen kann, aber es gibt kein gutes Instrument, mit dem das Objekt seine Ressourcen beim Ableben automatisch loswerden könnte. Die Finalizer haben sich für diesen Zweck als wenig hilfreich erwiesen und so muss sich der Benutzer des Objekts meist selbst darum kümmern, dass die Ressourcen des Objekts freigegeben werden. Am Beispiel eines Input-Streams sieht es folgendermaßen aus:

String readFirstLineFromFile(String path) throws IOException {
  BufferedReader br = new BufferedReader(new FileReader(path));
  try     { return br.readLine(); }
  finally { br.close(); }
}

Der Benutzer des BufferedReader-Objekts muss dafür sorgen, dass die verwendete Datei wieder geschlossen wird. Dazu genügt nicht einmal ein einfacher Aufruf von close()am Ende der Methode, sondern der Benutzer benötigt eine finally-Klausel, damit die Datei auch im Fall einer Exception geschlossen wird.

In dieser Situation soll das Automatic Resource Management helfen. Die Idee besteht darin, dass Klassen mit dynamischen Ressourcen wie ein BufferedReader ein spezielles Interface AutoCloseable implementieren können (und im Fall von JDK-Klassen es auch tun):

public interface AutoCloseable {
  void close() throws Exception;
}

Eine Klasse, die AutoCloseable implementiert, wird als Resource Type bezeichnet. Dazu gibt es eine neue Form des try-Blocks, ein so genanntes „try-with-resources“, bei dessen Verlassen automatisch die close()-Methoden aller spezifizierten Resource-Typen aufgerufen werden. Das sähe zum Beispiel folgendermaßen aus:

String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
      return br.readLine();
    }
}

Bei diesem Beispiel muss man übrigens aufpassen, wie man das try-with-resources gestaltet. Die folgende Variante wäre ungünstig:

String readFirstLineFromFile(String path) throws IOException {
    try (FileReader    fin = new FileReader(path);
         BufferedReader br = new BufferedReader(fin)) {
      return br.readLine();
    }
}

Wenn anstelle von einer Ressource zwei Ressourcen aufgelistet werden, dann wird auch für beide Ressourcen die close()-Methode aufgerufen. Da aber die close()-Methode von BufferedReader ohnehin bereits die close()-Methode von FileReader aufruft, würde letztere zweimal gerufen. Das ist in diesem Fall zwar nicht völlig falsch, aber auch nicht besonders sinnvoll. In anderen Fällen kann es durchaus erwünscht und notwendig sein, dass für jede separat aufgelistete Ressource die jeweilige close()-Methode gerufen wird. Der Aufruf erfolgt übrigens in umgekehrter Reihenfolge der Deklaration, d. h. die erste Ressource wird als letzte weggeräumt.

Suppressed Exceptions: Bei einem solchen try-with-resources-Konstrukt kann es nun passieren, dass nach einer Exception aus dem try-Block noch eine weitere Exception beim automatischen Aufruf der close()-Methode ausgelöst wird. Bisher hat eine solche zweite Exception (z. B. aus einem finally-Block) die erste unterdrückt, denn es kann immer nur eine Exception an den Aufrufer der Methode weitergeleitet werden. Damit keine Information verloren geht, ist die Klasse Throwable um Informationen über unterdrückte Exceptions ergänzt worden, und hat entsprechende Methoden addSuppressedException() und getSuppressedExceptions(). Die Methode readFirstLineFromFile() im obigen Beispiel würde nun im JDK 7 die erste Exception aus dem try-Block propagieren, und diese Exception enthält die zweite unterdrückte Exception aus der close()-Methode. Ohne try-with-resources sähe es folgendermaßen aus:

void method() throws Exception {
  Resource r = new Resource();
  try { ... does something useful that may raise an exception ...
        throw new Exception("from try block");
  } catch (final Exception e) {
        ... does cleanup and re-throws initial exception ...
        throw e;
  } finally {
        r.close();
  } 
}

Es würde diejenige Exception weiter propagiert, die von der close()-Methode geworfen wurde. Eine Exception aus dem try-catch-Konstrukt ginge in einem solchen Fall verloren. Mit try-with-resources sähe es folgendermaßen aus:

void method() throws Exception {
  try (Resource r = new Resource()) {
     ... does something useful that may raise an exception ...
     throw new Exception("from try block");
  } catch (Exception e) {
     ... does cleanup and re-throws initial exception ...
     throw e;
  }
}

Die close()-Methode wird automatisch gerufen. Wenn sie eine Exception auslöst, dann wird diese Exception in eine zuvor im try-Block ausgelöste Exception als Suppressed Exception eingepackt. Propagiert würde dann die initiale Exception aus dem try-catch-Konstrukt mit der unterdrückten Exception aus close() im Gepäck. Auf diese Weise geht keine Exception-Information verloren.

Was passiert eigentlich, wenn ein try-with-resources-Konstrukt eine explizite finally-Klausel hat und darin auch eine Exception geworfen wird? Hier ist ein Beispiel:

void method() throws Exception {
  try (Resource r = new Resource()) {
     ... does something useful that may raise an exception ...
     throw new Exception("from try block");
  } finally {
     ... does something useful that may raise an exception ...
     throw new Exception("from finally block");
  }
}

Es sind nun drei Exceptions im Spiel: Diejenige aus dem try-Block, eine weitere aus der close()-Methode und noch eine dritte aus dem finally-Block. Welche Exception bekommt der Aufrufer zu sehen?

Da die close()-Methoden aller Ressourcen vor den Anweisungen im finally-Block aufgerufen werden, sind wir wieder in der Situation, in der wir ohne das neue try-with-resources-Konstrukt waren: Es wird nur die allerletzte Exception aus dem finally-Block geworfen; die Exception aus dem try-Block mit der eingepackten Exception aus der close()-Methode geht verloren.

Wie ist die Situation, wenn es noch komplizierter wird und es im try-with-resources-Konstrukt sowohl eine catch- als auch eine finally-Klausel gibt? Im Wesentlichen ist es so wie oben beschrieben: Nach Verlassen des try-Blocks werden die close()-Methoden der Ressourcen gerufen, danach geht es im catch- und anschließend im finally-Block weiter (Listing 2).

Listing 2
void method() throws Exception {
  try (Resource r = new Resource()) {     
     ... does something useful that may raise an exception ...
     throw new Exception("from try block");
  } 
                      Hier werden die close()-Methoden gerufen.
  catch(Exception e) {
     ... does cleanup and re-throws initial exception ...
     throw e;
  } 
  finally {
     ... final work ...
  }
}

Die Exception, die nach einer Exception im try-Block von einer close()-Methode ausgelöst wird, ist als Suppressed Exception in die erste Exception eingepackt. Wenn sie im catch-Block mit einem Rethrow propagiert wird, dann geht nichts verloren. Wenn allerdings im catch-Block kein Rethrow gemacht wird, sondern eine andere Exception geworfen wird, dann kommt beim Aufrufer nur diese andere Exception an und der Rest der Exception-Information geht verloren. Dasselbe passiert, wenn danach im finally-Block noch eine Exception geworfen wird: Dann wird nur diese allerletzte Exception weitergegeben und alles andere ist verloren. Sinnvoll ist also im catch-Block nur Folgendes:

  • Der Rethrow der initialen Exception, die dann automatisch die Exception von einer close()-Methode als unterdrückte Exception mitbringt.
  • Das Behandeln der initialen Exception; nach der Behandlung wird dann keine Exception mehr propagiert.
  • Wenn man eine andere als die initiale Exception werfen will, dann sollte man explizit die vorangegangene Exception eingepackt mitliefern. Das würde man aber nicht als unterdrückte Exception machen, sondern als Ursache, d. h. man würde den Exception-Chaining-Mechanismus verwenden, den es schon seit Java 1.4 gibt. In Listing 3 ist ein Beispiel dargestellt.
Listing 3
void method() throws Exception1, Exception2 {
  try (Resource r = new Resource()) {
     ... does something useful that may raise an exception ...
  } catch (Exception1 initialExc) {
     ... does cleanup and re-throws initial exception ...
     throw initialExc;
  }
  catch(Exception2 initialExc) {
     ... handle exception ...  
  } 
  catch(Exception3 initialExc) {
     ... re-map exception ...  
     throw new Exception4(initialExc);
  } 
  finally {
     ... final work - no exceptions ...
  }
}

Wenn nun beispielsweise im try-Block eine Exception3 ausgelöst wird, dann wird sie im dritten catch-Block gefangen und auf eine Exception4 abgebildet. Der Aufrufer bekommt diese Exception4, in der als Ursache (siehe Throwable.getCause()) die initiale Exception steckt, in der die unterdrückte Exception von der close()-Methode enthalten ist.

Ursache vs. Unterdrückung: Es mag ein wenig verwirrend erscheinen, dass es nun zwei Mechanismen für das Einpacken von Exceptions in andere Exceptions gibt: unterdrückte Exceptions (siehe Throwable.getSuppressed()) und die Ursache (siehe Throwable.getCause()). Eine unterdrückte Exception wird automatisch erzeugt im Zusammenhang mit dem neuen try-with-resources-Konstrukt. Man kann weitere unterdrückte Exceptions mit Throwable.addSuppressed()selber hinzufügen. Eine Exception kann also beliebig viele unterdrückte Exceptions enthalten. Unterdrückte Exceptions entstehen „gleichzeitig“ und gehören zur selben Fehlersituation; es handelt sich um einen initialen Fehler und seine Folgefehler.

Eine Ursache hingegen entsteht beim Abbilden einer Exception auf eine andere Exception. Das ist der Exception-Chaining-Mechanismus, den wir schon seit Java 1.4 kennen und der in geschichteten Architekturen häufig vorkommt. Die Exception-Kette entsteht nie automatisch, sondern sie muss ausdrücklich mit Throwable.initCause()oder durch die Benutzung des Exception-Konstruktor hergestellt werden. Dabei enthält jede Exception nur genau eine Ursache, die wiederum genau eine Ursache enthält usw. Die Ursachen entstehen „nacheinander“ und beschreiben eine Abbildungskette über einen Call Stack hinweg.

Zusammenfassung

In diesem Beitrag haben wir einige der neuen Sprachmittel aus dem Project Coin betrachtet. Die Verbesserungen beim Exception Handling bestehen aus einem Multi-Catch, der redundanten Code beim Exception Handling vermeidet, und einem verbesserten Rethrow, bei dem der Compiler die Exception-Typen automatisch deduziert. Das Automatic Resource Management sorgt dafür, dass für so genannte Resource-Typen die Ressourcenfreigabe automatisch angestoßen wird und dabei keine Exceptions verloren gehen. Dazu wurde ein try-with-resources-Konstrukt erfunden und die Exceptions können nun unterdrückte Exceptions enthalten. Die übrigen Sprachverbesserungen im Zusammenhang mit Generics werden wir im nächsten Artikel besprechen.

Angelika Langer arbeitet selbstständig als Trainer mit einem eigenen Curriculum von Java- und C++-Kursen. Kontakt: http://www.AngelikaLanger.com.

Klaus Kreft arbeitet selbstständig als Consultant und Trainer. Kontakt: http://www.AngelikaLanger.com.

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.