Kurs:Java – ein schneller Einstieg/Ausnahmen behandeln

Ausnahmen behandeln Bearbeiten

Die erste Reaktion bei einem Fehlverhalten des selbst erstellten Programms ist fast immer falsch. Nicht Java hat einen Fehler, sondern das eigene Programm. Damit wurde ein "Feind" lokalisiert, der äußerst schwierig zu überwinden ist – die eigenen Gedankengänge. Hier ist die Lokalisierung zum Glück einfach.

Lokalisieren des Konzeptionsfehlers Bearbeiten

Sobald sich das System wieder beruhigt hat, sollte an der Bewältigung dieses Problems gearbeitet werden. Hier gleich ein Hinweis: Die umfangreichen Ausgaben im Kommandozeilenfenster sind vielleicht für angehende Systemanalytiker zu Zeiten ihres Informatikstudiums interessant. In der Praxis bedeuten sie jedoch nur, dass ein sehr unangenehmer Fehler vorhanden ist, der in der Konzeption selbst liegt.

Was wurde vergessen? Diese Frage mag zunächst im Vordergrund stehen, dafür ist es aber zu früh. Nicht die Überlegung nach etwas Fehlendem sollte stattfinden, sondern die nach "zu viel" oder auch "zu früh". Hier einige Vorschläge:

  • Die Methode calculate() wird mit Zeichenketten konfrontiert und deshalb bei der Division durch 0 scheitern.
Leider falsch. Die Methode wird zwar aufgerufen, aber der Fehler liegt tiefer.
  • Die Methode valueOf(...) gibt fehlerhaften Werte zurück, weil keine korrekten Zeichenketten vorhanden sind.
Schon besser, aber trotzdem falsch. Eine genaue Untersuchung würde ergeben, dass diese Methode überhaupt nichts zurückgibt.
  • Es ist kein methodischer Fehler, sondern ein Instanzierungsfehler.
Das ist es. In der Methode valueOf(...) wird versucht ein Objekt der Klasse Float aus einer Zeichenfolge zu instanzieren. Selbst wenn es gelänge überhaupt ein Float-Objekt zu erzeugen, was soll dann bitte für ein float-Wert in Zeichenfolgen wie "XYZ", "schnippendipp" oder "wrzlprm" hineininterpretiert werden? Hier ist also der Fehler zu suchen.
Aufgabe:
  1. Warum ist der Fehler konzeptioneller Art?
  2. Wie können derartige Fehler bereits im Ansatz vermieden werden?

Auffangen von Exceptions Bearbeiten

Erste Hinweise auf das eigentümliche Verhalten finden sich in der Float-API-Dokumentation. Da steht etwas von "throws NumberFormatException". Soll das etwa bedeuten, dass Dieses Float-Objekt mit Ausnahmen um sich wirft?

Genau das bedeutet es. Die Macher von Java nehmen ihre Bezeichner beim Wort. Wenn also eine unkorrekte Zeichenkette vorliegt, also ein String mit Merkmalen die nicht einem float-Wert entsprechen, wird bei der Instanzierung der Klasse eine Ausnahme wegen eines fehlerhaften Zahlenformats erzeugt. Ausnahmen (Exceptions) sind in Java ganz normale Klassen, die eben ganz normale Objekte bereitstellen. Der Programmierer muss diese Objekte eben nur "auffangen" und entsprechend reagieren.

Exceptions erkennen und entsprechend reagieren Bearbeiten

Sequenzen die "umherfliegende" Exceptions nach sich ziehen können, werden nach einem bestimmten Schema verfasst. Dieses Schema funktioniert nach dem "Versuch-und-Irrtum-Prinzip" und wird in Java unter der Bezeichnung "ExceptionHandling" geführt. Zunächst wird der Versuchsblock (try {...}) mit der/den kritischen Anweisungen erstellt. Weil hier das eine oder andere Exception-Objekt unkontrolliert durch die Gegend fliegen könnte, wird ein Fängerblock (catch(...) {...}) unmittelbar hinter den Versuchsblock gestellt. Natürlich darf dieser Fänger nicht jedes "vorbeifliegende" Exception-Objekt abfangen, sondern nur ganz bestimmte. Die ihm als Argument übergeben werden. Die Grundlage eines "Abfangsystems" für fehlerhafte Floats ist

Float zahl = null;
try   {
       zahl = new Float( text);
      }
catch (NumberFormatException nfe)
      {
       System.out.println( "Unkorrekte Eingabe.");
       System.exit( 1);
      }

