Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 7 Ausnahmen
  gp 7.1 Konzeption
  gp 7.2 Ausnahme-Mechanismus
  gp 7.3 Details zur Ausnahme-Behandlung
  gp 7.4 Ausnahme-Kategorien in Java
  gp 7.5 Overriding von Ausnahmen
  gp 7.6 Deklaration neuer Ausnahmen
  gp 7.7 Einsatz von Ausnahmen
  gp 7.8 Ausnahmen-Behandlung
  gp 7.9 Zusammenfassung
  gp 7.10 Testfragen

Kapitel 7 Ausnahmen

Methoden müssen auf Laufzeitfehler, die bei der Programmausführung auftreten, geeignet reagieren, entweder selbst oder indem sie die Ursache und die Kontrolle an die aufrufende Methode weiterreichen. Diesen Kontrollmechanismus nennt man in Java Ausnahmebehandlung.
Die Behandlung von Laufzeitfehlern ist in der Java-Plattform auf Basis einer differenzierten Hierarchie von Ausnahme- bzw. Exception-Klassen gelöst.
Der effiziente Einsatz von Ausnahmen zum Design robuster Klassen ist ergo das zentrale Thema.

Compiler-Fehler
vs. Laufzeitfehler

Fehler können anhand des Zeitpunkts ihres Auftretens unterschieden werden: Fehler treten schon bei der Übersetzung oder erst zur Laufzeit auf.

Sicherlich sind Fehler, die der Compiler entdeckt, die angenehmeren. Man kann sie analysieren und den Code entsprechend anpassen.

Im ungünstigeren Fall treten Laufzeitfehler erst beim Anwender – dem Kunden – auf und beenden das Programm ohne geeignete Behandlung, lassen es »abstürzen«.

Die Behandlung der Laufzeitfehler macht bei interaktiven Programmen häufig den Hauptteil des Codes aus. Die produktiven Programmteile sind mit unzähligen Kontrollanweisungen durchsetzt, die auf widrige Umweltsituationen reagieren müssen.

Laufzeitfehler werden häufig ignoriert!

Bei traditionellen Sprachen wie C besteht das Übel darin, dass Fehler, die an eine aufrufende Methode als Ergebnis oder Parameter weitergereicht werden, durchaus ignoriert werden können.

Dies führt dann zu dem noch unangenehmeren Phänomen, dass die Absturzursache unklar bleibt.

Ausnahmen können nicht ignoriert werden

In Java begegnet man diesem Laissez-faire-Verhalten mit der Erzeugung und dem Auslösen von Ausnahme-Objekten , wenn nötig, durch die JVM selbst.

Diese Ausnahme-Objekte, einmal auf den Weg gebracht, müssen im Programm abgefangen werden oder erzeugen nette Konsol-Meldungen.


Galileo Computing

7.1 Konzeption  downtop

Bei der Konzeption neuer Sprachen geht man davon aus, dass eine konventionelle Behandlung der meisten Laufzeitfehler nicht genügt.

Icon

Die wesentlichen Design-Anforderungen sind

Forderungen an die Behandlung von Laufzeitfehlern

gp  minimal-invasiv: Eine möglichst geringe (negative) Auswirkung auf den normalen Programmablauf (normal flow of control).
gp  Klarheit: Konzentration der Fehlerbehandlung auf klar erkennbare Programmabschnitte.
gp  Erweiterbarkeit: Je nach Programmumgebung treten andere, neue Laufzeitfehler auf, die in das System integiert werden müssen.

Unterscheidung von Laufzeitfehlern

Auch Laufzeitfehler können je nach Art oder Ursache in Kategorien unterteilt werden. Im Sprachverständnis von Java signalisieren Laufzeitfehler zwei unterschiedliche Arten von Ausnahme-Situationen:

Fehlerhafter Code

gp  fehlerhaften Programm-Code, der verbessert werden muss

Unpassende Stelle

gp  Fehler, die dort, wo sie auftreten, nicht sinnvoll behandelt werden können

Zum ersten Fall: Der Programm-Code muss so verbessert werden, dass diese Fehler nicht mehr auftreten können.

Zum zweiten Fall: Der normale Programmablauf muss zwangsläufig unterbrochen werden und der Fehler an einer »angemesseneren« Stelle behandelt werden. Eine Rückkehr an den Punkt des Auftretens wird ausgeschlossen.

Java-Konzeption:
Es gibt kein Zurück
(not recoverable)

Das Konzept von Java ist stringent und durchaus diskussionswürdig. Es steht im Gegensatz zu Konzepten, die auch Laufzeitfehler in den normalen Kontrollfluss einbinden können.

Visual Basic – ich bitte um Nachsicht – unterscheidet u.a. leichte Fehler, die erst einmal ignoriert werden können (On Error Resume Next). Schwere Fehler müssen zwar sofort behandelt werden, aber erlauben es durchaus, anschließend wieder an der Stelle fortzufahren, an der sie aufgetreten sind.


Galileo Computing

7.2 Ausnahme-Mechanismus  downtop

Basisklasse
Throwable

In Java sollen Ausnahmen immer Instanzen von Subklassen der Klasse java.lang.Throwable sein. Bei einem Laufzeitfehler muss man das Auslösen einer Ausnahme vom Behandeln der Ausnahme unterscheiden.


Galileo Computing

7.2.1 Ausnahme-Auslösung (exception throwing)  downtop

Wer löst Ausnahmen aus?

Eine Ausnahme kann wie folgt ausgelöst werden:

gp  durch die JVM
gp  explizit durch eine Anweisung

Ausnahme-Behandler (exception handler)

Die Ausnahme ist immer eine Instanz einer Ausnahme-Klasse, die den entsprechenden Laufzeitfehler identifiziert und wird von der JVM an einen passenden Ausnahme-Behandler weitergereicht (siehe 7.2.2).

throw:
explizites Auslösen einer Ausnahme

Explizit wird eine Ausnahme durch

          throw throwableExpression;

ausgelöst, wobei throwableExpression eine Instanz vom (Sub-)Typ Throwable ergeben muss.

Beispiele

In der folgenden Methode wird eine Ausnahme durch die JVM ausgelöst, wenn der Divisor Null ist:

Implizites Auslösen einer Ausnahme

static int intDiv (int i, int j) {
  return i/j;  // bei einer Ausnahme erfolgt kein return
}

Hat j den Wert 0, erzeugt die JVM eine Instanz der Klasse ArithmeticException und aktiviert die Ausnahme.

In der nächsten Methode wird bei einem null-Argument eine Instanz von NullPointerException erschaffen und mittels throw ausgelöst:

Explizites Auslösen einer Ausnahme

class Point { int x,y; /*...*/ 
}
static double distanceFromOrigin (Point p) {
  if (p==null)
    throw new java.lang.NullPointerException("p ist null");
  return Math.sqrt(p.x*p.x+p.y*p.y);
}

Bei diesen beiden Methoden wird die return-Anweisung beim Auftreten einer Ausnahme nicht mehr ausgeführt.


Galileo Computing

