1. Exceptions

Unvorhergesehene Fehler können zu jeder Zeit auftreten. Unsere Programme müssen darauf vorbereitet sein und mit dieser Situation umgehen können. In den nächsten Aufgaben soll es darum gehen, Ausnahmen abzufangen, zu behandeln und selbst Probleme über Ausnahmen zu melden.

Voraussetzungen

  • Notwendigkeit für Ausnahmen verstehen

  • geprüfte und ungeprüfte Ausnahmen unterscheiden können

  • Ausnahmen mit try-catch abfangen können

  • Ausnahme-Weiterleitung mit throws kennen

  • Ausnahmen mit throw auslösen können

  • eigene Ausnahmeklassen schreiben können

  • Ressourcen mit try-mit-Ressourcen schließen können

Verwendete Datentypen in diesem Kapitel:

1.1. Exception fangen

Geprüfte Ausnahmen müssen abgefangen oder nach oben an den Aufrufer weitergeleitet werden. Bei geprüften Ausnahmen zwingt uns der Compiler dazu, bei ungeprüften Ausnahmen gibt es diese Pflicht nicht — falls wir jedoch eine RuntimeException nicht behandeln, führt das zum Abbruch des ausführenden Threads. Daher empfiehlt es sich, auch ungeprüfte Ausnahmen immer abzufangen und zumindest zu loggen.

1.1.1. Die längste Zeile einer Datei ermitteln ⭐

Erfolgreiche Piraten müssen ein gutes Gedächtnis haben, und Captain CiaoCiao möchte testen, ob alle blitzgescheit denken können. Er liest allen für einen Test eine Liste von Namen vor. Am Ende der Liste müssen alle den längsten Namen aufsagen können. Da Captain CiaoCiao aber zu sehr mit dem Vorlesen beschäftigt ist, soll eine Software am Ende den längsten Namen ausgeben.

Aufgabe:

  1. Die Datei http://tutego.de/download/family-names.txt enthält Familiennamen. Speichere die Datei lokal auf dem eigenen Dateisystem.

  2. Lege eine neue Klasse LongestLineInFile mit einer main(…​)-Methode an.

  3. Setze die Files.readAllLines(…​) in die main(…​)-Methode.

  4. Welche Ausnahme(n) muss/müssen aufgefangen werden?

  5. Was ist der längste Name (gemäß der String length()) in der Datei?

  6. Bonus: Welches sind die zwei längsten Namen in der Datei?

Eine Datei kann man in Java so einlesen:

String filename = ...
List<String> lines = Files.readAllLines( Paths.get( filename ) );

1.1.2. Ausnahmen ermitteln, Lachen am laufenden Band ⭐

Entwickler müssen im Blick haben, welche

  • Sprachkonstrukte,

  • Konstruktoren,

  • Methoden

Ausnahmen auslösen. Nur bei geprüften Ausnahmen gibt die IDE einen Hinweis, aber nicht zum Beispiel bei jedem Array-Zugriff, wo auch prinzipiell eine ArrayIndexOutOfBoundsException ausgelöst werden könnte.