Diese Zeilen in die Methode valueOf(...) statt der ursprünglichen Float-Instanzierung müsste auf fehlerhafte Eingaben mit einem Hinweis und anschließendem Programmabbruch reagieren. Tatsächlich funktioniert diese Lösung, kann aber nur als Teillösung angesehen werden, die sich kein Anwender bieten zu lassen braucht. Selbstverständlich darf sich das System nicht einfach wegen eines Tippfehlers verabschieden, sondern muss geduldig warten, bis die Eingabe korrekt ist.

Aufgabe:

Diese "Versuch-und-Irrtum"-Vogehensweise auf die Eingaben im "RabattPanel" anwenden.

Unterschiede zwischen Anwendung und Benutzer bei Eingabefehlern Bearbeiten

Zunächst wird festgelegt, was geschehen soll wenn die Eingabe felherhaft ist.

Der Focus kehrt auf das Feld mit der fehlerhaften Eingabe zurück. Das Feld soll außerdem rot hinterlegt sein und der fehlerhafte Text in gelb erscheinen.

Was für eine Berechnung fehlerhaft ist und was ein Anwender als fehlerhaft akzeptiert ist zweierlei. Wenn überhaupt kein Eingabetext vorhanden ist, stellt diese Tastsache für die Berechnung zweifellos einen Fehler dar, dem Anwender dürfte dieses Annahme allerdings nur schwer zu vermitteln sein. Deshalb darf die Farbänderung der Felder nur dann erfolgen, wenn sie einen textuellen Inhalt haben.

Derartig umfangreiche Anforderungen verlangen nach speziellen Methoden. Eine davon wird sich mit der Korrektheit von Eingaben auseinandersetzen, eine andere mit der Kennzeichnung (Farbgebung) der betroffenen Felder. Allerdings muss vorher überprüft werden, ob überhaupt gerechnet werden soll. Zweifellos wird der Anwender bei einem leeren Betragsfeld auch ein leeres Ergebnisfeld erwarten. Damit ergibt sich bereits ein Entwurf für eine Sequenz:

// wenn betragFeld leer, dann ausgabeFeld leer machen
if (betragFeld.getText().length() < 1) ausgabeFeld.setText( "");

Es ist tatsächlich nur ein Entwurf, denn das Ausgabefeld wird zwar geleert, aber unmittelbar im Anschluss erfolgt die Berechnung mit dem bekannten "Ergebnis". Etwas fehlt.

// wenn betragFeld leer, dann ausgabeFeld leer machen und fertig
if (betragFeld.getText().length() < 1)
   {
    ausgabeFeld.setText( "");
    return;
   }

Die Methode wird jetzt verlassen bevor eine Berechnung ausgeführt wird. Leider funktioniert auch dieser Entwurf nicht. Die Methode "valueOf(...)" ist typisiert. Das heisst, sie muss etwas zurück geben. Hier ist irgendein float-Wert verlangt. Besser ist es, die Methode erst aufzurufen, wenn die Eingabe in Ordnung ist.

Es geht erst einmal um die Überprüfung nicht leerer Eingaben. Die bereits besprochene Versuch-und-Irrtum-Sequenz kommt zur Anwendung. Einer Methode wird der Eingabetext als Argument übergeben und der Rückgabewert gibt Auskunft darüber, ob der String den Anforderungen eines möglichen Float-Objekts genügt.

private boolean inputOk( String input) {
Float zahl = null;
 try   {
        zahl = new Float( input);
       }
 catch (NumberFormatException nfe)
       {
        return false;
       }
 return true;
}

Die ursprüngliche Methode valueOf(...) kann also bleiben wie sie ist, wenn gewährleistet ist, dass die Methode inputOk(...) unmittelbar vorher aufgerufen und deren Ergebnis entsprechend berücksichtigt wird.

Wo ist der günstigste Ort, diese neue Methode aufzurufen? Als Antwort dient selbstverständlich die Reaktion auf den Rückgabewert false. Wenn dieser Wert geliefert wird, soll das entsprechende Eingabefeld seine Farbe ändern und es darf keine Berechnung erfolgen. Innerhalb der Methode calculate(), die ohnehin als Argument dient, sollte also die Überprüfung nicht stattfinden. Damit bleibt nur die Methode actionPerformed(...) des internen EventListeners, in der sich ja bereits die Überprüfung auf leer befindet.

Farben von Komponenten ändern Bearbeiten

Wenn der String in betragFeld nicht ok ist, dann erhält das betragFeld einen roten Hintergrund und einen gelben Vordergrund und die Methode wird verlassen.