7.2.2 Ausnahme-Behandlung (exception handlingdowntop

Icon

Die Behandlung einer Ausnahme erfolgt nach generellen Regeln:

Unbehandelte Ausnahme

1. Wird eine Ausnahme ausgelöst, gilt sie als unbehandelt und führt zum sofortigen Abbruch des normalen Programmablaufs.

Such-Muster
der JVM

2. Die JVM sucht als Nächstes im umgebenden Code bzw. in den aufrufenden Methoden so lange nach einer try-Anweisung, bis sie einen passenden catch-Block findet oder aber die oberste Methode der Threads, main() oder run(), verlassen muss.

Unbehandelte Ausnahme: Abbruch!

3. Das Verlassen des obersten Methoden-Blocks führt bei einer unbehandelten Ausnahme zum Abbruch (der Thread).

try behandelt
Ausnahmen

4. Nur eine try-Anweisung kann Ausnahmen behandeln:
      try { tryStatementSequenz }
    [ catch (ExceptionType1 e1) { exceptionHandlerSequenz1 }
      ...
      catch (ExceptionTypeN eN) { exceptionHandlerSequenzN } ]
    [ finally { finallyStatementSequenz } ]
Dem try-Block muss zumindest ein optionaler catch- oder finally-Block folgen.

catch-Behandlung von Ausnahmen

5. Sofern catch-Blöcke vorhanden sind,
    gp  werden nacheinander die einzigen Parameter ExceptionTypeX darauf untersucht, ob die unbehandelte Ausnahme-Instanz ein (Sub-) Typ ist.
    gp  wird bei einem passenden ExceptionTypeX der zugehörige catch-Block ausgeführt, alle nachfolgenden catch-Blöcke übersprungen und die Ausnahme gilt als behandelt.
    gp  kann hierdurch erneut eine Ausnahme ausgelöst werden.

finally ist
unabhängig von Ausnahmen

6. Sofern ein finally-Block vorhanden ist,
    gp  wird er immer am Ende der try-Anweisung ausgeführt, egal, ob eine Ausnahme aufgetreten, behandelt oder nicht behandelt ist.

Lost Exception!

    gp  kann eine unbehandelte Ausnahme durch eine return-Anweisung »verloren” gehen oder aber durch die Auslösung einer neuen Ausnahme ersetzt werden.

Ausnahme
unbehandelt? Propagieren!

7. Wurde die Ausnahme behandelt und keine neue ausgelöst, wird die Ausführung nach der try-Anweisung fortgesetzt. Ansonsten wiederholt sich die Suche, beginnend mit dem zweiten Schritt.

Ausnahmen können also nur mit Hilfe von catch-Ausnahme-Parametern erkannt und behandelt werden.

Der finally-Block kann nur indirekt Ausnahmen durch Überschreiben oder Missachtung entfernen, was durchaus zu Problemen führen kann (siehe hierzu 7.3.3).

Aktivitäts-Diagramm (UML)

Aktivitäts-Diagramme:
Darstellung von Prozessen in UML

Aktivitäts-Diagramme wurden in Kapitel 4, Modellierung und UML, zwar nicht formal vorgestellt, sind aber zur Darstellung von Prozessabläufen und Bearbeitungsschritten recht reizvoll.

Start und Ende des Ablaufs sind unschwer an den massiven Kreisen zu erkennen. Die Bearbeitung (Aktivität) wird in Rechtecken dargestellt und folgt den Pfeilen und Entscheidungsrauten, wobei die jeweilige Bedingung (guard) in eckigen Klammern erfüllt sein muss.

Objekte, die beteiligt sind, und Kommentare runden das Diagramm ab.

Aktivitäts-Diagramm
zur Behandlung von Ausnahmen


Abbildung
Abbildung 7.1   Behandlung von Ausnahmen vom Typ Exception

In Abb. 7.1 wird der Versuch unternommen, das generelle Muster der Ausnahmebehandlung – analog zu den genannten Bearbeitungsschritten – anhand eines Aktivitäts-Diagramms darzustellen.

Beispiele

Auslösen einer NullPointerException

Zuerst wird in der Methode distanceFromOrigin() (siehe 7.2.1) eine NullPointerException ausgelöst:

double d= 0.0;
try { 
  d= distanceFromOrigin(null); 
  System.out.println(d);        // wird nicht ausgeführt     ¨
}  
catch (Exception e) { d= Double.NaN; }
finally             { System.out.println(d); }  // :: NaN

Exception-Handling
2. Schritt

Erklärung: Die Ausnahme wird in distanceFromOrigin() ausgelöst und nicht behandelt. Also sucht die JVM im umgebenden Block.

4. Schritt

Die Suche endet in der try-Anweisung, die die Methode enthält.

1. Schritt

Diese wird sofort abgebrochen, ohne println() in Zeile ¨ auszuführen.

5. Schritt

Da NullPointerException eine Subklasse von Exception ist, ist der einzige catch-Block der passende Ausnahme-Behandler und die Ausnahme gilt als behandelt.

6. Schritt

Abschließend wird noch der finally-Block ausgeführt.

Variationen:
catch-Blöcke, finally-Block

Die folgende Klasse Test ist aufgrund ihrer verschiedenen Versionen interessant:

public class Test {
  public static void main(String[] args) {
    int[] iarr= {0,1,2};
    int i=0;
  /* Version 1 */
    try { while(true) System.out.println(1/iarr[i++]); }
    catch (IndexOutOfBoundsException e) {
      System.out.println("Ind");
    }
 /* Version 2
    catch (ArithmeticException e) { 
      System.out.println("Ari");
    } */
 /* Version 3
    finally { System.out.println("fin");
    } */
  }
}

Exception-Handling
2. Schritt

Version 1 (nur erster catch-Block): In der while-Schleife tritt sofort eine ArithmeticException auf. Da ArithmeticException keine Subklasse von IndexOutOfBoundsException ist, passt der erste catch-Block nicht. Die JVM muss die Applikation Test abbrechen.

5. Schritt

Version 2 (inkl. zweitem catch-Block): Nun gibt es einen passenden catch-Block. Die Ausnahme gilt als behandelt und das Programm beendet normal (:: Ari).

6. Schritt

Version 3 (nur erster catch-Block und finally): Es gibt zwar keinen passenden catch-Block, allerdings wird vor dem Programmabbruch erst finally ausgeführt (:: fin).


Galileo Computing

7.3 Details zur Ausnahme-Behandlung  downtop

Es gibt noch einige interessante Details, die in den Bearbeitungsschritten (siehe 7.2.2) noch nicht angesprochen wurden.


Galileo Computing

7.3.1 catch-Reihenfolge  downtop

Icon

Folgen einem try-Block mehrere catch-Blöcke, muss aufgrund der sequenziellen Abarbeitung der catch-Blöcke folgende Reihenfolge eingehalten werden:

Reihenfolge der
catch-Exceptions

gp  catch-Blöcke mit spezielleren Ausnahmen müssen vor catch-Blöcken mit generellen Ausnahmen angegeben werden.

Der Compiler prüft die Einhaltung der Regel und erzeugt ggf. eine »unreachable«-Fehlermeldung:

Nach dem fünften Schritt in 7.2.2 verdeckt ein generellerer Ausnahmetyp alle speziellen, d.h. alle nachfolgenden Subtypen, die somit nicht ausgeführt werden könnten.

Das kleine Beispiel »Test« provoziert eine Fehlermeldung:

public class Test {
  public static void main(String[] args) {

Compiler erzeugt »unreachable« -Meldung

    try { System.out.println(1/0); }
    catch (Exception e) { }                      // C-Fehler
    catch (ArithmeticException e) { }    
  }
}

Ein Vertauschen der beiden catch-Blöcke löst das Problem.


Galileo Computing

7.3.2 Geschachtelte Ausnahmen  downtop

try-Anweisungen schachteln

Ausnahme-Behandlungen können ineinander geschachtelt werden, was sehr angenehm sein kann.

        try-block          
            try-block      
               try-block  
               ...
            catch-blocks  
            finally-block
        catch-blocks     
        finally-block   

Ausführung von geschachtelten try-Anweisungen

Gemäß dem fünften Schritt in 7.2.2 werden bei Auslösen einer inneren Ausnahme zuerst alle zugehörigen inneren catch-Blöcke durchsucht. Sollte es keinen passenden catch-Block geben, erfolgt nach dem zweiten Schritt sukzessive die Suche nach einer try-Anweisung in den umgebenden Blöcken.

Beispiel

static int div (Integer i1, Integer i2) {
  try {
    try {
      return i1.intValue()/i2.intValue();
    }
    catch (NullPointerException npe) {
      return 0;
    }
  }
  catch (ArithmeticException ae) {
    return Integer.MAX_VALUE;
  }
  /* finally { System.out.print("fin "); } */
}

Version ohne finally-Block:

Integer i= new Integer(7);
System.out.println(div(i,null));           // :: 0
System.out.println(div(i,new Integer(0))); // :: 2147483647
System.out.println(div(i,new Integer(2))); // :: 3

Version mit finally-Block : Vor jeder Zahl erfolgt die Ausgabe fin.


Galileo Computing

7.3.3 finally und unbehandelte Ausnahmen  downtop

Funktion von finally in der
try-Anweisung Icon

In einem finally-Block hat man keinen direkten Zugriff auf die ausgelöste Ausnahme. Allerdings kann man – meist aus Versehen – die Ausnahme durchaus entfernen oder mit einer anderen überschreiben. Wichtig ist die folgende Regel:

Unbehandelte Ausnahme

gp  Es kann nur jeweils eine unbehandelte Ausnahme von einer Methode bzw. einem Block nach außen übergeben werden.

Aufgrund dieser Regel wird eine Ausnahme, die im try-Block nicht behandelt wird, durch eine Ausnahme überschrieben, die im finally-Block ausgelöst und nicht behandelt wird.

Beispiele

finally-Problem:
verlorene Ausnahme!

Die Methode finallyTest1() erzeugt eine verlorene Ausnahme:

static void finallyTest1() throws Exception {
  try     { throw new Exception("try"); }
  finally { throw new Exception("fin"); }
}

Der Aufruf erzeugt nach der o.a. Regel:

try {       
  finallyTest1();
}
catch (Exception e) {
  System.out.println(e.toString()); // :: java.lang.Exception: 
fin
}

Die Variation finallyTest2() beseitigt das Problem der verlorenen Ausnahme:

finally-Variation mit innerem
try-catch

static void finallyTest2() throws Exception {
  try { throw new Exception("try"); }
  finally {
    try { throw new Exception("fin"); }
    catch (Exception e) { 
/* wird hier behandelt */ }
  }
}

return-Anweisung im finally-Block

In der folgenden Methode neverThrowsExceptions() hat die return-Anweisung in finally einen recht subtilen Nebeneffekt, der im zweiten Punkt des sechsten Schritts in 7.2.2 angesprochen wurde.

Icon
Wirkung von return im finally-Block

Es ist das Problem der verlorenen unbehandelten Ausnahme:

static void neverThrowsExceptions() throws Exception 
{
  try { throw new Exception(); }  // geht verloren!
  finally {
    return;   // Methode wird normal verlassen!
  }           // die Exception wurde implizit behandelt!
}
gp  Eine Methode, deren finally-Block eine return-Anweisung ausführt, liefert keine Ausnahmen nach außen.

Galileo Computing

7.3.4 Rethrowing in catch  downtop

Auch in catch-Blöcken können wieder neue Ausnahmen ausgelöst werden.

Rethrowing: Erneutes Auslösen derselben Ausnahme

gp  Unter Rethrowing versteht man die erneute Aktivierung derselben Ausnahme mittels throw, die dem catch-Block übergeben wurde.

Dies ist in Fällen sinnvoll, in denen catch – eventuell abhängig von bestimmten Bedingungen – die Ausnahme nur teilweise oder gar nicht behandeln kann und die Ausnahme deshalb weiterreichen muss.

Beispiel

In der Methode mean() wird eine Division durch 0 abgefangen (d.h., das Array hat keine Elemente). Wird mean() dagegen mit null aufgerufen, wird die Ausnahme nach außen weitergereicht.

Rethrowing einer RuntimeException

  public static int mean(int[] iarr) {
    try {
      int m= 0;
      for (int i=0; i<iarr.length; m+=iarr[i++]);
      return m/iarr.length;
    }
    catch (RuntimeException e) { 
      if (e instanceof NullPointerException) throw e;
      else return 0;
    }
  }

Hier drei verschiedene Aufrufe:

  System.out.println(mean(new int[] {1,2,3}));  
// :: 2
System.out.println(mean(new int[] {})); // :: 0
System.out.println(mean(null)); // NullPointerException

Galileo Computing

7.4 Ausnahme-Kategorien in Java  downtop

Das Basis-Package java.lang stellt bereits eine umfangreiche Klassen-Hierarchie von Ausnahmen bereit, die Auswirkungen auf den Compiler und die Art der Verwendung hat.


Galileo Computing

7.4.1 Hierarchie-Konzept  downtop

Icon

Throwable stellt die Basisklasse aller Ausnahmen dar (Abb. 7.2). Nach Java-Konvention sollen

Erzeugung von Ausnahmen

gp  von der Klasse Throwable – obwohl nicht abstrakt – keine eigenen Instanzen erzeugt werden und
gp  möglichst nur Ausnahmen von Leaf-Klassen erzeugt werden.

Begründet wird der letzte Punkt damit, dass die Hierarchie eine disjunkte Klassifikation der Laufzeitfehler darstellt, die den Fehler immer weiter präzisiert. Eine Leaf-Klasse bzw. -Instanz ist also informativer.

Ausnahme-Hierarchie: eine disjunkte Ausnahme-Kassifikation


Abbildung
Abbildung 7.2   Ausnahme-Kategorien

Icon

Nach Throwable folgen zwei große Kategorien von Fehlern:

Ausnahme-
Hierarchie

gp  Exception ist die Basisklasse für alle Ausnahmen, die im Code abgefangen und behandelt werden sollen.
gp  Error ist die Basisklasse für alle Fehler, die als nicht behebbar angesehen werden, also auch im Code nicht abgefangen werden sollen.

Mit anderen Worten: Error-Ausnahmen sollen tabu sein.


Galileo Computing

7.4.2 Checked vs. unchecked Exceptions  downtop

checked vs. unchecked
Exceptions

Die Begriffe checked bzw. unchecked Exceptions stehen für Ausnahmen, die im Code geprüft oder deklariert bzw. nicht geprüft werden müssen.

r Ausnahme-Klassen, die zu den checked Exceptions zählen, gilt folgende Regel:

Icon

Der Compiler stellt sicher, dass checked Exceptions

Compiler-Prüfung von checked Exceptions

gp  nur dann in Methoden nicht behandelt werden brauchen, wenn sie im Methoden-Kopf mittels throws deklariert sind:
   [mModifiers] ResultType methodName (parameterList)
                           throws Throwable1[,...ThrowableN]

Warnung zu
unchecked Exceptions

gp  in einer übergeordneten Methode entweder mittels try-catch abgefangen oder erneut im Kopf mittels throws deklariert werden.

Für unchecked Exceptions gibt es keine Regel, nur einen Warnhinweis:

gp  Diese Art von Ausnahmen kann zu jedem Zeitpunkt von der JVM oder explizit durch Code ausgelöst werden und führt dann ohne Behandlung immer zum Abbruch der Ausführung (Thread).

Icon

Separation: checked vs. unchecked

Error/RuntimeException sind unchecked

Zu den unchecked Exceptions gehören alle Error- und RuntimeException-Klassen bzw. -Subklassen, zu den checked Exceptions dann die restlichen (siehe Abb. 7.2).


Der Vorteil des Konzepts von checked Exceptions liegt in ihrer expliziten Natur. Man erkennt sie sofort bei Verwendung und hat nur die Alternative zwischen Behandeln und erneutem Weiterreichen.

Compiler-Prüfung

checked bzw. unchecked
Exception: Compiler vs. JVM

Die Unterscheidung der Ausnahme-Klassen in checked bzw. unchecked ist »magic«, d.h. kann nicht durch ein Marker-Interface erfolgen. Eo ipso wird – man ahnt es bereits – die Einhaltung der Checked-Exception-Regel nur vom Compiler geprüft.

gp  Die JVM kennt keine Unterschiede zwischen checked und unchecked Exceptions.

Galileo Computing

7.4.3 Basisklasse Throwable  downtop

Throwable:
nur logisch abstrakt

Ein Blick in die Implementation von Throwable zeigt – im Gegensatz zu dem suggestiven Interface-Namen – eine voll implementierte Klasse, die allen Subklassen wie Exception oder Error praktisch bereits die Arbeit abnimmt.

Die wohl wichtigste Eigenschaft von Throwable, und damit aller Subklassen; ist die Fähigkeit, bei der Instanzierung einer Ausnahme eine Fehlerbeschreibung anzugeben.

Java bietet dazu – sofern erwünscht – einen ausführlichen Execution Stack Trace, d.h. eine Lokalisierung des Ursprungs und seiner Proliferation durch den ausgeführten Code.

Da Throwable nicht selbst instanziert werden soll, erübrigt sich die Darstellung von Konstruktoren bzw. Methoden.

Error – »tödlicher« Laufzeitfehler


Galileo Computing

7.4.4 Ausnahmen vom Typ Error  downtop

Wie bereits in 7.4.1 erwähnt, signalisieren Fehler vom Typ Error: »Hände weg vom Programm«. Interessant ist nur, welche Fehler die Java-Entwickler darunter verstehen.

Die größte Gruppe, Ausnahmen vom Typ LinkageError, können zwar nicht im Programm-Code behandelt werden, müssen aber darauf untersucht werden, warum eine Klasse zur Laufzeit nicht eingebunden werden kann. Die häufigste Ausnahme NoClassDefFoundError kennt dagegen jeder Java-Programmierer.

Ausnahmen vom Typ VirtualMachineError drücken Fehler innerhalb der JVM aus, wobei OutOfMemoryError und StackOverflowError »von außen« beeinflusst werden können.

Die Klasse ThreadDeath gehört zwar logisch nicht zum Typ Error, wurde hier aber platziert, um nicht mit einem catch(Exception e) versehentlich abgefangen zu werden.

Diese Art der Ausnahme wird durch den mutwilligen Thread-Tod mittels stop() ausgelöst und erzeugt – im Gegensatz zu anderen – keine Meldung. Man kann ihn also schlichtweg ignorieren.


Galileo Computing

7.4.5 Ausnahmen vom Typ Exception  downtop

Java-Konvention zum Typ Exception

Alle für den Code interessanten Ausnahmen sind vom Typ Exception, da sie entweder im Programm behandelt werden müssen oder fehlerhaften Code signalisieren, der verbessert werden muss.

Icon

Nach Java-Konvention

Deklaration und Benennung

gp  enden alle Namen von Ausnahmen dieses Typs mit Exception.
gp  werden eigene Ausnahme-Klassen als Subklassen von Exception deklariert, und zwar normalerweise als checked Exception.

Galileo Computing

7.4.6 Ausnahmen vom Typ RuntimeException  downtop

RuntimeException
signalisiert einen Programmierfehler

Ausnahmen vom Typ RuntimeException signalisieren, einfach gesagt, Fehler im Code, die der Compiler leider nicht aufdecken konnte.

gp  Robuster Code muss so geschrieben werden, das Ausnahmen vom Typ RuntimeException nicht mehr auftreten.

So einfach diese Aussage, so schwierig ihre Umsetzung. Denn diese Fehler

gp  müssen nicht explizit abgefangen werden.
gp  werden häufig durch unzulässige Argumente beim Methodenaufruf ausgelöst.

Der zweite Punkt fällt dann unter das Thema Kontraktmanagement.

Icon

Gerade deshalb werden in der Tabelle 6.1 zu den häufigsten RuntimeExceptions exemplarisch einfache Vermeidungsstrategien angegeben.

Vermeidung von RuntimeExceptions

Tabelle 7.1   Vermeidungsstrategien zu RuntimeExceptions
RuntimeException Wird verhindert durch:
ArithmeticException Vor div- bzw. mod-Operationen int-Operanden vorher prüfen
IndexOutOfBoundsException String- oder Array-Index prüfen
ClassCastException Mit instanceof-Operator den Typ prüfen
NullPointerException Referenz auf nicht null prüfen
NumberFormatException Konvertierungen von Strings nach numerische Typen in try-catch einbetten


Galileo Computing

7.4.7 Checked Exceptions  downtop

Aufgrund des Lazy-Programmer-Syndroms fallen die meisten Ausnahmen in den Packages der Java-Plattform und anderer Anbieter unter die Kategorie »checked Exceptions«.

Checked Exceptions: unvermeidliche Umgebungsfehler

Im Gegensatz zu RuntimeException enthält diese Kategorie Fehler, die durch die Programmumgebung und nicht durch eigenen fehlerhaften Code verursacht werden.

Hierunter fallen u.a. Klassen, die nicht (mehr) gefunden werden, Ein- und Ausgabefehler, Kommunikations-, Netzwerk- oder andere Hardwareprobleme.

Insbesondere diese Fehler müssen in einem robusten Programm unbedingt abgefangen werden, und es müssen – je nach Aufgabe – Strategien implementiert werden, wie diesen Fehlern zu begegnen ist.


Galileo Computing

7.5 Overriding von Ausnahmen  downtop

Icon

Beim Design von Methoden in einer Basisklasse bzw. einem Interface muss berücksichtigt werden, ob ein Überschreiben dieser Methode eventuell eine checked Exceptions notwendig macht. Denn es gilt die Regel:

Overriding bzw. Überschreiben von Ausnahmen

gp  Overriding einer Methode, die eine checked Exception deklariert, kann nur durch eine Methode erfolgen, die alternativ
    gp  Ausnahmen vom selben Typ oder Subtyp oder
    gp  keine Ausnahmen

deklariert.

Erklärung: Diese »einengende« Vererbungsrestriktion musste für Klassen und Interfaces eingeführt werden. Der Grund liegt in der Kombination aus Substitutions- und Checked-Exception-Regel:

Denn ohne die Overriding-Regel könnten Instanzen einer Subklasse eine Ausnahme auslösen, die in der Superklasse nicht deklariert ist.

Eingesetzt in Code, der nur Ausnahmen der Superklasse abfangen muss, würden sie dann locker die Regel für checked Exceptions (siehe 7.4.2) aushebeln.

Beispiele

Overriding von Ausnahmen bei Vererbung

interface I { void f() /* throws Exception */ ; 
}
class A     { void f() throws Exception{} }
// C-Fehler, da f() in I keine Exception deklariert
class B extends A implements I {
  public void f() throws Exception {} 
}

Wird im Interface I die Methode f() analog zu A deklariert, gibt es aufgrund der Regel keinen Compiler-Fehler in Klasse B.

In den nächsten zwei Beispielen wird die Ausnahme-Hierarchie des Packages java.io verwendet (siehe Abb. 7.3).


Abbildung
Abbildung 7.3   Ausschnitt aus der Exception-Hierarchie von java.io
interface I { void f() throws FileNotFoundException; 
}
class A { void f() throws EOFException{} }
class B extends A implements I {
  // die nachfolgenden Deklarationen sind nicht 
möglich
  public void f() throws FileNotFoundException{} // C-Fehler
  public void f() throws EOFException{}          // C-Fehler
  public void f() throws FileNotFoundException,
                         EOFException {}         // C-Fehler
  // einer der nachfolgenden Deklarationen ist möglich
  public void f() {}                             // ok!
  public void f() throws Error {}                // auch ok!
}

Siehe hierzu die o.a. Regeln.

Implementierungs-Hierarchie

Auch bei Hierarchien gilt die Overriding-Regel für Ausnahmen, wie das nächste Beispiel zeigt:

interface I1            { void f() throws IOException; 
}
interface I2 extends I1 { 
  void f() throws ObjectStreamException; 
}
class B implements I2 {
  // nur noch (Sub-)Typen von ObjectStreamException möglich
  public void f() throws InvalidClassException {}
}

Die Klasse B muss sich an Interface I2 orientieren.

Icon

Abschließend die Konsequenz, formuliert als Design-Regel:

Notwendige Ausnahmen in Basisklassen

gp  Methoden in Basisklassen bzw. Interfaces müssen bereits die checked Exceptions deklarieren, die in Subklassen benötigt werden.


Galileo Computing

7.6 Deklaration neuer Ausnahmen  downtop

Die Standard-Plattform enthält eine unüberschaubare Zahl von speziellen Ausnahme-Klassen, auf die man – sofern sie passen – zurückgreifen kann.

Natürlich kann man in einem eigenen Package – analog zu java.io – auch eigene Ausnahme-Klassen deklarieren. Ob man eine checked oder unchecked Exception deklariert, soll erst im folgenden Abschnitt behandelt werden. Zuerst interessiert der Mechanismus.

Icon
Muster für die Deklaration neuer Ausnahmen

   class NewException extends ExistingException 
{
     public NewException () {
        // evtl. Aufruf von super(...);
     }
     public NewException (String description) {
       super(description);  // explizite Fehler-Beschreibung
     }
   }

Ist ExistingException eine unchecked Exception, ist dies NewException ebenfalls.

Mit super(description) wird eine Fehlerbeschreibung bis nach Throwable durchgereicht, der einen entsprechenden Konstruktor bereitstellt:

public class Throwable implements java.io.Serializable {
  public Throwable(String message) { 
 fillInStackTrace(); detailMessage = message;
  }
  //...
}

Aufgrund des Marker-Interfaces Serializable können alle Ausnahmen gespeichert werden.10 

Beispiel (Umwandlung einer Exception)10 

Deklaration neuer Ausnahmen

Eine gültige Teile-Nummer besteht aus maximal zehn Stellen und enthält nur Ziffern oder Bindestriche.11  Es soll eine PrimaryKeyException im Konstruktor ausgelöst werden, wenn die Teile-Nummer ungültig ist. Diese soll explizit abgefangen werden.

class PrimaryKeyException extends Exception {   
 // checked!
public PrimaryKeyException () { super("Fehlerhafter Teile-Primärschlüssel!"); } public PrimaryKeyException (String description) { super(description); } }
class Teil {
  String nr;
  Teil(String nr) throws PrimaryKeyException {
    if (nr.length()>10) throw new PrimaryKeyException();
    try { Long.parseLong(nr.replace('-','0')); }
    catch (NumberFormatException e) {  // Umwandlung einer
      throw new PrimaryKeyException(); // Runtime-Exception
    }
    this.nr=nr;
  }
  //...
}

Galileo Computing

7.7 Einsatz von Ausnahmen  downtop

Bisher wurden Ausnahme-Mechanismen anhand bestehender Hierarchien vorgestellt und gezeigt, wie man korrekt reagiert.

Aber die eigentlich schwierig Frage, wie Ausnahmen in eigenen Applikationen sinnvoll und effizient eingesetzt werden, ist noch unbeantwortet.

Soviel vorab: Es gibt keine Ausnahme-Pattern, jedoch durchaus Idiome. Die Ausnahme-Behandlung ist mit anderen Worten an die jeweilige Sprache gebunden, und Antworten auf Fragen zum optimalen Ausnahme-Design bewegen sich gerade in Java auf dünnem Eis.12 

Icon


Galileo Computing

7.7.1 Missbrauch von Ausnahmen  downtop

Zum Einstieg ein Hinweis:

Ausnahmen als
Ersatz für andere Kontrollstrukturen?

gp  Ausnahmen dienen nicht als Ersatz für andere Kontrollstrukturen.

Das folgende Code-Segment ist ein extremes Beispiel:

double[] val= {1.,2.,3.};
double sum=0.;
try {
  int i=0;
  while (true)
    sum+= val[i++]; 
}
catch(Exception e) {}     // ignoriert Index-Überschreitung!
System.out.println(sum);  // :: 6.0

Natürlich sind die Dinge nicht immer so klar wie in diesem Beispiel.


Galileo Computing

7.7.2 Ausnahmen zur Einhaltung von Kontrakten  downtop

Assertion – Kontrakt

Unter Kontrakten – auch Assertions genannt – versteht man formale Eigenschaften, die vom Client einer Methode eingehalten werden müssen oder auch von der Methode selbst (siehe auch 6.12.1).

Eine häufige Kontraktverletzung besteht darin, die Preconditions (Vorbedingungen) beim Aufruf einer Methode nicht einzuhalten. Deshalb:

Ausnahmen: Einsatz bei
Kontrakt- Missachtung

gp  Robuste Methoden erzwingen die Einhaltung ihrer Preconditions mit Hilfe von Ausnahmen.

Diese Regel ist nicht ganz so trivial in der Umsetzung, denn es gibt Alternativen.

Default-Werte
bei Kontraktverletzung

Default-Alternative: Werden Vorbedingungen nicht eingehalten, kann man mit Hilfe von Default-Werten die Operationen durchführen. Dies muss für den Client allerdings klar erkennbar sein, da er ansonsten von den Ergebnissen sehr überrascht sein könnte.

Ausnahme-Auswahl: Zur Wahl stehen checked vs. unchecked Exceptions. Beim Typ RuntimeException ist dem Client freigestellt, ob er mittels try-catch fehlerhafte Argument abfängt oder ob er darauf verzichten will. Im anderen Fall lässt man ihm keine Wahl (siehe 7.7.3).

Kontrakt-Beispiel: Konstruktor

Ausnahmen im Konstruktor

Fehlerhafte Argumente bei Konstruktoren durch Default-Werte zu ersetzen, ist nicht gerade genial. Wozu ist wohl ein No-Arg-Konstruktor da?

Ohne die Hilfe von Ausnahmen kann sich ein Konstruktor nicht wirksam gegen fehlerhafte Aufrufe wehren, da konventionelle Fehlermitteilungen durch Rückgabewerte entfallen.

Icon
Ausnahme verhindert Instanz-Anlage

gp  Bei Auslösen einer Ausnahme in einem Konstruktor wird die Instanz nicht angelegt, und eine Referenz auf die Instanz hat weiterhin den alten Wert.13  14 

Beispiel14 

class Teil {
  static final String mes= "Fehlerhafter Teile-Schlüssel!";
  String nr;
  // throws nicht notwendig, aber eine explizite 
Drohung!
  Teil(String nr) throws IllegalArgumentException {
    if (nr.length()>10) throw new IllegalArgumentException(mes);
    try { Long.parseLong(nr.replace('-','0')); }
    catch (NumberFormatException e) {
      throw new IllegalArgumentException(mes);
    }
    this.nr=nr;
  }
}

Der Vorteil einer Unchecked-Variante liegt beim Client, der nun die Wahl hat:

public class Test {
  public static void main(String[] args) {
    String nr="234-x";
    Teil t= null;
    try { t= new Teil(nr); }
    catch (RuntimeException re) {
      System.out.println(t==null);             // :: true
    }
    // ohne try Programm-Abbruch mit Ausnahme
    new Teil(nr);         
  }
}

Galileo Computing

7.7.3 Kommunikation per Ausnahme  downtop

Die JVM unterscheidet überhaupt keine Ausnahmen, der Java-Compiler kennt nur checked und unchecked Exceptions, eine hilfreiche, aber viel zu grobe Unterteilung.

Ausnahmen
differenziert nach Kommunikationsart


Abbildung
Abbildung 7.4   Ausnahme-Klassifikation anhand der Kommunikationsart

Die Kommunikation zwischen Sender (Server) und Empfänger (Client) mittels Ausnahmen sollte man ein wenig genauer differenzieren (siehe Abb. 7.4).

Kommunikationssignal

Kommunikations-signal

Ausnahmen, die zu einer Unterbrechung des normalen Programmablaufs führen, können auch Kommunikations-Signale sein:

Ausnahme als Interrupt-Objekt

gp  Begreift man eine Ausnahme als besonderes Signal15  des Senders, dann ist es ein Interrupt-Objekt, welches den normalen Programmablauf eines eventuell unbekannten Empfängers unterbricht.

Spätestens bei den Threads wird klar, das Ausnahmen als Signale auch zu Kommunikationsaufgaben herangezogen werden, was aber leider nur sehr ungenügend von der Java-Sprache unterstützt wird.

Fehler im Code

Ausnahme:
Signalisieren
von Code- oder Umgebungsfehler

Fehler-Signale können – der Interpretation der Java-Entwickler folgend – in Fehler im Code und Umgebungsfehler unterschieden werden

Fehler im Code müssen letztendlich entfernt werden (siehe 7.4.6).

Icon

Hat man für diese Art von Laufzeitfehlern checked Exceptions gewählt, steht man anschließend vor vielen try-catch-Anweisungen, die den Code sinnlos aufblähen. Dies führt zum ersten Separations-Idiom.

RuntimeException: geeignet für Fehler im Code

Exception-Separation: Unterscheide Fehler im Code von Umgebungsfehlern und signalisiere Codierungsfehler mit Hilfe einer Subklasse von RuntimeException.

Die Nichteinhaltung von Kontrakten fällt häufiger unter die Rubrik »Fehler im Code«. Ein unzulässiges null-Argument kann durchaus mit einer NullPointerException geahndet werden. Ein Client hat ja die Wahl, den Kontrakt einzuhalten.

Umgebungsfehler

RuntimeException: ungeeignet für Umgebungsfehler

Umgebungsfehler dürfen nicht ignoriert werden. Hierfür ist eine anonyme RuntimeException nicht sinnvoll, da man bei der Benutzung des Services die try-catch-Anweisungen nicht weglassen darf.

Normale Reaktion: Die Server-Methode erklärt die Ausnahme im Kopf der Methode mittels throws als Subtyp von Exception (nicht Runtime!) und erwartet, dass sich der Client um das Problem kümmert.

Error: tötlicher Umgebungsfehler

Harte Reaktion: Die Server-Methode löst eine Ausnahme vom Typ Error aus. Als Resultat stirbt wahrscheinlich auch der Client, da dieser mit catch(Exception e) {...} keinen Error abfangen kann.

Fatale vs. nicht fatale Fehler

Fatale Fehler:
der Client
entscheidet

Der Client unterscheidet die bei ihm eintreffenden Ausnahmen in fatale Fehler, die ihn seinerseits zum Abbruch zwingen, und in nicht fatale.

gp  Was fatal ist, bestimmt der Client.

Nicht fatale Fehler:
Wiederholen/Ignorieren

gp  Nicht fatale Fehler können mit der Strategie »Wiederholen« (mit/ohne Variation) oder »Ignorieren« behandelt werden.

Ein gescheiterter Verbindungsaufbau oder Sperrversuch eines Datensatzes führt zwar zu einer Ausnahme, aber eventuell erst nach mehrmaliger Wiederholung zu einem fatalen Fehler.

In einer Multimedia-Anwendung mag es Laufzeitfehler geben, die aber bis zu einem Grenzwert ignoriert werden müssen.16  Dies führt zum zweiten Separations-Idiom.

Icon
Separiere fatale Fehler

Exception-Separation: Der Client unterscheidet Ausnahmen nach fatalen und überbrückbaren Fehlern.


Galileo Computing

7.8 Ausnahmen-Behandlung  downtop

Ausnahmen zu deklarieren und auszulösen ist die eine Sache, die richtige Behandlung der Ausnahmen im Client-Code meistens die schwerere.

Ein Grund liegt darin, dass try-catch-Anweisungen logisch ähnlich aufgebaut sind wie switch- und goto-Anweisungen.

Ist dann der Code, der auf try-catch-Anweisungen folgt, auch noch abhängig davon, ob und welche Ausnahmen aufgetreten sind, führt dies zum Spaghetti-Syndrom.17 


Galileo Computing

7.8.1 Beispiel: Messwert-Import  downtop

Messwert-Import

Ausnahme-Behandlung an kurzen Code-Fragmenten zu diskutieren, hat wenig Überzeugungskraft. Deshalb sollen anhand des Beispiels »Messwert-Import« die bisherigen und folgenden Design-Vorschläge diskutiert und umgesetzt werden.

Aufgabe: Einlesen und Prüfen

Aufgabe: Einlesen und Überprüfen von Messwert-Paaren (Datum, Temperatur) aus einer Textdatei, Übertragen der Werte in eine Datenbank und Bereitstellen der gelesenen Werte in einem Container (siehe Abb. 7.5).

Bildliche Vorstellung der Aufgabe


Abbildung
Abbildung 7.5   Messwert-Import in eine RDBMS-Tabelle

Wichtig: Bei der Klasse TempImport steht primär das Ausnahme-Design im Mittelpunkt. Das Klassen-Modell und die RDBMS/JDBC-Anweisungen interessieren hier nur marginal.

Klassen zu
Messwert-Import: TempValue TempImport


Abbildung
Abbildung 7.6   Applikation TempImport

Messwert-Import-Design: Ausnahme-Sicht

Die Entscheidung über die Übertragung der neuen Messwerte in die Datenbank kann aufgrund der Restriktionen erst am Ende des Einlesens fallen. Da die Messwerte als Objekte in eine Kollektion übertragen werden, ist eine minimale Messwert-Klasse TempValue nützlich.

Betrachten wir die beiden Klassen nur aus der Sicht der Ausnahmen.

Ausnahmen zur Klasse TempValue

class TempValue {
  public TempValue(String sDate, String sTemp)
   throws ParseException, NumberFormatException, 
          RangeException /* <-zu deklarieren */ {}
}

Server-Sicht: Die Ausnahmen des Konstruktors zählen natürlich zu den fatalen Fehlern (in TempValue); die Instanz wird nicht angelegt.18  Zumindest zwei der drei Ausnahmen sind notwendig, um Formatfehler von der Überschreitung des erlaubten Temperatur-Intervalls zu unterscheiden.

Client-Sicht: Aus der Sicht der Client-Klasse TempImport sind alle drei Ausnahmen nicht fatal, da fehlerhafte Messwert-Zeilen erst einmal nur registriert werden und nicht zum Abbruch führen.

Ausnahmen
zur Klasse TempImport

Die zweite Klasse hält eine kleine Überraschung bereit:

class TempImport {
  public static void importTemp(String fileName, 
                                 Collection tempValues)
    throws ClassNotFoundException, FileNotFoundException,
           ParseException, RangeException, SQLException,
           IOException, NullPointerException {}
}

Ein Client der Methode importTemp() wird mit einer wahren Flut von Ausnahmen aus unterschiedlichen Packages konfrontiert.

Icon

Wir reagieren mit einer Design-Regel:

Keine Proliferation von Ausnahmen diverser Packages

gp  Man kann den Client nicht mit allen möglichen fatalen Ausnahmen aus verschiedenen Packages konfrontieren, die man selbst nutzt.

Spontane Folgerung: Eine Methode erklärt nur noch die Ausnahme Exception!

Wirkung: Der Client kann entweder seinerseits mit einer schönen Konsol-Meldung abbrechen oder mit Hilfe des instanceof-Operators versuchen, auf die einzelnen Fehler sinnvoll zu reagieren.

Der Operator instanceof ersetzt aber nur catch-Blöcke durch if-Anweisungen und setzt wiederum voraus, dass die o.a. Ausnahme-Typen bekannt sind. Wie? Nun ja, indem die Methode diese dem Client mitteilt. Das war wohl zu spontan. (q.e.d.)

Icon

Façade-Pattern

Das Façade-Pattern:
Schutz des Clients vor sinnloser Komplexität

Das Façade-Pattern schützt Clients vor einem komplexen Subsystem, indem es deren Service in eine einfach zu handhabende Schnittstelle kapselt (siehe Abb. 7.7).


Abbildung
Abbildung 7.7   Façade-Pattern
Galileo Computing

7.8.2 Design neuer Ausnahmen  downtop

Die Umsetzung des Façade-Patterns besteht im Design eigener einfacher Ausnahmen, welche die der verwendeten Packages ersetzen. Betrachten wir hierzu zuerst das Design der Java-Plattform. Es gibt zwei Extreme.

Design nach dem Standard-Package-Muster

Design von
Ausnahmen à la Standard-
Plattform

Die verschiedenen Ausnahme-Hierarchien der Java-Standard-Plattform sind leider kein leuchtendes Beispiel der Objekt-Orientierung.

Bis auf wenige Ausnahmen (etwa ParseException)19  sind alle Subklassen von Exception keine Spezialisierungen mit neuen Eigenschaften oder neuem Verhalten. Ausnahme-Instanzen unterscheiden sich – wenn überhaupt – nur in Eigenschaftswerten, d.h. in einer unterschiedlichen Meldung. Dies folgt dem Prinzip:

Pro Objekt eine Klasse.20  Dies bescherte der Java-Community eine stattliche Anzahl von Ausnahme-Klassen, die bereits im Namen ihre Objekt-Meldung tragen.

Design nach dem Muster SQLException

Design von
Ausnahmen à la java.sql

Das genannte Standard-Prinzip wurde zwar tapfer durchgehalten, nahm aber ein jähes Ende im java.sql-Package, welches nur noch die Ausnahme SQLException kennt.21 

SQLException erklärt neue Fehlereigenschaften, die mit getSQLState() bzw. getErrorCode() abgefragt werden können, und sieht sogar eine Verkettung von SQLException-Instanzen vor.

Typsichere Konstanten: ein alternatives Design-Muster

Compiler-Aufgabe:
Überprüfung der Ausnahme-Syntax

Ein Grund, warum die Plattform-Entwickler jede Ausnahme-Art mit einem neuen Typ beantwortet haben, liegt wohl in der Typsicherheit. Wird eine Ausnahme hinter throw, throws oder catch fehlerhaft geschrieben, erkennt dies der Compiler sofort, ein smartes Feature!

Die Alternativen, Ausnahmen wie z.B. NullPointerException mittels Strings "NullPointerException" oder Konstante

      static final int NULL_POINTER_EXCEPTION= 43;

zu identifizieren, leiden unter dem Problem, dass der Compiler beliebige, auch fehlerhafte Strings oder Zahlen akzeptiert. Das ist nicht akzeptabel!

Typsichere Alternative:

Design von
Ausnahmen: type-save Constants

gp  Unterschiedliche Ausnahme-Arten in einem Package werden durch verschiedene Instanzen (nicht Subtypen!) repräsentiert. Die Ausnahme-Arten werden mit Hilfe von typsicheren (type-safe) Konstanten identifiziert.

Ergebnis: Der Compiler ist in der Lage, fehlerhafte Angaben im Code zu erkennen, und trotzdem gibt es nur eine Ausnahme-Klasse.

Icon

Idiom zur typsicheren Konstante

Klassen-Muster
zu type-save Constants


Abbildung
Abbildung 7.8   Basis-Muster für typsichere Konstanten

Zum Muster gehört eine final erklärte Klasse, die die Konstanten enthält, und den einzigen Konstruktor private deklariert. Clients können somit nur die Konstanten nutzen (Abb. 7.8).

Das Idiom hat Variationen, z.B. ob man den Wert null als Konstante zulassen will oder ob man noch zusätzliche Eigenschaften benötigt.

Des Weiteren könnte man die Klasse TypesafeConstClass über ein zusätzliches Interface nutzen, um bei der Angabe der Konstanten den lästigen Klassennamen zu vermeiden.22 

Deklaration von
typsicheren
Konstanten

Verwenden wir das Idiom in der Variante »mit einer Eigenschaft«:

// typsichere Konstanten zu Ausnahmen
final class TempErr {                      // immutable! 
  public final String msg;                 // blank final
  public static final TempErr RangeErr= new TempErr("Range");
  public static final TempErr ParseErr= new TempErr("Parse");
  public static final TempErr TextOpenErr= new TempErr("TextOpen");
  //...
  private TempErr(String s) { msg= s; };
  public String toString()  { return msg; }
}

Deklaration von
typsicheren
Ausnahmen

// checked Exception: sehr minimalistisch, ohne 
Meldung!
class TempException extends Exception {    // immutable!
  public final TempErr Err;
  public TempException(TempErr tempErr) {
    if (tempErr==null) throw new NullPointerException();
    Err= tempErr;
  }
}

Abbildung
Abbildung 7.9   Erweitertes Messwert-Import-Modell

In der Klasse TempValue ist nur die Umsetzung auf die Ausnahme TempException und die Einhaltung des Immutable-Idioms interessant.

Einsatz von
typsicheren Ausnahmen am Beispiel TempValue

class TempValue {                      // immutable!
  private static DateFormat            // Format: tt.mm.yy
          df= DateFormat.getDateInstance(DateFormat.SHORT);
  public  static double minTemp;
  public  static double maxTemp=-1.0;
  private java.util.Date d;  
  private double temp;
  // muss aufgerufen werden, da maxTemp= -1.0
  public static boolean setTempRange(double minT,double maxT) {
    if (minT<=maxT) {
      minTemp= minT; maxTemp= maxT; return true;
    }
    return false;
  }
  public TempValue(String sDate, String sTemp) 
         throws TempException {
    try {
     d= df.parse(sDate);              // parse ist sehr tolerant
     if (!sDate.equals(df.format(d))) // deshalb noch ein Check ¨
        throw new TempException(TempErr.ParseErr);
      temp= Double.parseDouble(sTemp);
    }
    catch (Exception e) { //ParseException, NumberFormatException
      throw new TempException(TempErr.ParseErr);
    }
    if (minTemp > temp || temp > maxTemp)
      throw new TempException(TempErr.RangeErr);
  }
  // getDate() verletzt Kompositition, d.h. immutable
  // java.util.Date getDate() { return d; }
  String getStringDate()   { return df.format(d); 
}
  double getTemp()         { return temp; }
}

Erklärung: Damit TempValue immutable bleibt, darf getDate() keine Referenz von d nach außen liefern (siehe hierzu auch 4.2.5).

Der String-Vergleich bei ¨ prüft zusätzlich die Gültigkeit des Datums im Format tt.mm.yy, denn die Methode parse() akzeptiert z.B. auch "60.12.99" und rechnet dies einfach in "29.01.00" um.


Galileo Computing

7.8.3 Ein Idiom zur Behandlungs-Strategie  downtop

Die Methode setToleranz() ist aus der Sicht der Behandlungen diverser Ausnahmen interessant. Man hat die Wahl zwischen einem schwer durchschaubaren Spagetti-Code oder einem einfachen Idiom:23 

Icon

Ausnahme-Behandlung23 

Behandlungs-Strategie zu
Ausnahmen

gp  Vermeide – sofern die Semantik es zulässt – try-catch-Anweisungen für jede einzelne Methode.24 
gp  Reagiere zuerst auf Kontrakt-Verletzung.
gp  Behandle in einer einzigen äußeren try-catch-Anweisung alle fatalen Ausnahmen und in einem inneren try-catch die nicht fatalen.
gp  Verwende das Cleanup-Idiom (3.3.4) und verlagere alle notwendigen abschließenden Aktionen in den finally-Block.
gp  Behandle alle Ausnahmen, die durch die Aktionen im finally-Block entstehen, in finally selbst (siehe 7.3.3).

Code-Muster
zum Idiom

Code-Muster zum Idiom24 

   // vorab die Kontrakt-Prüfung, dann
   try {
      Aktionen mit fatalen Fehlern!
      try {
         Aktionen mit nicht fatalen Fehlern!
      }
      catch (NonFatalException1 e) {...}
      catch (NonFatalException2 e) {...}
      ...
   }
   catch (FatalException1 e) {...}
   catch (FatalException2 e) {...}
   ...
   finally { 
     try { abschließendes Cleanup  }
     catch(Exception e) {...}
   }

Galileo Computing

7.8.4 Behandlungs-Strategie beim Messwert-Import  downtop

Umsetzung des Idioms zu Ausnahme-Behandlungs
in der Klasse TempImport

Die Implementation von TempImport folgt dem o.a. Behandlungs-Muster, wobei die innere try-catch-Anweisung in einer while-Anweisung eingebettet ist.

Erklärungen werden direkt vor die einzelnen Anweisungen gestellt.

class TempImport {
  private static double parseERate= 0.01;  // default 1% 
  private static double tempERate=  0.01;  // default 1%
  // -------------------------------------------------------
  //    erwartet Dateiname und eine Kollektion
  //    die Übergabe als Interface-Argument erlaubt
  //      1. die Wahl der Collection-Klasse durch den Client
  //      2. die Herausgabe von Messwerten, auch
  //         wenn eine Ausnahme aufgetreten ist.
  // -------------------------------------------------------
  public static void importTemp(String fileName, 

                                Collection tempValues)
                                throws TempException {
    // --- der Kontrakt heißt: keine null-Argumente 
    ---

Kontrakt-Missbrauch mit RuntimeException beantworten

    if (fileName== null || tempValues==null)
      throw new NullPointerException(
                        "fileName oder tempValues null");
    BufferedReader txt=  null;  // Textdatei
    Connection     con=  null;  // Datenbank-Verbindung
    Statement      stmt= null;  // SQL-Statement
    // ---- nun folgen die vielen Umweltprobleme 
        ---     
    // ---- nur ein großes try, nicht viele einzelne    ---
    // ---- siehe Behandlungs-Idiom oben                 ---   

Eine äußere
try-Anweisung für alle fatalen Fehler

    try {
      // Text-Datei mit Name fileName wird geöffnet
      // fataler Fehler löst FileNotFoundException aus!
      txt= new BufferedReader(new FileReader(fileName));
      // Klasse mit dem JDBC-ODBC-Treiber wird geladen
      // fataler Fehler löst ClassNotFoundException aus!
      Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
      String s;
      int numLines= 0, // Anz. nicht leerer Messwert-Zeilen
          numPErr=0,   // Anz. Parse-Fehler
          numRErr=0;   // Anz. Range/Intervall-Fehler
      // ---- zeilenweise aus Textdatei einlesen 
      ----
      // ---- fataler Lesefehler löst IOException aus  ----
      while ((s= txt.readLine())!= null) {
        // ---- leere Textzeilen 
sind zu überspringen  ----
        if (!(s= s.trim()).equals("")) { 
          numLines++;
          // ---- String in Token (Datum,Temp) zerlegen 
----
          // ---- Trenner sind white spaces             ----
          StringTokenizer st= new StringTokenizer(s);
          // ---- es folgen die nicht fatalen Fehler 
   ----
          // ---- in einem inneren try                  ----

Eine innere
try-Anweisung für alle nicht fatalen Fehler

          try {
            // ---- zufügen zur Collection 
hinzufügen
            // ---- löst nicht fatale Fehler aus
            tempValues.add(
              new TempValue(st.nextToken(),st.nextToken()));
          }
          // ---- zählen und weitermachen  
             ----

Behandlung
aller nicht
fatalen Fehler

          catch (NoSuchElementException e) { numPErr++; 
}
          catch (TempException e) {
            // ---- Unterscheidung der Ausnahme 
        ----
            if (e.Err == TempErr.ParseErr) numPErr++;
            else numRErr++;
          }
        }
      }
      // ---- wenn keine Messwerte vorhanden ‡ Ende    ----
      // ---- da sonst ArithmeticException              ----
      // ---- wichtig: finally kommt vor return         ----
      if (numLines==0) return; 
     
      // ---- sind die nicht fatalen Fehler fatal? 
    ----
      if (numPErr/numLines>parseERate)
        throw new TempException(TempErr.ParseErr);
      if (numRErr/numLines>tempERate)
        throw new TempException(TempErr.RangeErr);
      // ---- Verbindung zur Datenbank aufbauen 
        ----
      // ---- fatale Fehler lösen SQLException aus      ----
      con= DriverManager.getConnection("jdbc:odbc:AccessTest1");
      stmt= con.createStatement();
      // ---- Iteration durch die Messwert-Kollektion 
  ----
      for (Iterator i= tempValues.iterator(); i.hasNext(); 
) {
        TempValue tv= (TempValue) i.next();
        // ---- Einfügen in die Datenbank  
             ----
        stmt.executeUpdate("INSERT INTO MESSWERTE 
VALUES ('"+
                           tv.getStringDate()+"',"+
                           Double.toString(tv.getTemp())+")");
      }
    }
    // ---- zentrale Behandlung aller fatalen Fehler 
   ----
    // ---- und Umwandlung in eine TempException        ----

Behandlung aller
fatalen Fehler

    catch (FileNotFoundException e) {
      throw new TempException(TempErr.TextOpenErr);
    }
    catch (ClassNotFoundException e) {
      throw new TempException(TempErr.JDBCDriverErr);
    }
    catch (IOException e) {
      throw new TempException(TempErr.TextReadErr);
    }
    catch (SQLException e) {
      // ---- Abfrage auf 08001 ist nicht so toll 
      ----
      if (e.getSQLState().equals("08001"))
        throw new TempException(TempErr.ConnectErr);
      throw new TempException(TempErr.SQLExecErr);
    }

Cleanup im
finally-Block

    // ---- nicht null ‡ Öffnen war erfolgreich 
       ----
    // ---- Cleanup-Aktion: Schließen                   ----
    finally {
      // ---- ohne try-catch würde hier eine Ausnahme   ----
      // ---- alle fatalen Fehler ersetzen              ----
      try {

Icon

// siehe zu dieser Lösung abschließenden 
Kommentar

Keine Resume-Next-Logik

 if (txt!= null)  txt.close();
        if (con!= null)  con.close();
        if (stmt!= null) stmt.close();
      } catch (Exception e) {}
    }
  }
  // --------------  Ende von importTemp()  ----------------
  public static boolean setTempRange(double minT, 
double maxT) {
    return TempValue.setTempRange(minT,maxT);
  }
  public static boolean setToleranz(double parseErrRate,
                                    double tempErrRate) {
    if (0 <= parseErrRate && parseErrRate < 1 &&
        0 <= tempErrRate  && tempErrRate < 1) {
      parseERate= parseErrRate; tempERate= tempErrRate;
      return true;
    }
    return false;
  }
}

Typensichere
Ausnahmen:

Die Verwendung der Import-Klasse kann wieder als Code-Fragment dargestellt werden:

Nur ein catch-Block, keine Typvergleiche mit instanceof

TempImport.setTempRange(-10.0,30.0);
Collection c= new java.util.ArrayList();
try {
  TempImport.importTemp("C:/Messwerte/TempValues.txt",c);
}
catch (TempException e) {
  System.out.println(e);
  System.out.println(e.Err);  // Art der Ausnahme!
}

Im abschließenden finally-Block von importTemp() werden alle geöffneten Ressourcen wieder geschlossen. Wirklich alle?

try-Anweisung und das
Ignorieren von Ausnahmen

Nein, denn der Code basiert auf der einfachen Annahme, dass alles, was geöffnet wurde, auch geschlossen werden kann. Beteiligt daran ist das try-catch, welches beim ersten Fehler, z.B. verursacht durch eine txt.close-Anweisung, abbricht, und damit – man ahnt es bereits – die restlichen Ressourcen con, stmt offen lässt.

In diesem Fall ist die Lösung einfach, unelegant und simuliert eine »On Error Resume Next«-Logik25  :

  if (txt!=null) try { txt.close(); } catch (Exception 
e) {}
  ...

Damit ist auch die Einschränkung »sofern die Semantik es zulässt« im ersten Punkt des Idioms zur Ausnahme-Behandlung erklärt.


Galileo Computing

7.9 Zusammenfassung  downtop

Ohne ein schlüssiges Ausnahme-Konzept ist das Schreiben von robusten Klassen extrem schwierig, wenn nicht unmöglich.

Java bietet ein Ausnahme-Konzept, welches Fehler auf der Basis »not recoverable« behandelt. Unterstützt wird es vom Compiler bzw. der JVM und einer umfangreichen Ausnahme-Hierarchie in der Plattform.

Anhand der Unterscheidung der Subtypen Error, checked und unchecked Exception können Laufzeitfehler differenziert behandelt werden.

Der Ausnahme-Mechanismus bietet mit dem finally-Block immer die Möglichkeit von Cleanup-Aktionen an, selbst wenn fatale Fehler aufgetreten sind.

Bei der Deklaration eigener Ausnahmen im Server und der Behandlung von Ausnahmen beim Client helfen u.a. ein Façade-Pattern sowie einige Idiome.

Am Beispiel »Messwert-Import« werden alle vorgestellten Design- bzw. Behandlungs-Hinweise und Idiome abschließend demonstriert bzw. diskutiert.26 


Galileo Computing

7.10 Testfragen  toptop

Zu jeder Frage können jeweils eine oder mehrere Antworten bzw. Aussagen richtig sein.

             throw new EClass();
   class MyException extends Exception {}
   abstract class B {
    abstract void f() throws Exception;
    abstract void g();
  }
  class D extends B {
    f() /* ... */ { /*...*/ }
    g() /* ... */ { /*...*/ }
  }
    class ExTest {
      static void f(int i, String s) {
        if (s=="") throw new NullPointerException();
        System.out.println (s);
        System.out.println (1/i);
      }
    }
Welche Meldungen sind Teil der Ausgabe der folgenden Anweisung?
          ExTest.f(0,null);
8. Es ist folgende Klasse deklariert:
     class CatchTest {
       static void f(String s) throws Exception {
         try { int i= Integer.parseInt(s); }
         catch (Exception e) {
           System.out.println("F1"); throw e;
         }
         finally { System.out.println(s); return; }
       }
     }
Welche Zeilen sind Teil der Ausgabe der folgenden Anweisung?
     try { CatchTest.f("a"); }
     catch (Exception e) { System.out.println("F2"); }
   class ThrowTest {
     void f() {
       if (true) throw new RuntimeException();            // 3
       if (true) throw new RuntimeException();            // 4
       if (true) throw new Error();                       // 5
       if (true)
         try { throw new Exception(); } catch (Error e){} // 7
       if (true)
         try { new Exception(); } catch (Throwable e){}   // 9
     }
   }





1    In Englisch sehr anschaulich mit »Throwing an exception« beschrieben.

2    Werden Ausnahmen auch zur Kommunikation in »außergewöhnlichen Fällen« benutzt, ist das Konzept zu inflexibel. Leider gibt es diese Situation auch in der Java-Sprache selbst, u.a. bei der Kommunikation von Threads (siehe Kapitel 9, Threads).
Dies führt dann zu einem unpassenden Programm-Design, da um die Defizite in Java »herumprogrammiert« werden muss.

3    Leaf-Klassen haben keine Subklassen mehr.

4    Sollte der Grund nicht klar sein, unbedingt das Marker-Interface-Idiom in 6.10 nacharbeiten!

5    Der Stack Trace sollte natürlich nur für das Debugging von Code interessant sein und weniger für den genervten Kunden.

6    Jedoch kann z.B. auch eine Ausnahme vom Typ VerifyError mit Hilfe einer inneren Klasse provoziert werden, welches kein Sicherheitsproblem, sondern eine Inkonsistenz der JVM aufdeckt (siehe Kapitel 8, Innere Klassen).

7    Siehe hierzu 7.7.2

8    Man denke nur an Programme zur Maschinensteuerung, die bei Umweltfehlern nicht einfach mit Abbruch oder Konsol-Meldungen à la Dr. Watson reagieren können.

9    Unchecked Exceptions sind ausgenommen und können beliebig deklariert oder weggelassen werden.

10    Zur Unchecked-Variante siehe 7.7.2.

11    Das ist zu trivial, da dann auch Teile-Nummern wie »--1--234--« zugelassen wären.

12    Das Thema ist deshalb so heikel, weil beim Ausnahme-Design zur Sun-Plattform der bekannte MS-Grundsatz »Unsere Fehler sind Features« befolgt wurde (siehe 7.8.2).

13    Sofern man nicht eine this-Referenz vor der Ausnahme nach außen gereicht hat.

14    Siehe Beispiel zur Checked-Variante in 7.6.

15    Im Sinn der UML ist mit einem Signal ein Signal-Objekt verbunden.

16    Eine Audio/Videoausgabe wird nicht gleich wegen eines fehlerhaften Datensignals abgebrochen.

17    Endlose Fallunterscheidungen als Reminiszenz an das dunkle Prä-OO-Zeitalter.

18    Die Instanz wird nicht angelegt.

19    Zum Beispiel ParseException

20    Dies steht im krassen Gegensatz zur Klassifizierungs-Regel in 4.1.1.

21    Das alte Muster wäre aufgrund der unzähligen datenbankspezifischen Ausnahmen gegen einen gewaltigen Exception-Baum gefahren. Interessant, SQLWarning ist zwar eine Subklasse von SQLException, wird aber nicht mit throw ausgelöst.

22    Dies bedeutet allerdings, dass der Konstruktor nicht private sein darf.

23    Die Relevanz des Idioms wird zur Übung erklärt (Hilfe: Man programmiere einfach die Methode importtemp()).

24    Dies ist nicht immer möglich, z.B. dann nicht, wenn Fehler ignoriert werden müssen (siehe hierzu Endkommentar zur Implementation von importTemp()).

25    Visual Basic-Jargon, siehe auch 7.1.

26    Ein schönes delegations-basiertes Aufnahmesystem fiel leider dem Rotstift zum
Opfer, da es inklusive eines passenden Beispiels den Rahmen des Kapitels gesprengt hätte.

  

Perl – Der Einstieg




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de