Aufgabe:

  • Welche Ausnahmen müssen wir abfangen, wenn wir folgenden Block übersetzen möchten? Nutze nur die Javadoc, um das herauszufinden.

    Clip clip = AudioSystem.getClip();
    clip.open( AudioSystem.getAudioInputStream( new File("") ) );
    clip.start();
    TimeUnit.MICROSECONDS.sleep( clip.getMicrosecondLength() + 50 );
    clip.close();
  • Kann/sollte man Ausnahmen zusammenfassen?

  • Optionale Erweiterung: Finde ein paar Lacher-Dateien im Internet (unter https:// soundbible.com/tags-laugh.html gibt es zum Beispiel freie WAV-Dateien). Speichere die WAV-Dateien lokal. Spiele in einer Endlosschleife zufällige Lacher hintereinander ab.

1.1.3. String-Array in int-Array konvertieren und nachsichtig bei Nichtzahlen sein ⭐

Die Methode Integer.parseInt(String) konvertiert einen String in eine Ganzzahl vom Typ int und löst eine NumberFormatException aus, wenn keine Konvertierung möglich ist, etwa bei Integer.parseInt("0x10") oder Integer.parseInt(null). Die Java-Bibliothek bietet keine Methode zur Konvertierung eines String-Arrays mit Zahlen in ein int-Array.

Aufgabe:

  • Schreibe eine neue Methode static int[] parseInts(String... numbers), die alle gegebenen Strings in Ganzzahlen konvertiert.

  • Die Anzahl der übergebenen Strings bestimmt die Größe des Rückgabe-Arrays.

  • Wenn ein String im Array an einer Stelle nicht konvertiert werden kann, kommt an die Stelle eine 0. null als Argument in der Übergabe ist erlaubt und führt zu 0.

  • Der Aufruf von parseInts() ohne Argumente ist in Ordnung, aber parseInts(null) muss zu einer Ausnahme führen.

Beispiel:

String[] strings = { "1", "234", "333" };
int[] ints1 = parseInts( strings );                                // [1, 234, 333]
int[] ints2 = parseInts( "1", "234", "333" );                      // [1, 234, 333]
int[] ints3 = parseInts( "1", "ll234", "3", null, "99" );          // [1, 0, 3, 0, 99]
int[] ints4 = parseInts( "Person", "Woman", "Man", "Camera, TV" ); // [0, 0, 0, 0, 0]

1.1.4. Quiz: Und zum Schluss finally ⭐

Wie lautet die Ausgabe des folgenden Java-Programms?

public class TryCatchFinally {
  public static void main( String[] args ) {
    try {
      System.out.println( 1 / 0 );
      System.out.println( "I’m gettin’ too old to jump out of cars." );
    }
    catch ( Exception e ) {
      System.out.print( "That’s why everybody talks about you." );
    }
    finally {
      System.out.println( "Frankly, my dear, I don’t give a damn." );
    }
  }
}

1.1.5. Quiz: Ein einsames try ⭐

Gibt es einen try-Block ohne catch?

1.1.6. Quiz: Gut gefangen ⭐

Bei Ausnahmeklassen spielt Vererbung eine wichtige Rolle. Jede Ausnahmeklasse ist von einer Oberklasse abgeleitet, etwa IOException von Exception, Exception selbst von Throwable. Diese Klassenhierarchie kommt zur Geltung, wenn Ausnahmen aufgefangen werden. Prinzipiell ist es mit einem einzigen catch-Block möglich, auf alle Ausnahmen zu reagieren, die in einem Stückchen Programmcode auftauchen:

try {
  // Tu was
} catch ( Exception e ) {   // oder ( Throwable e )
  // Loggen
}

Die Ausnahmebehandlung könnte im catch-Block alle Ausnahmen abfangen, das heißt ein catch (Exception e) oder catch (Throwable e) nutzen. Ist das gut oder schlecht?

1.1.7. Quiz: Zu viel des Guten ⭐

Wie ist die Reaktion des folgenden Java-Programms?

public class TooMuchMemory {
  public static void main( String args[] ) {
    try {
      byte[] bytes = new byte[ Integer.MAX_VALUE ];
    }
    catch ( Throwable e ) {
      System.out.println( "He had the detonators." );
      e.printStackTrace();
    }
  }
}

1.1.8. Quiz: try-catch in Vererbung ⭐⭐

Angenommen, die Klasse Pudding will die Schnittstelle Eatable implementieren und calories() realisieren:

interface Eatable {
  void calories() throws IOException;
}

class Pudding implements Eatable {
  @Override
  public void calories() ??? {
  }
}

Was für eine throws-Klausel muss bei calories() in Pudding anstelle der drei Fragezeichen stehen?

1.2. Eigene Ausnahmen auslösen

Ausnahmen haben ihren Ursprung in:

  • der falschen Nutzung gewisser Sprachkonstrukte, wie z. B. der Ganzzahldivision durch 0, der Dereferenzierung über eine null-Referenz, einer unmöglichen Typanpassung bei Objekten, fehlerhaftem Array-Zugriff usw.

  • explizit erzeugten Ausnahmen durch das Schlüsselwort throw. Hinter throw steht eine Referenz auf ein Ausnahmeobjekt. Dieses Objekt wird im Regelfall mit new neu aufgebaut. Die Typen stammen entweder aus Bibliotheken, wie z. B. IOException, es können aber auch eigene Ausnahmeklassen sein, die von Throwable abgeleitet werden müssen, im Regelfall aber Unterklassen von Exception sind.

1.2.1. Quiz: throw und throws ⭐

Was ist der Unterschied zwischen den Schlüsselwörtern throw und throws? Wo stehen die Schlüsselwörter?

1.2.2. Quiz: The Division fails ⭐

Lässt sich das nachfolgende Programm übersetzen? Wenn ja, und wenn wir es ausführen, was gibt es für ein Ergebnis?

class Application {
 public static void main( String[] args ) {
   try { throw 1 / 0; }
   catch ( int e ) { e.printStackTrace(); }
 }
}

1.3. Eigene Ausnahmeklassen schreiben

Die Java SE bietet eine große Anzahl von Ausnahmetypen, doch sie sind in der Regel technologieabhängig, wenn es zum Beispiel eine Zeitüberschreitung im Netzwerk gibt oder der SQL-Befehl falsch ist. Das ist für Low-level-Funktionalität in Ordnung, doch Software ist in Schichten aufgebaut, und in der äußersten Schicht geht es vielmehr um die Konsequenz diese Low-Level-Events: Es gab eine IOException → Konfiguration konnte nicht geladen werden; es gab eine SQLException → Kundendaten konnten nicht aktualisiert werden usw. Diese Ausnahmen werden durch neue semantische Exception-Klassen modelliert.

1.3.1. »Watt ist unmöglich« mit eigener Ausnahme anzeigen ⭐

Elektrogeräte ohne Leistungsaufnahme gibt es nicht, genau wie negative Wattwerte.

Aufgabe:

  • Erstelle eine eigene Ausnahmeklasse IllegalWattException. Leite die Klasse von RuntimeException ab.

  • Die Ausnahme soll immer dann ausgelöst werden, wenn bei setWatt(watt) die Wattzahl kleiner oder gleich Null ist.

  • Teste das Auftreten der Ausnahme durch Auffangen.

1.3.2. Quiz: Kartoffeln oder anderes Gemüse ⭐

Lässt sich das folgende Programm erfolgreich übersetzen?

class VegetableException extends Throwable { }

class PotatoException extends VegetableException { }

class PotatoVegetable {
 public static void main( String[] args ) {
   try { throw new PotatoException(); }
   finally { }
   catch ( VegetableException e ) { }
   catch ( PotatoException e ) { }
 }
}

1.4. try-mit-Ressourcen

Eine wichtige Regel ist: Wer etwas aufmacht, macht es auch wieder zu. Schnell wird das vergessen, und dann gibt es nicht geschlossene Ressourcen, die zu Problemen führen können. Dazu zählen Datenverlust und Speicherprobleme. Damit Entwickler Ressourcen möglichst einfach schließen können, gibt es eine besondere Schnittstelle AutoCloseable und ein Sprachkonstrukt, die das Schließen kurz und einfach machen. Das spart Codezeilen ein und hilft, Fehler zu vermeiden, wenn zum Beispiel auch beim Schließen selbst wieder Ausnahmen ausgelöst werden.

1.4.1. Aktuelles Datum in Datei schreiben ⭐

Ein java.io.PrintWriter ist eine einfache Klasse zum Schreiben von Textdokumenten und kann auch direkt in Dateien schreiben.

Aufgabe:

  • Studiere die Javadoc von PrintWriter.

  • Finde heraus, wie man einen PrintWriter mit einer Ausgabedatei verbindet. Die Zeichenkodierung soll die der Plattform sein.

  • Schließe den PrintWriter korrekt mit try-mit-Ressourcen.

  • Das Java-Programm soll die String-Repräsentation von LocalDateTime.now() in die Textdatei schreiben.

1.4.2. Noten einlesen und in eine neue ABC-Datei schreiben ⭐⭐

Der berühmte Komponist Amadeus van Barsch bespricht am Telefon seine neusten Werke. Captain CiaoCiao ist ein großer Fan vom Komponisten und besorgt sich heimlich einen Audiomitschnitt, der als transkribierte Textdatei geliefert wird. Sie enthält alle Noten untereinander, wie:

C
D
C

Die Aufgabe gliedert sich in zwei Teile.

Aufgabenteil A, aus Datei lesen:

  • java.util.Scanner ist eine einfache Klasse zum Lesen und Verarbeiten von Textressourcen. Studiere in der Javadoc die Konstruktoren.

  • Im Konstruktor Scanner lassen sich diverse Quellen angeben, unter anderem Path; öffne einen Scanner, und übergib im Konstruktor über Paths.get("file.txt") eine Datei. Schließe den Scanner korrekt mit try-mit-Ressourcen.

  • Die Methoden hasNextLine() und nextLine() sind von besonderem Interesse. Lies eine Textdatei zeilenweise ein, und gib alle Zeilen auf der Konsole aus. Wenn zum Beispiel die Eingabedatei die Zeilen

    C,
    d
    d'

    enthält, ist die Ausgabe auf dem Bildschirm

    C, d d'
  • Erweitere das Programm so, dass nur die Zeilen mit Inhalt berücksichtigt werden. Wenn die Datei zum Beispiel eine Leerzeile enthält oder eine Zeile mit ausschließlich Weißraum, also Leerzeichen oder Tabulatorzeichen, wird sie nicht ausgegeben. Beispiel:

    C,
    
    
    d

    führt zu

    C, d
  • Nur gültige Noten dürfen erkannt werden, das sind

    C, D, E, F, G, A, B, C D E F G A B c d e f g a b c' d' e' f' g' a' b'

    Bedenke, dass das Komma an den Großbuchstaben kein Trenner, sondern ein Teil der Notenangabe ist, wie auch das Hochkomma an den Kleinbuchstaben der letzten Oktave. Barsch verwendet die internationale Schreibweise, schreibt also b statt h.

Captain CiaoCiao möchte sich die neue Komposition anhören und auf einem Notenblatt sehen. Hier bietet sich die Notation ABC an; eine Datei mit Noten von C, bis b' sieht so aus:

M:C
L:1/4
K:C
C, D, E, F, G, A, B, C D E F G A B c d e f g a b c' d' e' f' g' a' b'

Unter https://www.abcjs.net/abcjs-editor.html kann man die ABC-Datei anzeigen und sogar vorspielen lassen.

Image 290520 033932.511
Abbildung 1. Notenanzeige von https://www.abcjs.net/abcjs-editor.html

Die Grundidee vom Algorithmus ist die folgende: wir gehen mithilfe der Klasse Scanner zeilenweise durch die Datei und prüfen, ob der Inhalt der Zeile, die Note, in einer Datenstruktur vorkommt, die wir zur Validierung einsetzen. Handelt es sich um eine gültige Note, schreiben wir sie in das gewünschte Ausgabeformat.

Das Programm zum Einlesen der Noten soll ergänzt werden um einen Schreibanteil.

Aufgabenteil B, in Datei schreiben:

  • Öffne mit dem PrintWriter eine zweite Datei zum Schreiben; im Konstruktor kann man einen Dateinamen direkt übergeben. Achtung: Wähle einen anderen Dateinamen als die Quelldatei, sonst wird die Datei überschrieben!

  • Lies die Datei aus dem ersten Aufgabenteil weiterhin ein, aber schreibe die Ausgabe nicht mehr auf die Konsole, sondern in die neue Datei, sodass eine gültige ABC-Datei entsteht. Der PrintWriter bietet die von System.out bekannten Methoden print(String) und println(String).

  • Hinweis: Beide Ressourcen können (und sollten auch) in einem gemeinsamen try-mit-Ressourcen stehen.

1.4.3. Quiz: Ausgeschlossen ⭐

Gegeben ist in der main(…​)-Methode folgender Programmcode:

class ResourceA implements AutoCloseable {
  @Override public void close() {
    System.out.println( "close() ResourceA" );
  }
}

class ResourceB implements AutoCloseable {
  private final ResourceA resourceA;

  public ResourceB( ResourceA resourceA ) {
    this.resourceA = resourceA;
  }

  @Override public void close() {
    resourceA.close();
    System.out.println( "close() ResourceB" );
  }
}

// Version 1
try ( ResourceA resourceA = new ResourceA();
      ResourceB resourceB = new ResourceB( resourceA ) ) {
}

// Version 2
try ( ResourceB resourceB = new ResourceB( new ResourceA() ) ) {
}
  1. Welche Ausgaben folgen, wenn das Programm ausgeführt wird?

  2. Oftmals werden Ressourcen verschachtelt, und bei try-mit-Ressourcen gibt es zwei Varianten. Wie unterscheiden sich die Varianten, und welche Vor- und Nachteile haben sie?

1.5. Lösungsvorschläge

1.5.1. Die längste Zeile einer Datei ermitteln

Listing 1. com/tutego/exercise/util/LongestLineInFile.java
String filename = "src\\main\\resources\\com\\tutego\\exercises\\util\\family-names.txt";
try {
  Collection<String> lines = Files.readAllLines( Paths.get( filename ) );
  String first = "", second = "";
  for ( String line : lines ) {
    if ( line.length() > first.length() ) {
      second = first;
      first = line;
    }
    else if ( line.length() > second.length() )
      second = line;
  }
  System.out.println( first + ", " + second );
}
catch ( IOException e ) {
  System.err.println( "Error reading file "
                      + new File( filename ).getAbsolutePath() );
  e.printStackTrace();
}

Alles, was in Java mit einem Datenspeicher zu tun hat, kann Ausnahmen auslösen. Die Java-Standardbibliothek setzt standardmäßig auf geprüfte Ausnahmen, für Ein-/Ausgabe-Operationen finden wir IOException. Frameworks und Open-Source-Bibliotheken setzen vermehrt ungeprüfte Ausnahmen ein, weil es praktischer ist, wenn die Fehler einfach hochgehen können, bis es einen Behandler gibt.

Ein Blick auf die Javadoc von Files zeigt, dass eine IOException ausgelöst wird:

public static List<String> readAllLines(Path path) throws IOException

Read all lines from a file. Bytes from the file are decoded into characters using the UTF-8 charset.

Throws: IOException - if an I/O error occurs reading from the file or a malformed or unmappable byte sequence is read

Egal, ob geprüfte oder ungeprüfte Ausnahmen, wir sollten auf Ausnahmen reagieren. Dafür bietet Java zwei Möglichkeiten:

  • Wir könnten entweder an die Methode ein throws schreiben, was die Ausnahme dann an den Aufrufer der Methode weiter reicht, oder

  • wir behandeln den Fehler mit einem try-catch-Block.

Da IOException eine geprüfte Ausnahme ist, müssen wir die Ausnahme behandeln. Unsere Lösung verwendet einen try-catch-Block. Kommt es zu einer Ausnahme, wird der catch-Block abgearbeitet. Dann folgt eine Ausgabe auf dem Standardfehlerkanal, und die Methode printStackTrace() gibt einen Callstack ebenfalls auf der Kommandozeile aus. printStackTrace() stammt aus Throwable, der Basisklasse aller Ausnahmeklassen. Es ist Geschmackssache, ob man diese Form der Ausgabe haben möchte; in Produktivsoftware findet sich so etwas nicht, hier würde man zum Melden von Ausnahmen einen Logger verwenden.

Die gewählte Lösung arbeitet wie folgt: Wir merken uns die längste und zweitlängste Zeile in den Variablen first und second. Die erweiterte for-Schleife läuft über alle gelesenen Zeilen und untersucht sie. Jetzt wird jede Zeile line betrachtet. Ist die Länge von line größer als die Länge der ersten gespeicherten Zeichenfolge first, dann haben wir einen neuen Kandidaten für eine längste Zeile gefunden, sodass die bisher längste Zeile nun die zweitlängste Zeile darstellt. Wenn first aber länger als line war, wird nichts aktualisiert, aber line kann immer noch länger als second gewesen sein, also testen wir erneut mit der zweiten gespeicherten Zeile.

Am Ende der for-Schleife geben wir die erste und zweite Zeile aus.

Es gibt noch einen anderen Lösungsweg: Wir könnten die Liste mit einem Comparator nach der Länge der Zeilen sortieren, doch eine Sortierung ist irrelevant, wir brauchen nur die beiden längsten Zeilen. Wenn es auf die Performance nicht ankommt, ist aber das Sortieren die kürzeste Lösung. Zumal das Sortieren den Vorteil hat, dass die Aufgabe leicht erweitert werden kann, wenn nicht nur die ersten beiden längsten Zeilen gefragt werden, sondern z. B. die ersten zehn längsten Zeilen.

1.5.2. Ausnahmen ermitteln, Lachen am laufenden Band

Ausnahmen können ausgelöst werden durch fehlerhafte Programmkonstrukte, etwa eine Division durch 0 oder die Dereferenzierung einer null-Referenz. Dazu kann es geprüfte Ausnahmen geben oder ungeprüfte Ausnahmen. Methoden und Konstruktoren können diese Ausnahmen auslösen. Um zu verstehen, welche Ausnahmen wir auslösen müssen, müssen wir uns alle Methoden und Konstruktoren anschauen:

Tabelle 1. Ausnahmen des Konstruktors und Methoden, † erst ab Java 9
Methode/Konstruktorgeprüfte Ausnahme(n)ungeprüfte Ausnahme(n)

getClip()

LineUnavailableException

SecurityException, IllegalArgumentException

File(…​)

NullPointerException

getAudioInputStream(…​)

UnsupportedAudioFileException, IOException

NullPointerException

open(…​)

LineUnavailableException, IOException

IllegalArgumentException, IllegalStateException, SecurityException

sleep(…​)

InterruptedException

close()

SecurityException

Wir können gut ablesen, dass der Konstruktor bzw. die Methoden eine große Anzahl von Ausnahmen auslösen können. Die geprüften Ausnahmen werden dabei in der Java-Dokumentation immer hinter dem throws aufgeführt, die ungeprüften Ausnahmen aber in der Java-Dokumentation selbst und nicht hinter dem throws, denn es ist ungewöhnlich, dass dort ungeprüfte Ausnahmen auftauchen. Bei den ungeprüften Ausnahmen ist nicht immer bekannt, was ausgelöst werden kann; die Tabelle listet auf, was die Javadoc für ungeprüfte Ausnahmen dokumentiert.

Wer die Ausnahmen behandeln möchte, sollte sich überlegen, ob er Ausnahmen zusammenfassen sollte bzw. wie die Vererbungshierarchie aussieht, um eine ganze Kategorie von Ausnahmen aufzufangen.

AudioSystem Exceptions UML
Abbildung 2. UML-Diagramm der Vererbungsbeziehung

Das nachfolgende Programm fängt alle Ausnahmen ab und gibt Meldungen aus, bis auf InterruptedException, denn das sind Unterbrechungen vom sleep() und müssen nicht bearbeitet werden. Im Fall von RuntimeException sind es Programmierfehler auf der Seite der Softwareentwickler, deswegen geben wir den Stacktrace auf dem Logger-Kanal aus.

Listing 2. com/tutego/exercise/lang/exception/LaughingMyArseOff.java
static void play( String filename ) {
  try {
    Clip clip = AudioSystem.getClip();
    clip.open( AudioSystem.getAudioInputStream( new File( filename ) ) );
    clip.start();
    TimeUnit.MICROSECONDS.sleep( clip.getMicrosecondLength() + 50 );
    clip.close();
  }
  catch ( LineUnavailableException e ) {
    System.err.println( "Line cannot be opened because it is unavailable" );
  }
  catch ( IOException e ) {
    System.err.println( "An I/O exception of some sort has occurred" );
  }
  catch ( UnsupportedAudioFileException e ) {
    System.err.printf(
      "File %s did not contain valid data of a recognized file type and format%n",
      filename );
  }
  catch ( InterruptedException e ) {
    // No-op
  }
  catch ( RuntimeException e ) {
    Logger.getLogger( LaughingMyArseOff.class.getSimpleName() )
          .log( Level.SEVERE, e.getMessage(), e );
  }
}

Eine Auswahl verschiedener Lacher findet nicht statt, die eigene Methode play(String) kann aber gut in eine eigene Schleife gesetzt werden.

1.5.3. String-Array in int-Array konvertieren und nachsichtig bei Nichtzahlen sein

Listing 3. com/tutego/exercise/lang/exception/StringsToInteger.java
private static int parseIntOrElse( String number, int defaultValue ) {
  try {
    return Integer.parseInt( number );
  }
  catch ( NumberFormatException e ) {
    return defaultValue;
  }
}

public static int[] parseInts( String... numbers ) {
  int[] result = new int[ numbers.length ];

  for ( int i = 0; i < numbers.length; i++ )
    result[ i ] = parseIntOrElse( numbers[ i ], 0 );

  return result;
}

Unsere eigentliche Methode parseInts(…​) bekommt ein Array von Strings, die einzeln in Ganzzahlen konvertiert werden müssen. Bei der Konvertierung in Ganzzahlen über Integer.parseInt(String) gibt es eine Ausnahme, wenn die Zeichen nicht alle aus Ziffern besteht. Diese Konvertierung von einem String in eine Ganzzahl wollen wir in eine eigene Methode parseIntOrElse(String, int) auslagern. Die Methode bekommt einen String und einen Default-Wert, falls der String nicht in eine Ganzzahl konvertiert werden kann. Die Methode fängt die Ausnahme, die bei einer fehlerhaften Konvertierung auftritt, ab und liefert dann den Default-Wert zurück, andernfalls den konvertierten Wert. Eine NumberFormatException gibt es auch im Fall von Integer.parseInt(null), für die wir folglich automatisch den Default-Wert bekommen.

Wurde die Methode parseInts(String...) mit dem Argument null aufgerufen, führt numbers.length zu einer NullPointerException, was gewollt ist. Wenn das Argument ungleich null ist, legen wir ein neues Array mit Ganzzahlen an, das genauso groß ist wie das übergebene String-Array. Die for-Schleife läuft über das String-Array und übermittelt jeden Wert in die parseIntOrElse(…​)-Methode, wobei als Default-Wert 0 übergeben wird. So bekommen wir immer ein Ganzzahlergebnis zurück, entweder das konvertierte oder 0. Zum Schluss geben wir das Array zurück.

Fortgeschrittene Java-Entwickler können sich dran machen und die Schleife ersetzen. Denn es existiert die nützliche Arrays-Methode setAll(…​), die so implementiert ist:

Listing 4. OpenJDK-Implementierung von setAll(…​)
public static void setAll(int[] array, IntUnaryOperator generator) {
  Objects.requireNonNull(generator);
  for (int i = 0; i < array.length; i++)
    array[i] = generator.applyAsInt(i);
}

Das Besondere bei dieser Methode ist, dass nicht wir über das Array mit einer eigenen Schleife laufen, sondern das macht die Methode setAll(…​) für uns. Der Aufrufer muss nur das Array übergeben und ein spezielles Objekt vom Typ IntUnaryOperator, das als Funktion zu verstehen ist, die den Index (int) auf ein Objekt überträgt, das als Element im Array gespeichert wird.

Unsere Schleife können wir somit ersetzen:

Listing 5. com/tutego/exercise/lang/exception/StringsToInteger.java
public static int[] parseInts( String... numbers ) {
  int[] result = new int[ numbers.length ];
  Arrays.setAll( result, index -> parseIntOrElse( numbers[ index ], 0 ) );
  return result;
}

Die Schreibweise ist hier mit einem Lambda-Ausdruck formuliert, etwas, was wir später noch kennenlernen werden.

1.5.4. Quiz: Und zum Schluss finally

Das Programm lässt sich übersetzen, und wenn wir es ausführen, gibt es zur Laufzeit eine Ausnahme durch die Division durch 0, eine ArithmeticException. Dadurch kommt es nicht zur Ausgabe im try-Block. Die Ausnahme wird im catch-Block abgefangen, denn eine ArithmeticException ist eine besondere Exception, und das führt zur ersten Ausgabe:

That’s why everybody talks about you.

Da außerdem ein finally-Block folgt, dessen Code immer ausgeführt wird — egal, ob es zu einer Ausnahme kam oder nicht —, erscheint auf dem Bildschirm:

Frankly, my dear, I don’t give a damn.

1.5.5. Quiz: Ein einsames try

Ja, es ist durchaus legitim, dass es nur einen try-finally-Block gibt:

try {
} finally {
}

Solche Blöcke kann man verwenden, wenn eine Nachbearbeitung unabhängig von einer möglichen Ausnahme nötig ist, aber mögliche Ausnahmen nach oben weitergegeben werden sollen.

Ein try-Block ohne catch und ohne finally ist nicht korrekt. Allerdings gibt es ein try-mit-Ressourcen ohne catch oder finally, das automatisch Ressourcen schließt und nicht zwangsläufig mit einer Ereignisbehandlung assoziiert ist.

1.5.6. Quiz: Gut gefangen

Das allgemeine Abfangen jeder Ausnahme ist nicht zu empfehlen. Bei dieser Schreibweise werden oftmals Ausnahmen mit aufgefangen und behandelt, die überhaupt nicht zur gleichen Fehlerklasse gehören, etwa Programmierfehler, die eine NullPointerException auslösen.

Der Basistyp Throwable ist noch ein wenig schlimmer, weil unter Throwable auch Error steht, sodass catch (Throwable e) zum Beispiel ein StackOverflowError mit abfängt. Wer eine Ausnahme abfängt, der möchte sie auch behandeln. Ein Error zeigt aber in der Regel ein Problem der JVM an, das man nicht behandeln kann.

1.5.7. Quiz: Zu viel des Guten

Das Programm compiliert und wird ausgeführt, doch der Versuch, ein extrem großes byte-Array anzulegen, führt zu einem OutOfMemoryError.

Eine Besonderheit von Java ist, dass auch diese harten Fehler, die die virtuelle Maschine als Error-Ausnahmen auslöst, abgefangen werden können, denn Error-Objekte sind ebenfalls Untertypen von Throwable.

OutOfMemoryError UML
Abbildung 3. UML-Diagramm: OutOfMemoryError ist ein besonderes Throwable

Unser Programm fängt also erfolgreich OutOfMemoryError ab und gibt eine Konsolenausgabe, als ob nichts passiert wäre:

He had the detonators.
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
at TooMuchMemory.main(T.java:66)

Error-Objekte abzufangen, ist hochgradig kritisch, denn ein Error zeigt einen problematischen Zustand innerhalb der JVM an. Einfach weiterzumachen, kann zu unvorhergesehenen Fehlern führen. Die Javadoc schreibt bei java.lang.Error:

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.

Wenn man Error nicht behandeln kann, sollte ein Programm den Typ auch nicht auffangen und die JVM besser beenden lassen. Es mag allerdings Fälle geben, wo man einen Error abfangen möchte. Es könnte zum Beispiel ein Programm versuchen, native Bibliotheken zu laden, und, wenn das nicht gelingt, einen anderen Weg wählen.

1.5.8. Quiz: try-catch in Vererbung

Wenn Klassen Methoden von Schnittstellen implementieren, dann kann die Methode in der Klasse an manchen Stellen etwas anders aussehen als in der Schnittstelle:

  • Der Methodenname muss identisch zu dem in der Schnittstelle sein, doch beim Rückgabetyp sind Untertypen erlaubt. Da wir hier nur void haben und die Methoden nichts zurückgeben, haben wir für diese sogenannten kovarianten Rückgabetypen auch kein Beispiel.

  • Modifizierer können ergänzt werden, zum Beispiel final oder synchronized. Unterklassen können prinzipiell die Sichtbarkeit erhöhen, doch da alle abstrakten Methoden in Schnittstellen implizit public sind, ist das schon maximal sichtbar.

Auch bei der throws-Klausel sind gewisse Anpassungen möglich. In zwei Richtungen kann erweitert werden:

  • Die Methode kann sich dazu entschließen, überhaupt keine Ausnahmen auszulösen, daher kann die throws-Klausel auch fernbleiben.

  • Die Methode kann Untertypen von Ausnahmen auslösen, die die Oberklasse ausgelöst hat. Umgangssprachlich gesagt: Wenn die Methode der Oberklasse eine allgemeine Ausnahme auslöst und wir eine Behandlung für diesen allgemeinen Ausnahmetyp haben, dann kann diese Behandlung auch mit einem spezielleren Typ umgehen.

1.5.9. Quiz: throw und throws

Beide Schlüsselwörter tauchen im Zusammenhang mit Ausnahmen auf. Das Schlüsselwort throws wird dabei für geprüfte Ausnahmen an der Methodensignatur eingesetzt (prinzipiell können auch ungeprüfte Ausnahmen aufgeführt werden, doch das ist unnötig). throws drückt aus, dass die Methode geprüfte Ausnahmen auslösen kann. Hier können kommasepariert mehrere Ausnahmen stehen. Der Aufrufer der Methode muss dann alle diese Ausnahmen behandeln.

Das Schlüsselwort throw wird hingegen im Inneren von Methoden eingesetzt, um Ausnahmen auszulösen, die dann den Programmfluss in der Methode beenden. In der Methode kann es prinzipiell mehrere Stellen mit throw geben.

1.5.10. Quiz: The Division fails

Man kann das Programm nicht übersetzen, es gibt Compilerfehler an drei Stellen.

class Application {
 public static void main( String[] args ) {
   try {
     throw 1 / 0;          (1)
}
   catch ( int e ) {       (2)
e.printStackTrace();  (3)
}
 }
}
1In Java können nur Ausnahmen vom Typ Throwable ausgelöst werden. 1/0 ist jedoch vom Typ int. Die Division 1/0 erzeugt zwar eine ArithmeticException, allerdings ist der Ausdruck 1/0 kein Throwable, sondern einfach nur vom Typ int.
2Der nächste Fehler ist beim catch- Zweig. Hier muss etwas stehen was vom Typ Throwable ist — man kann keinen primitiven Datentyp einsetzen.
3Der dritte Fehler ist bei printStackTrace(), denn Methodenaufrufe auf primitiven Datentypen sind nicht gestattet.

In Java müssen alle Ausnahmen vom Typ Throwable abgeleitet sein. Das ist den anderen Programmiersprachen anders. In ähnlicher Form kann man das Programm in JavaScript durchaus ausführen, denn in JavaScript kann man beliebige Dinge als Ausnahmen melden.

1.5.11. »Watt ist unmöglich« mit eigener Ausnahme anzeigen

Listing 6. com/tutego/exercise/device/nswigu/IllegalWattException.java
public class IllegalWattException extends RuntimeException {

  public IllegalWattException() {
  }

  public IllegalWattException( String format, Object... args ) {
    super( String.format( format, args ) );
  }
}

IllegalWattException erweitert eine Oberklasse RuntimeException, die uns mehrere Konstruktoren zur Verfügung stellt. Wir bieten selbst zwei Konstruktoren an, die an die Oberklassen Konstruktoren delegieren. Der parameterlose Konstruktor delegiert automatisch nach oben, der zweite parametrisierte Konstruktor baut für die Fehlermeldung eine Zeichenfolge auf und geht damit an die Oberklasse, die diese Fehlermeldung vermerkt. Die Nachricht ist später über die Methode getMessage() verfügbar. Die eigentliche Nachricht wird in der Oberklasse Throwable vermerkt.

IllegalWattException UML
Abbildung 4. UML-Diagramm der Vererbungsbeziehung

Der parametrisierte Konstruktor von IllegalWattException weist eine Besonderheit auf, die nicht üblich für Ausnahmeklassen ist: Der Konstruktor IllegalWattException(String format, Object... args) nimmt einen Format-String und auch die Formatierungsargumente an und baut mit String.format(…​) einen String mit der gewünschten Formatierung auf und gibt die Fehlermeldung zwecks Speicherung an die Oberklasse weiter.

Das ElectronicDevice ist eine Klasse, die im Setter prüft, ob die Leistung nicht negativ oder 0 ist. Wenn doch, wird der Konstruktor von IllegalWattException mit einer vollständigen Fehlermeldung aufgebaut und die Ausnahme ausgelöst.

Listing 7. com/tutego/exercise/device/nswigu/ElectronicDevice.java
public void setWatt( int watt ) {
  if ( watt <= 0 )
    throw new IllegalWattException( "Watt cannot be 0 or negative, but was %f",
                                    watt );
  this.watt = watt;
}

Der Testcode ruft die Setter-Methode mit falschen Werten auf, sodass es eine Ausnahme gibt, die auf die Konsole geschrieben wird.

Listing 8. com/tutego/exercise/device/nswigu/Application.java
ElectronicDevice gameGirl = new ElectronicDevice();
try {
  gameGirl.setWatt( 0 );
}
catch ( IllegalWattException e ) {
  e.printStackTrace();
}

Die Konsolenausgabe ist:

com.tutego.exercise.device.nswigu.IllegalWattException: Watt cannot be 0 or negative, but was 0,000000
	at com.tutego.exercise.device.nswigu.ElectronicDevice.setWatt(ElectronicDevice.java:10)
	at com.tutego.exercise.device.nswigu.Application.main(Application.java:8)

1.5.12. Quiz: Kartoffeln oder anderes Gemüse

Das Programm lässt sich nicht übersetzen. Es gibt folgende Fehlermeldung vom Compiler:

Exception 'PotatoException' has already been caught

Für Ausnahmeklassen kann man eine Vererbungshierarchie aufbauen, wie für andere Klassen auch. In unserem Beispiel ist die erste eigene Ausnahmeklasse VegetableException. Davon gibt es eine Unterklasse PotatoException. Wenn wir in der main(…​)-Methode eine PotatoException auslösen, dann wird diese geprüfte Ausnahme in einem catch-Block aufgefangen. Der erste catch-Block fängt allerdings nicht den präzisen Typ PotatoException ab, sondern den Basistyp VegetableException. Das heißt, der erste catch-Block mit der allgemeinen VegetableException fängt PotatoException schon ab, und der nachfolgende catch-Block mit der spezielleren PotatoException ist überhaupt nicht mehr erreichbar.

1.5.13. Aktuelles Datum in Datei schreiben

Listing 9. com/tutego/exercise/io/WriteDateToFile.java
String fileName = "current-date.txt";
try ( PrintWriter writer = new PrintWriter( fileName ) ) {
  writer.write( LocalDateTime.now().toString() );
}
catch ( FileNotFoundException e ) {
  System.err.println( "Can't create file " + fileName );
}

Der Konstruktor von PrintWriter nimmt einen String für den Dateinamen an. Prinzipiell kann man beim PrintWriter auch eine Zeichenkodierung angeben, doch das war in der Aufgabenstellung nicht gefordert — es wird automatisch die Kodierung der Plattform verwendet. Wenn wir allerdings eine Kodierung als z. B. String angeben, müssen wir eine weitere Ausnahme behandeln. So ist lediglich eine FileNotFoundException zu behandeln, die der Konstruktor auslöst. Das erzeugte Objekt wird in einer Variablen writer zwischengespeichert, und das ist genau die Ressource, die am Ende automatisch geschlossen wird. Im Rumpf des try-mit-Ressourcen-Block wird ein LocalDateTime-Objekt mit der statischen now()-Methode aufgebaut, dann eine toString()-Repräsentation erfragt, und als String über die Writer-Methode write(…​) in den Datenstrom, also in die Datei geschrieben.

1.5.14. Noten einlesen und in eine neue ABC-Datei schreiben

Listing 10. com/tutego/exercise/io/ReadTextAndWriteABC.java
private static final String VALID_MUSICAL_NOTES =
    "C, D, E, F, G, A, B, C D E F G A B c d e f g a b c' d' e' f' g' a' b'";

public static void readTextAndWriteAsABC( String source, String target ) {
  try ( Scanner in      = new Scanner( Paths.get( source ) );
        PrintWriter out = new PrintWriter( target ) ) {

    out.println( "M:C" );
    out.println( "L:1/4" );
    out.println( "K:C" );

    String[] sortedMusicalNotes = VALID_MUSICAL_NOTES.split( " " );
    Arrays.sort( sortedMusicalNotes );

    while ( in.hasNextLine() ) {
      String line = in.nextLine();
      if ( Arrays.binarySearch( sortedMusicalNotes, line ) >= 0 ) {
        out.print( line );
        out.print( ' ' );
      }
    }
    out.println();
  }
  catch ( IOException e ) {
    System.err.println( "Cannot convert text file due to an input/output error" );
    e.printStackTrace();
  }
}

Die eigentliche Verarbeitung ist die folgende: Aus den beiden Parametern source und target konstruieren wir einen Scanner zum Lesen und einem PrintWriter zum Schreiben. Beides sind Ressourcen, die im try-mit-Ressourcen geschlossen werden müssen. Ein Semikolon trennt die beiden Ressourcen. Am Ende des try-Blocks werden alle Ressourcen unabhängig voneinander wieder geschlossen, egal, ob es eine Ausnahme gab oder nicht. Das geschieht in umgekehrter Reihenfolge. Zunächst wird der PrintWriter geschlossen, dann der Scanner. Die Konstruktoren der beiden Klassen sind etwas anders. Wir müssen aufpassen, dass der Konstruktor von Scanner nicht mit einem String aufgerufen wird, denn sonst zerlegen wir den String mit dem Dateinamen, und das wäre falsch. Damit der Scanner eine Datei von Zeichenfolgen trennen kann, konvertieren wir den Dateinamen in ein Path-Objekt und setzen dieses in den Konstruktor von Scanner.

Der Rumpf des try-Blocks schreibt die drei Zeilen mit dem Prolog der Datei. Anschließend laufen wir mit hasNextLine() so lange durch die Eingabedatei, bis es keine Zeilen mehr zu verarbeiten gibt. Wir lesen anschließend die Zeile ein und müssen prüfen, ob die Note gültig ist. Aus der Aufgabenstellung kennen wir alle Noten. Natürlich könnten wir mit einer großen switch-case-Anweisung fragen, ob die Zeile eine gültige Note enthält, allerdings wäre der Programmieraufwand hoch. Stattdessen wollen wir die Noten in ein String-Array setzen. In der Konstanten VALID_MUSICAL_NOTES haben wir alle gültigen Noten, ein split(" ") liefert ein Array mit gültigen Noten. Leider lässt sich nicht mit einem kompakten Ausdruck prüfen, ob ein Element im Array ist, deswegen gehen wir einen kleinen Umweg. Zunächst einmal sortieren wir das Array mit den Noten, und dann lässt sich Arrays.binarySearch(…​) einsetzen. Das ist im Schnitt schneller als lineare Suchverfahren, etwa über Arrays.asList(…​).contains(…​). Prinzipiell könnten wir die Noten im String vorsortieren, dann könnte das Sortieren pro Methodenaufruf entfallen. Der vorbereitete String sähe dann so aus:

"A A, B B, C C, D D, E E, F F, G G, a a' b b' c c' d d' e e' f f' g g'"

Arrays.binarySearch(…​) liefert einen Index größer gleich Null, wenn die Note vorhanden ist. Auf leere Zeilen müssen wir nicht prüfen, sie sind nicht Teil des Arrays. Eine gültige Note schreiben wir in die Datei, gefolgt von einem Leerzeichen.

Fehler können an verschiedenen Stellen auftreten — weil die Datei nicht vorhanden war, weil keine Datei zum Schreiben geöffnet werden konnte, weil während des Lesens Fehler auftreten oder während des Schreibens Fehler auftreten. Alle diese Fehler fängt ein gemeinsamer catch-Block ab.

1.5.15. Quiz: Ausgeschlossen

Wenn wir das Programm ausführen, gibt es folgende Ausgabe für Version 1:

close() ResourceA
close() ResourceB
close() ResourceA

Und folgende Ausgabe für Version 2:

close() ResourceA
close() ResourceB

Beide Ressourcen sind AutoCloseable, sodass die Typen im try-mit-Ressourcen verwendet werden können. Die close()-Methode der ersten Ressource, ResourceA, gibt eine Meldung auf dem Bildschirm aus. Die zweite Ressource, ResourceB, ummantelt die erste. Bei einem Aufruf von close() auf ResourceB, wird zunächst close() von der ummantelten ResourceA aufgerufen und dann eine Bildschirmmeldung ausgeben. Dieses Vorgehen ist für Ein-/Ausgabe-Ströme in Java ein übliches Verhalten.

Bei den Versionen 1 und 2 unterscheidet sich die Ausgabe aus folgendem Grund: In der ersten Version handelt es sich beim try-mit-Ressourcen um zwei Ressourcen, die in umgekehrter Reihenfolge wieder geschlossen werden. Nachdem erst ResourceA, dann ResourceB aufgemacht wurde, wird ResourceB als erste wieder geschlossen und danach ResourceA. Ein Aufruf von close() auf ResourceB führt als Erstes zum Schließen von ResourceA, und davon haben wir die erste Bildschirmausgabe. Dann geht es zurück in die close()-Methode von ResourceB, und wir haben die zweite Bildschirmausgabe. Nachdem ResourceB geschlossen wurde, wird auch von ResourceA die close()-Methode aufgerufen. Bei dieser Konstruktion lässt sich ablesen, dass von ResourceA die close()-Methode zweimal aufgerufen wird, was unter Umständen zu einem Problem führt. Es gibt spezielle Ressourcen, die man nicht zweimal schließen darf. Es kann also passieren, dass bei einem zweiten close()-Aufruf eine Ausnahme ausgelöst wird, weil zweimaliges Schließen nicht zulässig ist. Die meisten Ressourcen in Java können allerdings gut damit umgehen, wenn sie mehrfach geschlossen werden, und ignorieren das. Es ist aber wichtig, die API-Dokumentation zu studieren, ob eine geschlossene Ressource ohne Ausnahme noch einmal geschlossen werden darf oder nicht.

Die Variante 2 verhindert das doppelte Schließen von ResourceA. Das heißt, diese Variante hat einen Vorteil. Allerdings gibt es auch einen Nachteil: Die ResourceA wird nämlich dann nicht geschlossen, wenn der Konstruktor ResourceB eine Ausnahme auslöst. Das durch new angelegte Objekt vom Typ ResourceA wird intern nicht durch eine Variable referenziert und nimmt nicht am Schließvorgang von try-mit-Ressourcen teil.

Beide Varianten haben also ihre Schwachpunkte. Häufig wird man in Java Variante 1 wählen, weil das Ausführen der unterschiedlichen Ressourcen übersichtlicher ist und weil die meisten Ressourcen in Java das zweimalige Schließen verkraften. Variante 2 ist aber ebenfalls in Ordnung, denn Konstruktoraufrufe führen beim Ummanteln in aller Regel nicht zu Ausnahmen — es sei denn, die Ressource ist null —, sondern erst nachfolgende Operationen.