if (!inputOk( betragFeld.getText()))         // wenn nicht String in betragFeld ok
   {                                         // dann
    betragFeld.setBackground( Color.red);    // betragFeld erhält roten  Hintergrund
    betragFeld.setForeground( Color.yellow); // betragFeld erhält gelben Vordergrund
    return;                                  // Methode verlassen
   }

Diese Sequenz in die Methode actionPerformed(...) integriert (RabattPanel10.java), sollte bei einer nichtnumerischen Eingabe in betragFeld und anschließendem Klick auf die Schaltfläche "Berechnen" die neue Farbgestaltung zeigen.

Eingabemarken gezielt vom Programm setzen Bearbeiten

Es fehlt noch eine entsprechende Positionierung des Focus. Wenn also diese "Umfärbeaktion" erfolgt ist, muss der Cursor wieder im betragFeld blinken. Es liegt also nahe, eine Methode namens setFocus(...) zu suchen und entsprechend anzuwenden. Jedoch ist diese Methode nicht vorhanden. Der Grund liegt keinesfalls in der Willkür der Java-Macher begründet, sondern in der Tatsache, dass ein Focus-Handling sehr viel komplexer ist als angenommen. Eben diesem Focus-Handling kann aber der Wunsch nach Aufmerksamkeit (dem Focus eben) mitgeteilt werden. Die nach Aufmerksamkeit heischende Komponente teilt ihren Wunsch über die Methode requestFocus() mit und der Focus-Handler wird daraufhin alles in seiner Macht stehende tun, um diesem Wunsch nachzukommen.

betragFeld.requestFocus();

Diese Anweisung fehlte noch in der Fehlerbehandlung von betragFeld. Nun noch diese Anweisungen auf das rabattFeld anwenden und die Beispielaufgabe ist beinahe fertig. Die Datei "RabattPanel11.java" zeigt das bisher Erreichte.

Eine Sache fehlt noch. Dem Anwender werden seine "Fehler" immer vor Augen gehalten. Die Application ist extrem nachtragend, denn einmal gefärbte Felder werden ihre "Fehlermeldungen" nicht zurücknehmen. Es ist also noch diese letzte Hürde zu nehmen, bevor die ursprünglich gestellte Aufgabe bewältigt ist.

Die Felder sollen immer dann "normal" erscheinen, wenn keine fehlerhaften Eingaben vorhanden sind. Nicht fehlerhaft im Sinne des Anwenders! Also müssen sie immer dann "normal" gefärbt sein, wenn die Berechnung ausgeführt werden kann oder wenn eine leere Eingabe vorhanden ist. Damit wären fünf Stellen vorhanden, an denen Farbwechsel erforderlich werden könnten. Insgesamt wären das zehn Zeilen Code. Eine Anzahl, die an der Übersichtlichkeit eines ActionListeners erhebliche Zweifel aufkommen lässt. Also noch eine Methode zum "färben".

Der Methode sollen als Argumente nur das betroffene JTextField und ein Indikator für "Fehler" übergeben werden. Wieder ist ein Entwurf in "natürlicher" Sprache angebracht.

Wenn Fehler, dann färbe JTextField-Referenz gelb/rot und fordere den Focus an. Sonst färbe JTextField-Referenz schwarz/weiß.

private void setColor( JTextField field, boolean error) {
 if (error)                              // Wenn Fehler
    {                                    // dann
     field.setBackground( Color.red);    // Feld erhält roten  Hintergrund
     field.setForeground( Color.yellow); // Feld erhält gelben Vorderergrund
     field.requestFocus();               // Feld wünscht Focus
     return;                             // Methode verlassen
    }                                    // (sonst)
 field.setBackground( Color.white);      // Feld erhält roten  Hintergrund
 field.setForeground( Color.black);      // Feld erhält gelben Vorderergrund
}

Der Einsatz dieser Methode vereinfacht außerdem die Abläufe. So kann der – bisher unbemerkt gebliebene – Fehler in der Methode ActionPerfomed(...) auch einfacher gefunden werden. Zunächst jedoch der Einsatz von setColor(...).

public void actionPerformed( ActionEvent ae) {
 if (betragFeld.getText().length() < 1)
    {
     ausgabeFeld.setText( "");
     setColor( betragFeld, false);
     return;
    }
 if (!inputOk(betragFeld.getText()))
    {
     setColor( betragFeld, true);
     return;
    }
 if (rabattFeld.getText().length() < 1)
    {
     rabattFeld.setText( "");
     setColor( rabattFeld, false);
     return;
    }
 if (!inputOk(rabattFeld.getText()))
    {
     setColor( rabattFeld, true);
     return;
    }
 setColor( betragFeld, false);
 setColor( rabattFeld, false);
 ausgabeFeld.setText( ""+calculate());
}

Die Datei "RabattPanel12.java" enthält den gesamten Quelltext. Einschließlich des bereits erwähnten Fehlers.

Wenn in betragFeld keine und in rabattFeld eine fehlerhafte Eingabe erfolgt, so bleibt die geforderte Färbung von rabattFeld aus. Die Methode hat überhaupt keine Möglichkeit, rabattFeld in irgendeiner Form zu überprüfen, weil sie wegen der Leere in betragFeld vorher verlassen wird. Offensichtlich verhindern die "returns" weitergehende Tests. Aber sie sind nicht die alleinige Ursache, wie eine genauere Untersuchung zeigt.

Von den beiden Tests auf nicht vorhandene Eingabe wird nur erwartet, dass bei positivem Testergebnis im ausgabeFeld ebenfalls nichts steht und eine Berechnung verhindert wird. Eine Berechnung darf ebenfalls nicht erfolgen, wenn einer der beiden Tests auf Korrektheit ein positives Ergebnis liefert. Auch hier wäre ein leeren von ausgabeFeld angebracht. Es existiert überhaupt nur eine einzige Anweisung, die etwas in ausgabeFeld schreibt, nämlich die Methode calculate(). Daher kann dieses TextField ruhigen Gewissens gleich am Anfang der Methode geleert werden.

Weiterhin darf davon ausgegangen werden, dass Eingaben in leeren Feldern zu erwarten sind. Wenn also ein Eingabefeld leer ist, sollte der Focus auf dieses gesetzt werden, es sei denn ein Fehler liegt in einem anderen Feld vor. In einem unangenehmen Fall ist also betragFeld leer und rabattFeld hat eine fehlerhafte Eingabe. Der Focus muss dann auf dementsprechend gefäbten rabattFeld positioniert werden und nicht auf das leere betragFeld. Es geht aber noch viel unangenehmer. Der Anwender macht in beiden Eingabefeldern Fehler, klickt auf die Schaltfläche zur Berechnung und nur eines der beiden Felder wird gefärbt. Es sind zu viele Eventualitäten zu bedenken. Ein neues Konzept muss her.

Die Essenz ist: Nur berechnen, wenn beide Eingaben vorhanden und ok, sonst Focus auf erstes fehlerhaftes Feld oder erstes leeres Feld.

Ein erster Entwurf aus der Essenz mit den vorhandenen Methoden lautet:

  • AusgabeFeld leeren.
  • Wenn Text in betragFeld nicht ok, dann Test ob leer.
  • Wenn leer schwarz/weiß und für Focussierung vormerken.
  • Sonst gelb/rot und für Focussierung vormerken.
  • Wenn Text in rabattFeld nicht ok, dann Test ob leer.
  • Wenn leer schwarz/weiß und für Focussierung vormerken.
  • Sonst gelb/rot und für Focussierung vormerken.
  • Wenn Vormerkung für Focussierung, dann Focus anfordern und Methode verlassen.
Aufgabe:

Diesen Entwurf in einem Programm umsetzen.

  • Mit "RabattPanel12.java" vergleichen und Unzulänglichkeiten mit dem folgenden Text vergleichen.

Eine Vormerkung zur Focussierung! Wie soll das vonstatten gehen? Einfach durch die Referenzierung eines focussierbaren Objektes ohne den Focus gleich zu setzen. Also eine Referenz-Variable vom "Typ" JTextField erst einmal mit null vorbelegen. Wenn dann eine Vormerkung zur Focussierung erfolgen soll, dieser Variablen das Objekt (z.B. rabattFeld) zuweisen. ist die Variable am Ende der Test immer noch auf null, kann die Berechnung erfolgen weil alle Tests negativ verliefen. Im anderen Fall über das durch diese Variable referenzierte Objekt die Methode requestFocus() aufrufen und keine Berechnung durchführen.

Ach wenn es doch so einfach wäre. Es existieren verschiedene Prioritäten der Focus Anforderung. Wenn ein Feld leer ist und das folgende fehlerhaft, so darf nicht das leere Feld den Focus erhalten sondern das fehlerhafte. Zweckmäßig sind demnach zwei verschiedene Variablen für die Reservierung der Focus-Anforderung; eine Anforderung für leere Feöder und eine für fehlerhafte. Nun darf eine Reservierung innerhalb einer Prioritätsebene nur dann sofort erfolgen, wenn es sich die erste handelt. Eine zweite kann nicht angenommen werden.

Es scheint, als sei der Entwurf damit hinfällig. Ist er aber nicht, denn Entwürfe können erweitert werden.

  • AusgabeFeld leeren.
  • Wenn Text in betragFeld nicht ok, dann Test ob leer.
  • Wenn leer schwarz/weiß und für leer-Focussierung vormerken.
  • Sonst gelb/rot und für fehler-Focussierung vormerken.
  • Wenn Text in rabattFeld nicht ok, dann Test ob leer.
  • Wenn leer schwarz/weiß und
  • wenn noch keine Vormerkung für leer oder fehler, für leer-Focussierung vormerken.
  • Sonst gelb/rot und
  • wenn noch keine Vormerkung fehler, für fehler-Focussierung vormerken.
  • Wenn Vormerkung für fehler-Focussierung, dann Focus anfordern und Methode verlassen.
  • Wenn Vormerkung für leer-Focussierung, dann Focus anfordern und Methode verlassen.
  • schwarz/weiß für alle Eingabefelder.
  • Berechnung durchführen.
Aufgabe:

Diesen Entwurf in einem Programm umsetzen.

Wenn erfolgreich, dann überlegen ob es jetzt noch zu fehlerhaften Inhalten im Ergebnisfeld kommen kann.

Soweit der Entwurf. Die Realität ist in der Datei "RabattPanel13.java" zu finden. Irgendwie bleibt aber der Eindruck eines uneleganten Programms. Tatsächlich werden derartig umfangreiche Handler selten erstellt. Es ist kaum vorstellbar, welche Code-Monster bei 20 bis 30 Eingabefeldern entstehen, wie sie in der täglichen Praxis häufig anzutreffen sind. Monster werden durch gute Zuhörer zu wahren Schoßtieren. So kann denn auch dieses Problem mit einem geeigneten Zuhörer vereinfacht werden. Sein Name ist "FocusListener". Seine Fähigkeiten reichen weit über das hier angesprochene hinaus, sofern er richtig eingesetzt wird.

Nachweislich fehlerhafte Feldinhalte Bearbeiten

Das Ergebnis der ersten Java-Anwendung kann sich eigentlich sehen lassen. Viele Aspekte der Sprache wurden angesprochen, diskutiert, verworfen usw. Zufriedenheit könnte sich breitmachen, wäre da nicht der Kunde, der mit seinen in seiner grenzenlosen Unbedarftheit ein Ergebnis zutage fördert, das offensichtlich totaler Unsinn ist. Kunden werden nur von ihren Wünschen geleitet und die Möglichkeit einen Rabatt zu bekommen bringt sie auf die abstrusesten Ideen. So wird wahrscheinlich die folgende Feldkombination auftreten (die Werte mögen variieren, jedoch die Fehlerhaftigkeit bleibt):

Betrag    = 1000.00
Rabatt    = 5
Endbetrag = 50.00

Wie kann das sein? Das Programm hat doch immer korrekt gerechnet. Der Fehler muss "ganz unten" liegen und irgendwie wohl mit den komplexen Wandlungen von Zeichenketten, Zahlen und Werten zusammenhängen. Also erst einmal die angezeigten Werte eingeben und versuchen dieses Ergebnis nachzuvollziehen. Leider erscheint immer das korrekte Resultat, nach Klick auf die Schaltfläche. Es muss also am System des Kunden liegen. Aber auch "vor Ort" zeigt sich das Programm nur von seiner fehlerfreien Seite. Was hat der Kunde getan?

Er folgte seinen Wünschen. Daraufhin tippte er etwas unter Betrag und etwas unter Rabatt ein und klickte auf die Schaltfläche zum Berechnen. Das angezeigte Ergebnis entsprach nicht seinen Wünschen, woraufhin einfach die Wunschvorstellung im Ergebnisfeld eingetippt wurde.

Schutz vor Ergebnismanipulation Bearbeiten

Das Ergebnisfeld muss also vor derartigen Manipulationen geschützt werden. Die entsprechende Methode findet sich in der API-Dokumentation und lautet setEditable( ...). Als Argument wird einfach true oder false (in diesem Fall also false) übergeben. Die Anweisung

ausgabeFeld.setEditable( false);

sollte also irgendwo in der Nähe einer Verwendung des betreffenden Okjekts untergebracht werden. Nah genug ist z.B. die Integration des Objektes in das Panel.