Die objektorientierte Programmierung erzeugt sog. Objekte aus Klassen. Dieser Vorgang wird "Instanzierung" genannt und ist dadurch für den Anfänger keinesfalls deutlicher geworden.
Um hier etwas Licht in die Sache zu bringen, wird der Instanzierungsvorgang umgekehrt; aus Objekten sollen erst einmal die Klassen hergeleitet werden.
Hier ist offensichtlich eine Ansammlung von Objekten georetrischer Art vorhanden. Eine Klassifizierung bedarf zunächst einmal einer Ordnung. Jedoch ist Ordnung ein relativer Begriff. Soll nun nach Größe geordnet werden oder ist es angebrachter nach Formen zu unterscheiden? Soll die Hierarchie von links nach rechts oder von oben nach unten gerichtet sein?
Am einfachsten ist es in diesem Fall, alle genannten Überlegungen gleichzeitig anzuwenden. Dabei muss aber peinlich genau darauf geachtet werden, die Grenzen nicht zu verletzen. Diese Grenzen sind hier durch die konvexe Hülle gegeben. Auch darf bei der Einführung einer Ordnungsrelation kein Element (Objekt) fehlen oder hinzugefügt werden.
Die jetzt eingeführte Ordnung der Objekte folgt in vertikaler Richtung den Formen und in horizontaler den Größen. Die vorhandenen Abgrenzungen seien durchgehende Linien. Die Objekte wurden innerhalb der umhüllenden Grenze in kleinere Ansammlungen zusammengefasst oder gekapselt. Es ergeben sich neun Teile.
Alle Objekte zwischen zwei durchgehenden Grenzlinien bilden wiederum abgeschlossene Teile der Ansammlung (die horizontalen Linien seien ebenfalls durchgehend, auch wenn diese Vorstellung etwas Abstraktionsvermögen voraussetzt) mit jeweils drei Teilen. So lassen sich die Objekte entweder in einer umhüllenden, formgebenden Ordnung oder in einer umhüllenden größenbegrenzenden Ordnung zusammenfassen.
Bis jetzt ist es gelungen, gleichartige Objekt in abgeschlossenen Bereichen unterzubringen. Jeder dieser Bereiche ist ein Element der Menge eben dieser Bereiche. Es liegt deshalb eine Menge vor, weil die einzelnen Elemente wohlunterscheidbar sind. Diese Unterteilung hat natürlich einen eigenen Begriff und wird als Faserung einer Menge bezeichnet. In jedem Fal ist jetzt alles Formbasierende ist in horizontalen Fasern, alles Größenbasierende in vertikalen Fasern untergebracht. So verläuft die Faser der großen Objekte ganz rechts von oben nach unten, die Faser der runden Objekte dagegen ganz unten von links nach rechts.
Die Elemente innerhalb einer Faser werden Zellen genannt. Jetzt besteht das nächste Problem darin, die Objekte innerhalb der Zellen zu umzugestalten, dass jede Zelle als Teilmenge der gesamten Objektmenge vorhanden ist. Hier kommt ein wesentlicher Gesichtspunkt der Mengentheorie ins Spiel - die Wohlunterscheidbarkeit. Eine Menge besteht nur aus unterscheidbaren Objekten. Es existiert keine Menge der Form M = {4,4}, denn die Elemente sind voneinander nicht zu unterscheiden. Jede Zelle hat aber mehr als ein Element in sich, was tun?
Nun sind Zahlen nichts anderes als Faktoren und damit prinzipell nichtssagend. Mit 15 kann niemand etwas anfangen; fünfzehn Was? Burger, Euros, Cents, Millimeter oder was? Von diesen nichtssagenden Gebilden sollte Abstand genommen werden. Die Elemente innerhalb der Fasern sollen nun in (Teil)mengen umgewandelt werden und enthalten damit nur noch ein einziges Objekt.
Jetzt repräsentiert jede Zelle die Klasse des Elements das sie enthält und kann beliebig viele Objekte dieser Elemente bereitstellen. Aber noch mehr wurde erreicht; jede Faser ist nun eine übergrdnete Klasse der jeweiligen Fasern. So ist die linke Faser die Menge aller Klassen kleiner Objekte un die unterste Faser die Klasse aller runden Objekte
Die jetzt vorhandene Menge ist einem Einkaufskatalog mit jeweils einem Abbild eines Produkts vergleichbar. Von jedem Element einer Zelle kann einegewisse Anzahl bestellt werden. Jeder Zelle wird so die faktorisiernde Anzahl einfach als Eigenschaft zugeordnet.
Zelle der kleinen Dreiecke: Anzahl 5; oder Zelle der großen Kreise: Anzahl 4.
Der letzte Schritt zur Klassifizierung besteht in der Konfektionierung. In Kaufhäusern werden oft T-Shirts in den Größen S, M und L angeboten. Diese amerikanischen Konfektionsgrößen (Small, Medium, Large) liegen auch hier vor. Jede horizontale Faser besitzt die gleiche Unterteilung seiner Zellen. Wer Analogien zur Schnäppchenjagd im Kaufhaus mag, kann die unterschiedlichen Formen durch T-, Polo- und Sweat-Shirts ersetzen. In jedem Fall kann die horizontale Unterteilung durch Einführung der Konfektionsgröße enthallen.
Jetzt ist die Klassifizierung der Anfangs vorhandenen Objekte abgeschlossen. Für Freunde des realen Bezugs kann die hier dargestellt Klassenmenge als Hersteller von Shirts angesehen werden. Es existieren drei Klassen, die jeweils eine Hemdensorte erzeugen können. Damit die Erzeugung funktioniert, muss in diesem Fall die Anzahl und Größe der gewünschten Objekte angegeben werden.
Eine Zahl repräsentiert gemeinhin einen Wert. Es existieren die unterschiedlichsten Möglichkeiten Zahlen in Werte umzuwandeln. So war die Zeichenfolge "IV" vor geraumer Zeit eine durchaus übliche Repräsentation des Wertes "vier". Bis heute hat sich nichts an den Unterscheidungszwang zwischen Zeichenfolge/Zahl/Wert geändert. So ist denn auch in Java dieses Problem vorhanden.
Wenn irgendwo im Quelltext die Zeichenfolge 123 auftaucht, vermutet der Compiler immer den Wert einer ganzen Zahl dahinter. Wenn jedoch "123" gefunden wird, steht für den Compiler fest, dass es sich um einen Text handelt. Nun ist es sinnvoll, den verwendeten Dingen einen Namen zu geben und diesen Namen statt Zeichenfolge/Wert/Text zu verwenden. Diese Namen werden unter dem Begriff Variablen zusammengefasst und spielen in der Programmierung die zentrale Rolle. Variablen informieren den Compiler über den Ort des Wertes (sie referenzieren einen Speicherbereich) und über den Aufbau des Wertes. Mag die Notwendigkeit einer Ortsangabe noch einzusehen sein, so treten doch bei "Art des Wertes" erste Verständnisprobleme auf.
Zunächst sollen die Begriffe "Zeichenfolge", "Zahl" und "Wert" getrennt werden. Diese Unterscheidung bereitet am Anfang die größten Probleme.
Eingaben des Anwenders sind Zeichenfolgen. Unabhängig davon, ob nun ausschließlich Ziffern oder Buchstaben eingetippt werden, es handelt sich immer nur um eine Aneinanderreihung von Zeichen.
Der Anwender gibt eine Folge von Ziffern ein, also eine Zeichenfolge. Das Programm (nicht der Compiler) muss diese Folge in eine Zahl umwandeln. Wie diese Aufgabe bewältigt wird, zeigt ein anschließender Abschnitt. Hier genügt es, die Notwendigkeit der Umwandlung zu erkennen. Der Compiler wandelt diese Zahl nun in einen Wert.
Mit Werten wird ein Anwender nicht konfrontiert. Werte sind systeminterne Bitfolgen, die nach bestimmten Regeln aufgebaut sind (IEEE 754 ist eine solche Regel). Auch Programmierer sind kaum mit diesen Interna konfrontiert, es sei denn es sollen schnelle Programme geschrieben werden. Zu diesem Zeitpunkt ist nur wichtig, dass der Computer nur mit Werten umgehen kann, während der Anwender und auch der Programmierer nur an den Ergebnissen interessiert ist. Ergebnisse müssen aufbereitet werden, damit sie verständlich sind. Für die erste Überprüfung werden sie meist wieder eine Zeichenfolge umgewandelt, damit die angezeigt werden können.
Der Umgang mit Zahlen auf einem Computer ist also eine Art Keislauf. Zunächst wird eine "Zeichenfolge" entgegengenommen und in eine Zahl umgewandelt. Der Compiler macht einen Wert daraus und eine Berechnung (z.B. diese Zahl +5) kann angewiesen werden. Der Wert wird nun dieser Berechnung unterzogen und liegt im systeminternen Format vor. Zur Überprüfung muss nun der Wert in eine Zahl und weiter in eine Zeichenfolge gewandelt werden. Letztere wird dann angezeigt.
Das Thema "Zahlen" ist derart wichtig, dass ein behutsames Herantasten an die Abläufe in Programmen sehr wichtig ist. Deshalb soll hier die Addition mit dem jetzt vorhandenen Wissen detailliert besprochen werden.
Eine Zeichenfolge der Form "26" wird unter dem Namen "Zahl1" geführt. Natürlich ist es keine Zahl, sondern ein Text (String). Eine weitere Zeichfolge "74" wird unter dem Namen "Zahl2" geführt. Wenn es sich um Zahlen handelte müsste die Addition die Zahl 100 ergeben. Im Quelltext könnte nun etwas wie
Zahl1 = 26
Zahl2
= 74
Zahl1 + Zahl2
stehen. Selbst wenn es sich um ausführbare Anweisungen handeln würde, wäre es sinnlos. Zwar stünde irgenwo im Speicher der Wert 100, aber er ist nicht erreichbar. Ihm fehlt eine Referenz. Sinnvoller, aber noch nicht ausführbar, ist folgende Schreibweise:
Zahl1 = 26
Zahl2
= 74
Ergebnis = Zahl1 + Zahl2
Das Programm kann nun über die Referenz "Ergebnis" auf den errechneten Wert zugreifen. Aus dem bisher Erarbeitetem kann eine erste Konsequenz gezogen werden.
Alle Werte müssen über eine Referenz verfügen.
Ziel dieser detaillierten Auseiandersetzung mit Zahlen und Werten ist eine in Java ausführbare Programmsequenz. Java verlangt genaue Informationen über die Art der zu verarbeitenden Werte. Was ist also unter dem Begriff "Art des Wertes" zu verstehen? Ganz einfach, es ist der Typ gemeint. Tatsächlich kommt die zweite Bezeichnung der Sache bereits etwas näher.
Oft werden Geschäftsleute in zwei Kategorien unterteilt,. Die Integeren , die lieber das Geschäft ablehnen, als unseriös zu wirken und die Realisten, die sich auch mal zu ihren eigenen Gunsten "verrechnen". Diese zwei Typen können mit viel Phantasie auf Werte übertragen werden. Die Integer-Werte und die Real-Werte verhalten sich recht unterschiedlich wenn es ins Detail geht, sonst aber durchaus gleich. Integer-Werte können bedingt geteilt werden. Der Integer-Wert "fünf" teilt sich in zwei gleiche Hälften der jeweiligen Werte "zwei". Der Real-Typ würde jeweils "2,5"-wertige Hälften zulassen. Allerdings kann es vorkommen, dass eine Hälfte "2,500000001" wert ist und die andere "2,4999999997". Trotzdem würde der Real-Typ die Addition dieser beiden unterschiedlichen Werte wieder zur "5" vereinen. Richtig unangenehm wird es allerdings bei Werten um 1. Ein Integer-Wert ergibt bei eine Teilung durch 2 den Wert 0. Ein Real-Wert den 0.5 (mit der bereits erwähnten Möglichkeit zur Ungenauigkeit). Eine Überprüfung durch Addition beider Hälften wird bei Integer-Typen das fehlerhafte Ergebnis 0 und bei Real-Typen das korrekte Ergebnis 1 liefern. Offensichtlich können diese diese beiden Grundtypen unterteilt werden in die Merksätze:
Integers
machen keine halben Sachen.
Reals
sind flexibel aber nicht absolut genau.
Um die Nomenklatur von Java bezüglich der Reals, sie werden als Floats typisiert, etwas zu verdeutliche, kann vielleicht folgende Vorstellung herangezogen werden:
Ein Getränk in einer Bar ist teuer, genau abgemessen aber es kann nicht gänzlich genossen werden. Ein paar Tropfen bleiben im Glas und entziehen sich dem Genuss. Beim nächsten Drink sollte das alte Glas benutzt werden, denn es wird um eben diese Tropfen voller sein. Der unweigerlich entstehende Fehler durch die Ungenauigkeit wurde halbiert.
Java hat also die Wertetypen Integer und Float. Damit aber noch nicht genug, denn es gibt auch hier noch Abstufungen. Je nach Bedarf können lange oder kurze Zahlen bearbeitet werden, je nach erforderlicher Genauigkeit. Eine komplette Liste aller Wertetypen ist im Abschnitt Variablen.
Zurück zum Aufbau einer lauffähigen Programmsequenz zur Addition zweier Zahlen. Die Werte verfügen bereits über Referenzen (Zahl1, Zahl2 und Ergebnis), es fehlten ihnen aber die Typen. Offensichtlich genügen Integer-Typen für alle Werte, denn es existieren keine Kommastellen und es soll auch nicht dividiert werden. Nun ist Java eine objektorientierte Programmiersprache und die Vermutung drängt sich auf, dass hinter all den Referenzen, Typen und Werten das eine oder andere Objekt versteckt ist. Nein, weit und breit kein Objekt. Noch nicht! Bei dem nun um die Typenangabe erweiterten Quellcode
int Zahl1 = 26; int Zahl2 = 74; int Ergebnis = Zahl1 + Zahl2; |
handelt es sich um eine ausführbare Sequenz. Allerdings muss diese erst übersetzt werden, damit der Computer sie versteht. Dieser Übersetzungsvorgang soll nun prinzipiell erörtert werden (die tatsächlichen Abläufe eines Compilers werden hier natürlich nicht besprochen).
Zunächst findet der Compiler die eine Typenbezeichnung und reserviert den benötigten Speicherplatz. Jetzt wird der Bezeichner (Name) "Zahl1" gefunden und mit dem Ort des reservierten Speicherplatzes in Verbindung gebracht. Der Wert wird über den Bezeichner referenziert. Das Gleichheitszeichen "=" wird erkannt, womit der Compiler angewiesen wird, alle bis zum Semikolon ";" folgenden Zeichen irgendwie als Zahl zu interpretieren und deren Wert dann an der referenzierten Stelle abzulegen. Dieses Vorhaben wird ihm zweifellos gelingen und er verfährt mit der nächsten Zeile wieder genauso. Nun die dritte Zeile. Zunächst findet der Compiler wieder eine Typen- und eine Referenzangabe. Hinter dem Gleichheitszeichen steht jetzt zwar wieder eine Zeichenfolge (Zahl1), aber sie lässt sich nicht in eine Zahl umwandeln. Vielleicht hat diese Zeichenfolge aber etwas mit Werten zu tun. Letztendlich entpuppt sie sich als Referenz und eine Überprüfung des Typs ergibt, dass es sich um einen brauchbaren (Integer) handelt. Das Pluszeichen wird erkannt und der Compiler wird versuchen die nächste Zeichenfolge wieder als Zahl zu interpretieren. War nichts, ist wieder eine Referenz. Typ überprüfen, Typ ok. und nun aus den Referenzen und dem Pluszeichen eine Zahl zusammenbauen. Über "Zahl1" referenzierten Wert holen und merken, über "Zahl2" referenzierten Wert holen und zum gemerkten addieren. Neuen Wert merken und unter der über "Ergebnis" referenzierten Speicherstelle ablegen.
Soweit die prinzipielle Vorgehensweise des Compilers. Offensichtlich ein sehr komplexes Programm, dessen Prinzipien aber durchaus bei der täglichen Programmierarbeit hilfreich sind. Der nächste Abschnitt zeigt warum.
Die Aufgabe ist einfach (gelogen). Einem Kunden soll auf seinem Rechnungsbetrag ein Rabatt gewährt werden. Der Rechnungsbetrag steht in einem Anzeigefeld (muss aber zu Testzwecken manuell eingegeben werden),. Darunter der prozentuale Rabatt, ebenfalls in einem Anzeigefeld. Ein weiteres Anzeigefeld soll den endgültigen Betrag enthalten.
Zugegeben, die Aufgabe ist etwas komplexerer Natur. Die Vorgehensweise ist jedoch klar:
Das JContentPane eines JFrames wird mit einem JPanel belegt. Letzteres erhält einen LayoutManager der Klasse GridLayout zur Steuerung der Constraints der JComponents. Dem JFrame wird ein interner, argumentativer Listener in Form eines Adapters hinzugefügt und sofort mir dem WindowClosingEvent-Handler belegt. Der erforderliche FocusListener der JComponents wird natürlich als separate Klasse realisiert und ist auch für das ExceptionHandling der Zahleneingabe verantwortlich. FocusRequest liegt selbstverständlich auf den JTextField des Rechnungsbetrags.
Scheinbar hat diese Aufgabe mehr mit der amerikanischen Verballhornung der englischen Sprache gemein als mit der Berechnung von Rabatten. Über diese wurde kein Wort verloren, was soll das? Irgendwo müssen die Eingaben erfolgen, da liegt es nahe, ein eigenes Fenster mit den entsprechenden Möglichkeiten auszustatten. Nichts anderes wurde beschrieben. Außerdem dient dieser Ausflug in die GUI-Erstellung (GUI=GraphicalUserInterface) dem Kennenlernen der vorhandenen Dokumentation. Jedenfalls wird von der GUI-Programmierung noch die Rede sein, jedoch werden komplexere Oberflächen immer mit entsprechenden Werkzeugen, sog. IDEs, erstellt (IDE=IntegratedDevelopingEnvironment).
Um alle eventuell vorhandenen Illusionen zu rauben: Java umfasst in der einfachsten Installation über 25000 Befehle. Ein Wortschatz also, den noch nicht einmal alle Menschen in ihrer Muttersprache besitzen. Es ist also unmöglich, den gesamten Umfang von Java jemals zu beherrschen. Um trotzdem Programme erstellen zu können, muss die Online-Dokumentation der APIs (API=ApplicationProgrammingInterface) bei jeder Programmierung vorhanden sein.
In der "Beschreibung der Vorgehensweise" sind alle Java-relevanten Begriffe hervorgehoben. Diese sind in der API-Dokumentation vorhanden. Allerdings ist das Entscheidende dort nicht zu finden, nämlich wie die Daten in das Programm kommen. Von "Anzeigefeldern" war ursprünglich die Rede. Dabei handelt es sich um JTextFields, die für die Interaktionen natürlich eigene Methoden besitzen. Wie, warum, wieso, weshalb spielt für die gestellte Aufgabe eigentlich keine Rolle, denn zunächst muss die Berechnung erfolgen. Berechnungen benötigen Werte. Werte basieren auf Daten. Daten müssen irgenwo herkommen. Aus den JTextFields. In diese gibt der Anwender seine Zeichenfolgen ein und genau diese gilt es entgegenzunehmen. Die Klasse JTextField stellt die erforderlichen Methoden natürlich bereit, wie in der API-Dokumentation ersichtlich. Hier ein weiterer "Merksatz" für das arbeiten mit Java:
Objekte übergeben Properties mit Methoden
der Form getDasDa().
Objekte
erhalten Properties mit Methoden der Form setDasDa().
Für die Objekte der Klasse JTextField gilt zunächst einmal, dass es sich bei dem angezeigten Text um ein Property handelt und deshalb sehr wahrscheinlich auch mit einer get-Methode ausgelesen werden kann. Tatsächlich bestätigt ein Blick in die API-Dokumentation die Vermutung durch die Existenz der Methode getText().
Die Ursprungsdaten sind also Zeichenfolgen. Leider können sie nicht, wie im vorangegangenen Abschnitt dem Compiler überantwortet werden, denn sie fallen ja während des laufenden Programms an. Zeichenfolgen sind Strings. Es ist also sinnvoll, Zeichenfolgen als "Textwerte" anzusehen und sie über Referenzen wie Werte zu behandeln. Die erste Zeichenfolge soll die Bezeichnung "betrag" erhalten und die zweite die Bezeichnung "rabatt". Die entsprechende Sequenz lautet:
String betragText = "1234.56"; String rabattText = "7"; |
Die Typenbezeichnung String ist nicht so einfach wie die bereits bekannten (int, float ...). Zunächst fällt auf, dass der erste Buchstabe großgeschrieben ist. Das hat seinen Grund! Bei String handelt es sich bereits um eine Klassenbezeichnung, wodurch die beiden Referenzen "betrag" und "rabatt" Objekte referenzieren. Die Klasse String wird sehr häufig eingesetzt, weshalb ein Blick in die API-Dokumentation durchaus lohnend ist.
Wie soll nun ohne Hilfe des Compilers aus einem String (ab jetzt für Text) eine Zahl werden? Die Klasse String besitzt keine Methoden, die irgendwie hilfreich erscheinen. Überlegungen zum Typ des gewünschten Wertes führen weiter. Beim Betrag können Nachkommastellen auftreten, weshalb der Typ des Wertes mindestens "float" sein muss. Ein Blick in die API-Dokumentation bringt scheinbar nichts, denn diese klein-geschriebenen Typenvereinbarungen stehen nicht drin. Aber da steht etwas über "Float". Wenn es eine Klasse Float gibt, was soll dann dieses kleine float?
Eine sehr wichtige Regel, der hilft "Anfängerfehler" zu vermeiden und viel Arbeit erspart:
Kleingeschriebene Typen
erzeugen Werte.
Großgeschriebene Typen sind
keine sondern sind Klassen und erzeugen Zahlen.
In der Java-Notation sieht diese Regel ungefähr so aus:
float
einWert = 123.45;
Float eineZahl
= new Float( "124.45");
Für das Beispiel werden zwei Zahlen benötigt, die beide vom Typ Float (NEIN!!! Objekte der Klasse Float) sind. Objekte brauchen ebenfalls eine Referenz die bisherige Sequenz kann erweitert werden.
String betragText = "1234.56"; String rabattText = "7"; Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); |
Nun ist es eigentlich nicht einzusehen, dass mit diesen Zahlen nicht gerechnet werden kann. Zahlen sind mathematische Objekte (es sind immer nur Elemente von bestimmten Zahlenmengen). Rechnen ist ein Vorgang, der sich nur innerhalb bestimmter Zahlenmengen betreiben lässt. Zur Erinnerung: Ganze Zahlen sind nur bedingt teilbar (2/3=0). Rechnen ist nur mit Werten möglich. Es gilt also jetzt, den Wert der Zahlen (Float-Objekte) zu ermitteln. Werte sind Eigenschaften von Zahlen, also sollte es eine get-Methode geben, die den Wert dieses Objektes übergibt. Das englische Wort für Wert lautet value und eine Suche in der API-Dokumentation unter der Klasse Float nach getValue() ergibt nichts. Offensichtlich taugen die Merksätze auch nichts.
Mit etwas mehr Geduld und viel mehr Nachdenken zeigt ein Blick in die bisher aufgebaute Sequenz aber einen Hinweis. Aus dem "rabattText" wird ein Float-Objekt, obwohl kein Dezimalpunkt vorhanden ist. Das legt den Schluss nahe, dass Float-Objekte auch mit ganzzahligen Werte (int) klarkommen. Eine Folgerung daraus wäre, dieses Zahl-Objekt kann auch ganzzahlige Werte liefern. Also nach einer Methode namens getFloatValue() suchen. Bringt auch nichts, denn hier ist man von der schönen get-set-Regel abgewichen (warum weiß wohl Keiner mehr). Der Name der Methode lautet floatValue(). Übrigens gibt es die anderen Wertetypen ebenfalls, sie lauten dann entsprechend intValue(), doubleValue() usw. Für jeden Werte-Typ existiert in Java das entsprechende Zahlen-Objekt. Jedes Zahlen-Objekt kann seinerseits jeden Werte-Typ liefern.
Die Sequenz kann um die Wertetypen erweitert werden.
String betragText = "1234.56"; String rabattText = "7"; Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); |
Weil es so einfacher ist, mit Faktoren statt Ausdrücken zu rechnen, soll der rabattWert gleich in einen rabattFaktor und das Produkt aus rabattFaktor und betragWert in betragDifferenz abgespeichert werden. Der Endbetrag kann ebenfalls eine Wertdefinition mit der Bezeichnung endBetrag erhalten Damit erweitert sich die Sequenz erneut.
String betragText = "1234.56"; String rabattText = "7"; Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; |
Es ist hohe Zeit, diese Sequenz in einem Programm auszuprobieren. Zunächst wird also ein "Pflichtenheft" erstellt, in dem die wesentlichen Merkmale des Programms enthalten sind. Im vorliegenden Fall wird wohl eine kleine Auflistung genügen.
Es
soll eine Applikation (Anwendung) erstellt werden.
Der Name soll
Rabatt lauten.
Die Ergebnisse werden auf dem SystemStream
ausgegeben.
Egal was in Java gemacht wird - eine Klasse wird benötigt. Klassen haben einen Namen. Der zweite Punkt im "Pflichtenheft" kann durch
class Rabatt
bereits als erledigt betrachtet werden.. Klassen haben einen Anfang und ein Ende, was dem Compiler unbedingt durch entsprechende Klammern mitzuteilen ist.
class Rabatt { } |
Applications haben eine koordinierende Methode, die tatsächlich alles erst "in Gang bringt". Normalerweise ist diese Methode sehr kurz, weil sie nur die wesentlichen Objekte (JFrame) instanziert und anzeigt. Für das aktuelle Beispiel wird diese Methode also ungewöhnlich lang ausfallen. Methoden haben auch einen Namen, in diesem Fall ist der Name jedoch vorgegeben. Der Name der koordinierenden Methode lautet bei allen Applications main. Wegen der immer noch vorhandenen Kommandozeile (bei Windows DOS-Ebene), hat diese Methode noch ein Argument, um etwaige Angaben vor dem Star aus der Kommandozeile zu übernehmen. Die Methode erweitert sich also um dieses Argument zu main( String[] args). Auch Methoden müssen, wie Klassen, mit diesen Anfangs- und Endeklammern ausgestattet werden. Weil diese Methode zur Klasse "Rabatt" gehört, muss sie unbedingt zwischen den Anfangs- und Endeklammern der Klassendefinition stehen. Also einfügen, womit sich etwas wie
class Rabatt { main( String[] args) { } } |
ergibt. Unter dem Dateinamen "Rabatt.java" (Java unterscheidet zwischen Groß- und Kleinschreibung) gespeichert, liegt der erste lauffähige Java-Quelltext bereit. Die Übersetzung dieser Datei würde tatsächlich eine lauffähige aber völlig sinnlose Klassendatei erzeugen. In jedem Fall ist dieser Aufbau für alle Java-Anwendungen gleich und sie unterscheiden sich nur durch den Klassennamen. Jedenfalls kann in diesen Rahmen die gesamte, bisher erstellte Sequenz eingefügt werden. Der Ort ist natürlich die Methode main.
class Rabatt { public static void main( String[] args) { String betragText = "1234.56"; String rabattText = "7"; Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; } } |
Es fehlt nur noch ein Punkt im "Pflichtenheft", die Ausgabe auf dem "SystemStream". Wo ist das? Besser: Was ist das? Jedes System hat sog. standardisierte Ein-Ausgabedateien. Insgesamt sind es drei.
standard_in:
Eingabedatei = Tastatur
standard_out: Ausgabedatei = Konsole =
Bildschirm
standard_err: Fehlermeldungen = Konsole = Bildschirm
Diese System-Dateien sind in Java unter dem Begriff System vorhanden (was es mit System genau auf sich hat, folgt sehr viel später) Ein Stream ist etwas in dem Daten von einem Ort zum anderen "fließen". Der "Ausgabestrom" des Systems wird die Daten logischerweise zu seiner Ausgabedatei fließen lassen. Er ist erreichbar unter der Bezeichnung out. Aus das System greift Java über die Klasse System zu, womit der Instanzenweg Systen.out bereits feststeht. Streams, in diesem Fall also das Objekt out, haben auch ihre Methoden. Eine Methode gibt eine Zeile aus und lautet (wohl aus historischen Gründen) println( ... ). Diese Methode gibt erwartet eine Zeichenkette in der Argumentliste und gibt dies aus. So würde System.out.println( betragText); den String (Zeichenkette) auf dem Bildschirm anzeigen.
Schon ganz gut, aber es sollen ja Werte ausgegeben werden. Werte sind eben keine Strings, wie verlangt. Also alle Werte wieder in Strings wandeln? Nein, es geht zum Glück einfacher. Der Compiler von Java hilft. Eine Anweisung wie System.out.println( betragWert) ergibt eine Fehlermeldung beim Übersetzen. Der Compiler findet keine Methode, die es gestattet Werte des Typs float auszugeben. Eine kleine Änderung überzeugt den Compiler aber davon, dass der Wert als Bestandteil eines Strings anzusehen ist - System.out.println( "Betrag "+betragWert). Der Compiler erkennt in der Argumentliste zunächst einen String und ist erst einmal zufrieden. Nun folgt ein Pluszeichen, was vom Compiler dahingehend interpretiert wird, dass an den bereits erkannten String noch etwas angehängt (concatenated) werden soll. Nun findet der Compiler einen float-Wert und ist so tolerant, diesen in einen String umzuwandeln und an den vorhandenen anzuhängen. Dem Compiler kommt es übrigens nur auf das Vorhandensein eines Strings in der Argumentliste an, was bedeutet, dass diese nicht unbedingt mit einem String beginnen muss.
Die Ausgabe für das Beispiel soll ungefähr folgendes Aussehen haben:
7% von 1234.56
sind: ...
Der Endbetrag ist: ...
Erst werden die feststehenden Strings aufgebaut und danach die Variablen eingesetzt. Angeführt wird jede Ausgabeanweisung von
System.out.println( ?1+"% von "+?2+" sind: "+?3); System.out.println( "Der Endbetrag ist: "+?4); |
Sieht gut aus, wären da nicht die Fragezeichen. Diese werden natürlich ersetzt durch die Referenzen (Variablen) der entsprechenden Werte. Der Ersatz von "?1" ist der Wert der Prozentangabe rabattWert. Der Ersatz von "?2" ist der Wert des Ausgangsbetrags betragWert. Bei "?3" geht es bereits an die selbst errechneten Wert, und der Ersatz ist betragDifferenz. Soweit die (plural von Ersatz?) Substitutionen der ersten Ausgabezeile.
System.out.println( rabattWert+"% von "+betragWert+" sind: "+betragDifferenz); |
In der zweiten Zeile ist nur "?4" mit dem endgültigen Ergebnis endBetrag zu substituieren, wodurch diese zu
System.out.println( "Der Endbetrag ist: "+endBetrag); |
Sollte es wahr sein? Ja! Das erste Java-Programm ist fertiggestellt (natürlich erst, wenn die beiden Ausgabezeilen in die bestehende Sequenz eingefügt wurden. So sieht es also aus.
class Rabatt { public static void main( String[] args) { String betragText = "1234.56"; String rabattText = "7"; Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; System.out.println( rabattWert+"% von "+betragWert+" sind: "+betragDifferenz); System.out.println( "Der Endbetrag ist: "+endBetrag); } } |
Das Ergebnis aller bisherigen Bemühungen ist mit
7.0% von 1234.56 sind: 86.419205 Der Endbetrag ist: 1148.1409 |
zwar korrekt, aber irgendwie "mickrig"? Irgenwie fehlt die grafische Umgebung und Möglichkeiten der Eingabe und Gestaltung und überhaupt ...
Die Aufgabe war ja auch etwas anderer Natur. Da war von JFrame und JComponents und weiteren völlig unbekannten Begriffen die Rede. Genau diese Dinge sind es aber, die Java für den Anwender ausmachen. Hier wird gestaltet und entworfen. Hier sind die kreativen Fähigkeiten gefragt. Leider erschließt sich dieses Gebiet auch erst mit viel Mühe, weshalb es bereits zu diesem frühen Zeitpunkt angegangen wird.
Ein Fenster ist in Java natürlich als Klasse vorhanden. Es enthält die gewünschten Komponenten der Anwendung und entfernt sich selbst mit allen Bestandteilen aus dem System, wenn es geschlossen wird. So einfach sollte es sein, so einfach ist es aber nicht.
Für den Aufbau einer Application mit Fenstern muss zunächst wieder der Rahmen für Applications herhalten. Natürlich erhält diese Klasse einen neuen Namen.
class MyFrame { public static void main( String[] args) { } } |
Nun muss ein Fenster her. Es ist jedoch ein besonderes Fenster, denn in ihm laufen alle sichtbaren Aktionen ab. Es ist also so etwas wie ein Rahmen um die beteiligten Komponenten. Die Klassenbezeichnung lautet daher auch Frame. Um gleich in den neuen Standard zu wechseln, wird statt Frame die Klasse JFrame verwendet (auf die Unterschiede wird noch eingegangen). Also wird jetzt in der Methode main ein Objekt der Klasse JFrame gebildet. Die Referenz soll die Bezeichnung frame tragen.
Bei JFrame handelt es sich um eine Klasse, die in Java nicht zu den allgemein verfügbaren zählt. Klassen werden in Java in sog. Packages unterteilt. Es macht einfach keinen Sinn, ein kleines Applet mit allen Fähigkeiten einer Datenbank, eines Servers und einer Tabellenkalkulation auszustatten, die niemals genutzt werden. Die Klasse JFrame ist im Package swing untergebracht., das seinerseits wieder im Package javax residiert. Wenn jetzt die Frage auftaucht, woher man das alles wissen und behalten soll, so lautet die Antwort: "überhaupt nicht!". Der ganze Pfad durch die Packages steht in der API-Dokumentation der Klasse ganz am Anfang. So steht denn auch javax.swing in der Dokumentation von JFrame ganz vorn. Vor jedem JFrame braucht also nur javax.swing, gefolgt von einem Punkt, zu stehen und es treten keine Probleme auf.
class MyFrame { public static void main( String[] args) { javax.swing.JFrame frame = new javax.swing.JFrame(); } } |
Der Anwender erwartet eine Titelzeile mit informativem Inhalt. Die Klasse JFrame hat hierfür eine Methode mit dem Namen setTitle( ...). Der zu übergebende Parameter ist ein String und wird vom Objekt als Eigenschaft übernommen. Mit der Methode setTitle( ...) wird also das Titel-Property des Objekts frame geändert. Ein sinnvoller Titel wäre "Rabattberechnung". Also rein damit in den Quelltext.
class MyFrame { public static void main( String[] args) { JFrame frame = new JFrame(); frame.setTitle( "Rabattberechnung"); } } |
Java ist sehr konsequent, bezüglich der Anzeige. Es existiert zwar jetzt ein Fenster, aber es wird nicht angezeigt. Das Fenster-Objekt frame hat die Eigenschaft unsichtbar zu sein. Besser formuliert: "frame hat die Eigenschaft nicht sichtbar zu sein". Dieser kleine, aber wichtige Unterschied in der Formulierung macht die Funktionsweise der folgenden Methode deutlich. Das Property visible (sichtbar) hat zwei Zustände, die das gesamte Fenster betreffen, visible und das Gegenteil davon. Das Gegenteil von visible ist natürlich nicht invisible, sondern einfach not visible. Variablen und damit eben auch Properties, die nur zwei Zustände annehmen können werden boolsche Variablen genannt. Sie haben, wie int- und float-Variablen Werte. Allerdings handelt es sich jetzt nicht mehr um numerische, sondern um boolsche Werte. Bezogen auf das Property sichtbar, bedeuten die Werte:
Fenster
wird angezeigt: Es ist wahr, dass es sichtbar ist, oder
sichtbar = wahr.
Fenster
wird nicht angezeigt: Es ist nicht wahr, dass es
sichtbar ist, oder sichtbar = falsch.
Das Property visible hat also entweder den Wert true oder den Wert false. Natürlich soll das Fenster frame angezeigt werden, weshalb die Methode zum setzen dieses Properties aufgerufen wird.
class MyFrame { public static void main( String[] args) { javax.swing.JFrame frame = new javax.swing.JFrame(); frame.setTitle( "Rabattberechnung"); frame.setVisible( true); } } |
Die Ausführung dieses Programms ist ernüchternd, oder einfach enttäuschend. Das Fenster ist an Winzigkeit kaum zu unterbieten und ein Klick auf den "Entfernen"-Knopf führt zu weiteren Irritationen. Los wird man dieses Programm nur mit "Gewalt", also über den Task-Manager. Es fehlt einfach an Komfort.
Zunächst sollte dieses Fenster mit ein paar Komponenten ausgestattet werden, damit es überhaupt einen Inhalt hat. Die ursprüngliche Aufgabe enthielt Hinweise auf Anzeigefelder. Diese Felder sollen Text enthalten können und editierbar sein, so die Vermutung. Fields (Felder) und Text können vielleicht zu einer Bezeichnung wie TextField führen, hinter der sich eine Klasse verbirgt. API-Doku durchforsten und die Klasse TextField suchen. Bitte nicht einsetzen, die Application arbeitet mit einem JFrame, also der erweiterten grafischen Oberfläche. Also nach JTextField suchen und genau diese Klasse einsetzen. Unter einsetzen ist gemeint, zunächst latente Objekte (Referenzen ohne Inhalt) in den Quelltext einzubinden. Zu diesem Zweck wird der bisherige Quelltext etwas übersichtlicher gestaltet.
class MyFrame { public static void main( String[] args) { javax.swing.JFrame frame = new javax.swing.JFrame(); javax.swing.JTextField betragFeld = null; javax.swing.JTextField rabattFeld = null; javax.swing.JTextField ausgabeFeld = null; frame.setTitle( "Rabattberechnung"); frame.setVisible( true); } } |
Eigentlich sollen diese drei Felder in das Fenster integriert werden. Allerdings gibt es da einige Hürden zu überwinden.
Wie groß sollen die Felder
sein?
Wie sollen die Felder positioniert werden?
Was soll mit
der Positionierung geschehen, wenn sich die Größe des
Fensters ändert?
Eine Möglichkeit wäre: Felderpositionen und -größen festlegen und Frame-Window fixieren. Diese Option bleibt immer, führt jedoch zu Anwenderprotesten. Eine andere Möglichkeit besteht darin, die Felder dynamisch anzupassen. Die Flexibilität ist maximal, führt jedoch zu eine extrem aufwendigen Programmierung. Ein Mittelweg muss gefunden werden und existiert in der bei Java in Form sog. LayoutManager.
Objekte dieser Klassen können auf jeden Container angewendet werden, somit auch auf das Objekt frame. Damit wäre jedoch die Einschränkung verbunden, das Layout an ein bestehendes Fenster zu binden für Java-Programmierer eine geradezu diktatorische Einschränkung. Ein eigener Container wird mit eigenen Komponenten gefüllt, ein eigener LayoutManager wird eingesetzt und der fertige Container in das Fenster integriert.
Container sind in ihrer Instanz als Panel sehr flexibel, wie es z.B. Applet zeigt, das eine Instanz von Panel ist. Klar, es muss hier ein JPanel sein, denn auf swing basiert diese Anwendung. Der Quelltext wird nun um ein JPanel-Objekt erweitert und mit einem BorderLayout-Manager ausgestattet.
class MyFrame { public static void main( String[] args) { javax.swing.JFrame frame = new javax.swing.JFrame(); javax.swing.JPanel panel = new javax.swing.JPanel(); javax.swing.JTextField betragFeld = null; javax.swing.JTextField rabattFeld = null; javax.swing.JTextField ausgabeFeld = null; panel.setLayout( new BorderLayout()); frame.setTitle( "Rabattberechnung"); frame.setVisible( true); } } |
Diesmal wurde ein neues Objekt einfach in die Argumentliste einer Methode "hineininstanziert", ohne erst eine Referenz zu definieren. Diese Vorgehensweise bietet sich immer dann an, wenn Properties Objektreferenzen sind. Der Layoutmanager ist ein Property des Containerobjekts panel. Weil die ursprüngliche Eigenschaft des Containers (wahrscheinlich) nicht ein BorderLayout war, wird es einfach neu definiert.
Jetzt müssen die Textfelder mit real existierenden Referenzen ausgestattet werden, also Referenzen auf JTextField-Instanzen. Die Länge der Felder sollte gleich sein und damit auch bei größeren Beträgen die Übersicht bestehen bleibt, wird eine Länge von 7 bei der Instanzierung mit angegeben. Die ursprünglichen null-Referenzen werden einfach ersetzt.
class MyFrame { public static void main( String[] args) { javax.swing.JFrame frame = new javax.swing.JFrame(); javax.swing.JPanel panel = new javax.swing.JPanel(); javax.swing.JTextField betragFeld = new javax.swing.JTextField( 7); javax.swing.JTextField rabattFeld = new javax.swing.JTextField( 7); javax.swing.JTextField ausgabeFeld = new javax.swing.JTextField( 7); panel.setLayout( new java.awt.BorderLayout()); frame.setTitle( "Rabattberechnung"); frame.setVisible( true); } } |
Border bedeutet bekanntlich Grenze und diese sind nicht an Händigkeiten (links, rechts) sondern an Himmelsrichtungen (West, Ost) orientiert. Ein BorderLayout stellt allerdings nur vier Grenzangaben (NORTH, SOUTH, WEST, EAST) bereit und eine Angabe für das eingegrenzte Gebiet (CENTER). Mit diesen Orientierungen können die zu integrierenden Komonenten nun ausgestattet werden.
Eine Komponente wird in einen Container mit der Containermethode add( ...) integriert. Dabei ist die Komponente ein Argument und die Orientierung der Komponente ein weiteres. Die Orientierung ist aber ein Property des Layoutmanagers der ohne Referenz im main-Programm einfach an den Container delegiert wurde. Wie also an die Himmelsrichtungen herankommen? Hier kommt eine Besonderheit zutage, die statischen Variablen. Diese Variablen sind zwar Bestandteil eines Objekts, werden aber nur einmal erzeugt. Dadurch wird eine Referenzierung über den Klassennamen möglich. Abschließend wird das Panel einfach zum zentralen Inhaltscontainer des Fensters erklärt. Das Fenster muss sich vor der Anzeige noch über die "bevorzugten" Ausmaße seiner Componenten informieren, um dann seinerseits eine entsprechende Größe anzunehmen. auch hierfür existiert eine Methode, die den Namen pack() trägt. Sie wird normalerweise unmittelbar vor der Anzeige aufgerufen.
class MyFrame { public static void main( String[] args) { javax.swing.JFrame frame = new javax.swing.JFrame(); javax.swing.JPanel panel = new javax.swing.JPanel(); javax.swing.JTextField betragFeld = new javax.swing.JTextField( 7); javax.swing.JTextField rabattFeld = new javax.swing.JTextField( 7); javax.swing.JTextField ausgabeFeld = new javax.swing.JTextField( 7); panel.setLayout( new java.awt.BorderLayout()); panel.add( betragFeld, java.awt.BorderLayout.NORTH); panel.add( rabattFeld, java.awt.BorderLayout.CENTER); panel.add( ausgabeFeld, java.awt.BorderLayout.SOUTH); frame.setTitle( "Rabattberechnung"); frame.setContentPane( panel); frame.pack(); frame.setVisible( true); } } |
Das Programm funktioniert. Leider hat es noch einen kleinen Nachteil, man wird es nicht wieder los. Zwar verschwindet es aus der Anzeige, jedoch nicht aus dem System. Wenn es aus einem anderen Programm heraus gestartet wurde, erscheint es nicht einmal in der Task-Leiste, was zu unangenehmen Folgen führen kann.
Normalerweise verabschiedet sich eine fensterbasierende Anwendung, wenn das Fenster geschlossen wird. Ein Klick auf die entsprechende Schaltfläche und die Anwendung ist weg. Klicks werden in Java als Ereignisse (events) geführt. Events sind in der Vorstellungswelt der herkömmlicher Programmierung Ausnahmen. Die lenken die Aufmerksamkeit auf sich, jedoch ist das nicht immer erwünscht. In Java wird ein Event als "dazwischenreden" eingestuft, womit die Möglichkeit besteht, zuzuhören oder nicht. Nun ist Zuhören nicht jedermanns Sache, weshalb in Java eine spezielle Klasse existiert, deren Objekte nichts anderes tun als zuzuhören. Damit diese nicht völlig überfordert sind, hat praktisch jedes "Dazwischenredner" einen eigenen spezialisierten Zuhörer. Es ist nun Aufgabe der Programmierung, Zuhörer dabeizuhaben oder nicht. Bezogen auf den Klick der "Fenster-schließen"-Schaltfläche, bleibt dieser bisher ein "ungehörter Rufer in der Wüste".
Die "Window-close"-Schaltfläche ist zweifellos Bestandteil des Fensters. Es liegt demnach nahe, das Fenster selbst mit einem Listener (Zuhörer) auszustatten. Die Methode Ausstatten kann auch als hinzufügen interpretiert werden, was die fragmentarische Methodenbezeichnung add... nahelegt. Die Methode addWindowListener(...) ist denn auch die richtige, wie es die API-Dokumentation zeigt. Was soll der Listener tun, wenn er den Ruf nach "Feierabend" von der Schaltfläche vernimmt? Doch nur das Programm zu beenden und das System zu verlassen. Alle anderen Rufe sollen ungehört verhallen.
Wird ein Listener verlangt, der nur eine begrenzte Anzahl von Events (in diesem Fall nur ein einziges) verarbeiten soll, so ist ein Adapter erforderlich. Dieser gestattet den Aufbau funktionsfähiger Listener ohne alle im Interface vorgeschriebenen Methoden zu definieren (Interfaces werden noch detailliert besprochen). Der Name des Adapters für einen WindowListener lautet WindowsAdapter. Das betroffene Fensterobjekt frame erhält also über
frame.addWindowsListener( new WindowsAdapter () ...
Einen Listener für spezielle Events des Klasse WindowEvent. Nur das Event WINDOW_CLOSING ist von Belang, denn jetzt soll das Programm beendet und das System verlassen werden. Das Programm wird von Java automatisch beendet, wenn der Befehl System.exit( ...) ausgeführt wird. Es genügt in diesem Fall also, nur System.exit ( 0) aufzurufen. Im WindowListener ist dafür eine eigene Methode vorgesehen, die nun noch geschrieben werden muss, denn der WindowListener ist ein Interface. Das reale Objekt wird ja erst durch die Instanzierung über den WindowAdapter erzeugt. Irgendwo in der Klassendefinition des neuen WindowAdapters muss diese Methode angesiedelt werden. Am einfachsten werden Probleme dadurch gelöst, die erforderlichen Dinge immer erst dann bereit zu haben, wenn sie benötigt werden. Hier also unmittelbar nach dem Befehl eine neue Instanz des WindowAdapters zu bilden.
frame.addWindowsListener // WindowListener hinzufügen ( // Argument: WindowsListener new WindowAdapter() { // Instanzierung der Klasse public void windowClosing( WindowEvent e) { // Methode definieren System.exit( 0); // Methodenfunktionalität } // Ende Methodendefinition } // Ende Klassendefinition ); // Ende Argumentliste |
In Java ist also die Instanzierung von Objekten durch unmittelbar anschließende Klassendefinition sogar innerhalb von Argumentlisten möglich. Diese Vorgehensweise ist normalerweise nicht üblich, wird aber für einfache Testzwecke oft eingesetzt. In der Praxis ist eigentlich immer eine separate Klasse vorhanden.
Dieses Fragment kann nun in den Quelltext eingefügt werden und es ergibt sich ein erstes einsetzbares Programm mit GUI. Natürlich noch funktionalen Inhalt. Außerdem kann ein Anwender bisher nicht einmal ahnen was die Felder bedeuten, aber diese "Nebensächlichkeiten" werden nach und nach beseitigt.
class MyFrame { public static void main( String[] args) { javax.swing.JFrame frame = new javax.swing.JFrame(); javax.swing.JPanel panel = new javax.swing.JPanel(); javax.swing.JTextField betragFeld = new javax.swing.JTextField( 7); javax.swing.JTextField rabattFeld = new javax.swing.JTextField( 7); javax.swing.JTextField ausgabeFeld = new javax.swing.JTextField( 7); frame.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent e) { System.exit( 0); } } ); panel.setLayout( new java.awt.BorderLayout()); panel.add( betragFeld, java.awt.BorderLayout.NORTH); panel.add( rabattFeld, java.awt.BorderLayout.CENTER); panel.add( ausgabeFeld, java.awt.BorderLayout.SOUTH); frame.setTitle( "Rabattberechnung"); frame.setContentPane( panel); frame.pack(); frame.setVisible( true); } } |
Endlich kann auch die eigentliche Aufgabe dunkle Fetzen der Erinnerung an prozentuale Berechnungen in die Anwendung einbezogen werden. Praktisch kann die gesamte funktionale Sequenz des Programms "Rabatt" übernommen werden, wobei die Zeilen der Anzeige einfach "auskommentiert" werden. Diese Anzeige verlangte als Argument einen String und genau diese Klasse ist ja wohl für JTextFields geradezu prädestiniert. Ohne viel un der API-Dokumentation zu suchen wird eine Methode getText(...) bei den Objekten der JTextFields als vorhanden angenommen und belegt.
class MyFrame { public static void main( String[] args) { javax.swing.JFrame frame = new javax.swing.JFrame(); javax.swing.JPanel panel = new javax.swing.JPanel(); javax.swing.JTextField betragFeld = new javax.swing.JTextField( 7); javax.swing.JTextField rabattFeld = new javax.swing.JTextField( 7); javax.swing.JTextField ausgabeFeld = new javax.swing.JTextField( 7); String betragText = "1234.56"; String rabattText = "7"; Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; /* System.out.println( rabattWert+"% von "+betragWert+" sind: "+betragDifferenz); System.out.println( "Der Endbetrag ist: "+endBetrag); */ betragFeld.setText( betragText); rabattFeld.setText( rabattText); ausgabeFeld.setText( ""+endBetrag); frame.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent e) { System.exit( 0); } } ); panel.setLayout( new java.awt.BorderLayout()); panel.add( betragFeld, java.awt.BorderLayout.NORTH); panel.add( rabattFeld, java.awt.BorderLayout.CENTER); panel.add( ausgabeFeld, java.awt.BorderLayout.SOUTH); frame.setTitle( "Rabattberechnung"); frame.setContentPane( panel); frame.pack(); frame.setVisible( true); } } |
Auch das funktioniert wunderbar. Nachteilig ist jetzt eigentlich nur noch, dass sich die Anwendung von Eingaben des Anwenders völlig unbeeindruckt zeigt. Überhaupt wird die ganze Sache etwas unübersichtlich, denn hier setzt sich eine einzige Methode mit allen benötigten Objekten aller Klassen auseinander und hat auch noch mehr als 20 Zeilen. Die Einstellung zu alten Adelsgeschlechtern ist zweifellos geteilt, trotzdem war es einer derer von Hohenzollern, mit der Erkenntnis:
Teile und herrsche!
Beherrschung meint hier, die Kontrolle über alle beteiligten Komponenten der gesamten Aufgabestellung zu erhalten. Ferner muss geklärt werden, ob Methoden oder einzelne Objekte oder eine Kombination aus diesen zum Ziel führt. Zunächst also erst einmal: "Woraus besteht das Ganze?"
Fenster und Panel lautet die Antwort. Wenn derartige Konstellationen vorliegen, in Java ist das fast immer der Fall, sollten eigene Klassen für jeden Part definiert werden. Für das Fenster (frame) ist die Aufgabe sehr einfach, denn es muss nur ein Panel angezeigt werden. Die neue Klasse erhält den etwas hochtrabenden Namen "MainFrame.java". Damit nicht wieder bei jedem Objekt der gesamte Klassenpfad (Weg durch die Instanzen) geschrieben werden muss, sind in den ersten beiden Zeilen des Quelltextes einfach "import"-Anweisungen vorhanden, die den Compiler über die entsprechenden Wege informieren.
import javax.swing.*; import java.awt.event.*; class MainFrame { public static void main( String[] args) { JFrame frame = new JFrame(); frame.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent e) { System.exit( 0); } } ); frame.setTitle( "Rabattberechnung"); // frame.setContentPane( panel); frame.pack(); frame.setVisible( true); } } |
Abgesehen vom WindowAdapter und einer "auskommentierten" setContentPane-Anweisung entspricht dieser Quelltext dem ersten Entwurf eines Fensters. Das Panel mit den TextFields und die Berechnungen wurden entfernt, denn all das hat mit dem Fenster nichts zu tun. Es könnte ja sein, dass der Kunde sich entschließt statt einer Application ein Applet einsetzen zu wollen. In diesem Fall muss das Panel dann eben in ein Applet. Die Funktionalität bleibt davon unberührt.
Eine Klasse für ein spezielles Panel (es ist ja für die Aufgabe der Rabattberechnung spezialisiert) sollte natürlich alle Möglichkeiten bieten, die ein "normales" Panel auch hat, aber eben noch diese jene Erweiterung. Am einfachsten wird die bestehende Klasse JPanel einfach durch die eigene Klassendefinition erweitert (extended). Der Name soll "RabattPanel" lauten, womit bereits die ersten Zeilen feststehen.
import javax.swing.*; class RabattPanel extends JPanel { } |
Objekte dieser Klasse sind nicht eigenständig, weshalb sie keine main-Methode besitzen. Natürlich kann eine solche hinzugefügt werden, sie muss dann aber ein Fenster bereitstellen, in welchem das RabattPanel-Objekt anzuzeigen ist. Diese Klassendefinition erhält jedenfalls kein main().
Objekte oder Instanzen werden über einen Constructor erzeugt, der als Methode in die Klassendefinition integriert werden sollte. Für das aktuelle Beispiel ist eine entsprechende Methode sogar zwingend, denn wie sollten die TextFields sonst in das Panel gelangen. Die Constructor-Methode ist die einzige, die nichts zurückzugeben scheint, denn sie darf keine Typen- oder Klassenangabe besitzen. In Wirklichkeit gibt diese Methode sehr wohl etwas zurück, nämlich eine Referenz auf das erzeugte (constructed) Objekt selbst. Diese totale Selbstbezüglichkeit drückt sich denn auch im Namen der Methode aus, sie entspricht der Klassenbezeichnung (hier also RabattPanel).
import javax.swing.*; class RabattPanel extends JPanel { RabattPanel() { } } |
In dieser Methode werden also die speziellen Eigenschaften der erweiterten Klasse festgelegt oder vorbereitet. Die gesamte Sequenz aus dem ursprünglichen Programm "MyFrame", die für den Aufbau des Panels verantwortlich war, kann hier hineinkopiert werden.
import javax.swing.*; class RabattPanel extends JPanel { RabattPanel() { JTextField betragFeld = new JTextField( 7); JTextField rabattFeld = new JTextField( 7); JTextField ausgabeFeld = new JTextField( 7); String betragText = "1234.56"; String rabattText = "7"; Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; betragFeld.setText( betragText); rabattFeld.setText( rabattText); ausgabeFeld.setText( ""+endBetrag); panel.setLayout( new BorderLayout()); panel.add( betragFeld, BorderLayout.NORTH); panel.add( rabattFeld, BorderLayout.CENTER); panel.add( ausgabeFeld, BorderLayout.SOUTH); } } |
Eine kleine Änderung ist noch nötig. Die TextFields und der LayoutManager beziehen sich auf eine Referenz zu einer JPanel-Klasse. Diese Referenz existiert in der Klassedefinition überhaupt nicht, denn das Ganze ist ja selbst ein JPanel mit eben diesen speziellen Erweiterungen. Die gekennzeichneten Referenzangaben können nun einfach weggelassen werden, denn der Compiler bewegt sich ja im Umfeld eines Panels und berücksichtigt diese Tatsache automatisch. Besserer Programmierstil ist es jedoch, die Form der Selbstreferenzierung auch im Quelltext zu berücksichtigen. Wenn also eine Referenz auf diese Klasseninstanz gemeint ist, sollte eben this auch geschrieben werden. Die entsprechenden Zeilen lauten also:
this.setLayout( new BorderLayout()); this.add( betragFeld, BorderLayout.NORTH); this.add( rabattFeld, BorderLayout.CENTER); this.add( ausgabeFeld, BorderLayout.SOUTH); |
Durch Aufruf des Constuctor-Methode, mit vorangestelltem new, wird ein Objekt der Klasse "RabattPanel" erzeugt. Die im Quelltext von "MainFrame" stillgelegte Zeile
// frame.setContentPane( panel);
legt ein JPanel als "Inhaltsbereich" des Fensters fest. Das ist nun auch mit Objekten der Klasse "RabattPanel" möglich, denn sie erweitern die Klasse JPanel und sind damit ebenfalls eine Instanz dieser Klasse. Offensichtlich genügt es, diese Zeile in "MainFrame" zu ändern, um die ursprüngliche Funktionalität zu erhalten.
frame.setContentPane( new RabattPanel());
Das Fenster erhält also einfach eine Referenz auf das Objekt RabattPanel und verhält sich ansonsten genau wie vorher völlig passiv. Die Teilung scheint vollzogen, aber es gibt noch eine Menge in der neuen Klasse (RabattPanel) zu tun.
Das RabattPanel hat verbirgt seine Eigenschaften (Properties) vor anderen Programmen. Dabei könnten die TextFields doch auch von anderen genutzt werden, was zweifellos eine Bereicherung sein könnte. Vorstellbar ist eine Änderung des feststehenden Rechnungsbetrags, denn nach kurzer Einsatzdauer wird wahrscheinlich der Wunsch keimen, nicht nur Rechnungen über 1234.56 mit satten 7% zu rabattieren. Andererseits ist es bestimmt unerwünscht, wenn diese Felder unkontrolliert von anderen Programmen verändert werden können. Eigentlich ist es ja private Angelegenheit des Panels, diese TextFields mit Form und Farbe zu versehen; bei den Inhalten könnten jedoch Zugeständnisse gemacht werden.
Die JTextField-Objekte werden also jetzt der Klasse zur Verfügung gestellt. Sie werden einfach aus der Constructor-Methode entfernt und unmittelbar hinter die Definitionseinleitung (in den Definitionsblock) angesiedelt. Um dem Wunsch nach Privatsphäre Ausdruck zu verleihen, erhalten die Referenzierungen einleitend den Zusatz private. Dieser Zusatz definiert die "Sichtbarkeit" (view) der Variablen gegenüber anderen Objekten. Der Zusatz private gewährleistet völlige "Unsichtbarkeit" außerhalb der aktuellen Klassendefinition. Damit bleiben die Variablen (Referenzen auf die JTextField-Objekte) natürlich auch weiterhin für die Constructor-Methode sichtbar. Um den Text nicht mit ständig neuen Listings aufzublähen, wird hier einfach ein Link auf den entsprechenden Quelltext in der Datei "RabattPanel1.java" gesetzt.
Um nun mit der "Außenwelt" in Verbindung zu treten, werden Methoden benötigt, die es gestatten den Inhalt der TextField-Objekte zu ändern. In Anlehnung an die Nomenklatur in Java also set-Methoden. Die Namen werden mit setBetrag(...) und setRabatt(...) festgelegt. Die Argumente dieser Methoden sin gewiss keine Strings, sondern Wert vom primitive-Typ float. Wenn diese Methoden aufgerufen werden, nehmen sie also einen float-Wert entgegen, wandelt ihn in einen String und übergeben diesen String an das entsprechende JTextField-Objekt mit der bekannten set-Methode des Objekts.
void setBetrag( float value) { betragFeld.setText( ""+value); } void setRabatt( float value) { rabattFeld.setText( ""+value); } |
Beide Mathoden bedienen sich des "Tricks", einen Text aus Werten über den Compiler erzeugen zu lassen. Diese beiden Methoden sind nicht ohne Konsequenzen auf den Rest der Klasse. Denn unabhängig davon, ob diese Methoden vorhanden sind oder nicht, Das Ergebnis ist immer noch das gleiche, denn alle Angaben werden in der Constructor-Methode gemacht. Der nächste Schritt wird nun die Isolation des Berechnungsvorganges sein, mit dem Ziel, die Constructor-Methode auf die Instanzierung einer JPanel-Instanz zu beschränken. Sinnvoll ist hier also nur die Anwendung eines Layoutmanagers auf die JTextField-Objekte.
Eine Methode zur Berechnung arbeitet mit Werten. Das Ergebnis ist wieder ein Wert, weshalb diese Methode in jedem Fall einen float-Wert zurückgibt (return). Der Ansatz ist also gemacht, der Inhalt ergibt durch die "Verschiebung" der Berechnungssequenz aus der Constructor-Methode in die Berechnungsmethode.
float calculate() { String betragText = "1234.56"; String rabattText = "7"; Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; return endBetrag; } |
Nun müssen statt der vorgegebenen Strings zunächst die Inhalte der JTextField-Objekte betragFeld und rabattFeld benutzt werden. Betroffen sind davon die String-Variablen betragText und rabattText.
float calculate() { String betragText = betragFeld.getText(); String rabattText = rabattFeld.getText(); Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; return endBetrag; } |
Weil immer noch die Testphase des RabattPanels anliegt, sollten die neu hinzugekommenen Methoden auch nur in der aktuellen Klassendefinition getestet werden. Also müssen die Methodenaufrufe innerhalb der Constructor-Methode erfolgen.
RabattPanel2() { setBetrag( (float)1234.56); setRabatt( (float)7); ausgabeFeld.setText( ""+calculate()); this.setLayout( new BorderLayout()); this.add( betragFeld, BorderLayout.NORTH); this.add( rabattFeld, BorderLayout.CENTER); this.add( ausgabeFeld, BorderLayout.SOUTH); } |
Auffällig ist die explizite Angabe des Typs float vor den Werten innerhalb der Argumentlisten von setBetrag und serRabatt. Der Compiler geht bei jeder Zahleneingabe mit Dezimalpunkt von der maximal verfügbaren Genauigkeit aus. Diese wird über den primitive-Typ double erreicht. Weil aber eine Beschränkung auf float zu Unstimmigkeiten mit der angenommenen Genauigkeit führen würde, muss dieser Verzicht auf Genauigkeit von Programmierer explizit angegeben werden. Dieser Vorgang wird typecasting genannt und funktioniert auch mit Objekten. So könnte ein Objekt der Klasse RabattPanel ohne Probleme einer Referenzvariablen für JPanel-Klassen zugewiesen werden. Auch eine Wandlung in die reine awt-Klasse Container ist mit (Container)panel möglich. Wichtig ist nur, dass der Instanzenweg eingehalten wird. Eine Versuch das RabattPanel über (Float)panel in eine Number-Klasse zu wandeln wird scheitern, obwohl beide von der Klasse Object abgeleitet wurden sind die Wege durch die Instanzen verschieden. Vom typecasting wird in jedem Fall noch genauer die Rede sein. In jedem Fall ist jetzt eine neue Version des RabattPanels fertig und liegt in Form der Datei "RabattPanel2.java" vor.
In der Methode calculate wimmelt es von Hilfsvariablen, die zwar während der Entwurfsphase recht hilfreich waren, jetzt die Übersichtlichkeit beeinträchtigen. Die nächste und wohl die wichtigste Aufgabe besteht nun darin, diese Variablen nach und nach zu ersetzen oder zu substituieren. Bei komplexeren Programmen grenzt diese Arbeit an Strafarbeit, denn die zu substituierenden Variablen sind kaum zu finden. oft ist das ganze System nicht mehr lauffähig, weil versehentlich lokale Variablen gleichen Namens entfernt und durch globale Substitutionen ersetzt wurden. Eine zwar schreibintensive, dafür aber sichere Methode ist das "auskommentieren" der von den Substitutionen betroffenen Zeilen.
Im folgenden Beispiel sollen die rot gekennzeichneten Referenzen in den Argumentlisten der Constructoren für die Zahlen betragZahl und rabattZahl durch die blau gekennzeichneten Methodenaufrufe substituiert werden.
float calculate() { /* String betragText = betragFeld.getText(); String rabattText = rabattFeld.getText(); Float betragZahl = new Float( betragText); Float rabattZahl = new Float( rabattText); */ Float betragZahl = new Float( betragFeld.getText()); Float rabattZahl = new Float( rabattFeld.getText()); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; return endBetrag; } |
In den sich ergebenden neuen Zeilen sind die Substitutionen grün hervorgehoben. Dieser Zustand des Quelltextes ist syntaktisch korrekt und kann übersetzt und ausprobiert werden. War der Probelauf erfolgreich, kann der Quelltext um den "auskommentierten" Part bereinigt werden.
float calculate() { Float betragZahl = new Float( betragFeld.getText()); Float rabattZahl = new Float( rabattFeld.getText()); float betragWert = betragZahl.floatValue(); float rabattWert = rabattZahl.floatValue(); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; return endBetrag; } |
Eine oft vernachlässigte Optimierung von Programmen ist die mehrfache Instanzierung, obwohl die Kapselung in einer einzigen Methode den gleichen Zweck erfüllt. Hier werden für die Bereitstellung zweier Werte auch zwei Zahlenobjekte (Float) erzeugt. Um aus einem String einen Wert zu extrahieren, kann eine Methode viel effizienter eingesetzt werden. Eventuell autretende Fehler bei der Wandlung von Strings in Werte können hier abgefangen und eventuell korrigiert werden.
private float valueOf( String text) { Float zahl = new Float( text); return zahl.floatValue(); } |
Die Berechnung wird zur Privatangelegenheit des RabattPanels erklärt. und wieder sind zwei Zeilen in der Methode clculate überflüssig geworden. Die gesamten Änderungen sind in "RabattPanel3.java" ersichtlich
float calculate() { float betragWert = valueOf( betragFeld.getText()); float rabattWert = valueOf( rabattFeld.getText()); float rabattFaktor = rabattWert / 100; float betragDifferenz = betragWert * rabattFaktor; float endBetrag = betragWert - betragDifferenz; return endBetrag; } |
Jetzt wird einmal versucht, die Substitutionen nach mathematischen Gesichtspunkten anzugehen. Die Variable betragWert kommt in der Berechnung von endBetrag zweimal vor. Einmal direkt und einmal über die Variable betragDifferenz, die in ihrer Berechnung betragWert enthält. Die Subtraktion der Variablen betragDifferenz wird also durch ihre eigene Berechnungsvorschrift ersetzt.
float endBetrag = betragWert - betragWert * rabattFaktor;
Damit ist die Zeile mit betragDifferenz sinnlos und kann entfernt werden. In der neuen Zeile für endBetrag ist nun betragWert tatsächlich zweimal vorhanden, was den Verdacht auf weiter Optimierung nahelegt. Zur Gewissheit wird diese Annahme durch einfaches Ausklammern von betragWert, womit sich
float endBetrag = betragWert * (1 - rabattFaktor);
ergibt. Nur die Zeile mit dem rabattFaktor ist noch ein Ärgernis. Nicht weil sie Speicherplatz belegt, sondern weil die Namensgebung in diesem Zusammenhang völliger "Blödsinn" ist. Niemals wird ein Faktor nur subtrahiert, schon gar nicht von 1. Also "wegsubstiuieren"!
float endBetrag = betragWert * (1 - rabattWert / 100);
Wieder eine Zeile eingespart und auch noch mathematische Korrektheit berücksichtigt. Aber wozu der Umweg über eine Variable, deren einziger Zweck darin besteht als Rückgabewert zu fungieren um sich danach sofort wieder aus dem Speicher zu entfernen. Letzteres ist übrigens das Schicksal aller lokalen Variablen. Der legendäre "Chuck Moore" (Erfinder der Programmiersprache FORTH und damit Urvater von Java) prägte die Regel:
Speichere nicht, was du auch berechnen kannst.
Hier bedeutet dieser Satz einfach die Eliminierung der Variablen endBetrag und Umformulierung der return-Anweisung zu
return betragWert * (1 - rabattWert / 100);
Damit nimmt die Methode calculate beinahe lächerliche Ausmaße an. Die beiden verbliebenen lokalen Variablen können ebenfalls ersetzt werden, aber die Übersichtlichkeit der Berechnung ginge verloren.
float calculate() { float betragWert = valueOf( betragFeld.getText()); float rabattWert = valueOf( rabattFeld.getText()); return betragWert * (1 - rabattWert / 100); } |
Prinzipiell ist die ursprüngliche mit allen (anfangs noch kryptischen) Begriffen Aufgabe gelöst und steht in der Datei "RabattPanel4.java" bereit. Der Anwender muss seine Werte nur schnell genug eintippen (jedenfalls bevor Java mit den Berechnungen anfängt) und vorher herausfinden, was in diese Felder überhaupt einzutippen ist. Anwenderfreundlich ist anders.
Irgenwie muss dem Anwender die Bedeutung der Felder mitgeteilt werden. Am einfachsten dürfte eine Beschriftung vor jedem TextField sein. Beschriftungen werden labels genannt und stehen als Klasse bereit (API-Doku). Also vor jedes TextField einen Label gesetzt.
this.add( new JLabel( "Betrag"), BorderLayout.NORTH); this.add( betragFeld, BorderLayout.NORTH); this.add( new JLabel( "Rabatt"), BorderLayout.CENTER); this.add( rabattFeld, BorderLayout.CENTER); this.add( new JLabel( "Endbetrag"), BorderLayout.SOUTH); this.add( ausgabeFeld, BorderLayout.SOUTH); |
Die Überraschung ist nur am Anfang vorhanden (nach kurzer Zeit der Java-Programmierung weicht sie der Resignation). Nichts, aber auch gar nichts hat sich geändert. Kein Label weit und breit. Woher denn auch. in der ersten Zeile wird dem LayoutManager ein Label überantwortet, das bitte an der nördlichen Grenze residieren soll. In der darauffolgenden Zeile wird verlangt, an derselben Stelle ein TextField. unterzubringen. Natürlich wird der LayoutManager dieser Bitte nachkommen und genau dort ein TextField unterbringen, mit der Konsequenz, dass der Label überschrieben wird. Genau so wird es allen anderen Labels ergehen. Offensichtlich ist das Panel durch den LayoutManager dicht. Aber ein Blick gen Westen läßt Hoffnung keimen, denn dieser Grenzbereich ist frei. Also einen Label im Westen unterbringen, um zu probieren ob sich der Layoutmanager so überreden läßt, die Anwenderfreundlichkeit herzustellen.
this.add( new JLabel( "Betrag"), BorderLayout.WEST);
Diese Zeile vor den Integrationsanweisungen der TextFields eingefügt, zeigt tatsächlich einen Label mit dem Inhalt "Betrag" an. Abgesehen von der fehlerhaften vertikalen Position schon ein Erfolg. Einen weiteren Label im Westen unterzubringen scheitert an den bereits besprochenen Gründen, weshalb Nachdenken erforderlich ist (wie immer, wenn Aufgaben an ein Management delegiert werden).
Es werden eigentlich drei Labels (jeder in einer Zeile) verlangt, aber vielleicht kann ein Label mit drei Zeilen die gleiche Wirkung erzielen. Ändern der Anweisung zu
this.add( new JLabel( "Betrag\nRabatt\nEndbetrag"), BorderLayout.WEST);
führt zu einem traurigen Ergebnis. Labels können also nicht über mehrere Zeilen verteilt werden; jedenfalls nicht bei diesem LayoutManager. Könnten die Labels doch so einfach angeordnet werden wie es bei den TextFields der Fall war. Genau das können sie auch, denn nichts hindert daran, sie in einem Panel dem gleichen LayoutManagement zu unterziehen. Wie aber sollen dieses "LabelPanel" und das "AnzeigePanel" zusammenkommen?
Auf nach Westen (Westward Ho) lautet auch heutzutage die Antwort. Das "AnzeigePanel" enthält Components. Es ist also ein Container. Diese sind aber eine Instanz der Klasse Component, und können somit als solche eingesetzt werden. Wenn also die Positionierung einer einzigen Komponente im Westen funktioniert und ein Panel als eben solche angesehen wird, dürfte die Vermutung naheliegen, dass die Lösung bereits in Form der folgenden Sequenz gefunden ist.
JPanel labels = new JPanel(); labels.setLayout( new BorderLayout()); labels.add( new JLabel( "Betrag"), BorderLayout.NORTH); labels.add( new JLabel( "Rabatt"), BorderLayout.CENTER); labels.add( new JLabel( "Endbetrag"), BorderLayout.SOUTH); this.add( labels, BorderLayout.WEST); |
Diese Sequenz wird vor oder hinter der Integration der TextFields untergebracht. Ein Test des neuen Programms führt leider nur zu einem Teilerfolg. Zwar sind jetzt alle Labels untereinander, aber der LayoutManager beharrt auf seinen Ansprüchen im Norden und Süden des RabattPanels. Erfüllt das Management seine Aufgaben nicht den Anforderungen entsprechend, ist es überfordert. Eine Neubesetzung zieht sehr viel Verwaltungsaufwand nach sich und sollte als letzte Maßnahme in Betracht kommen. Vielleicht kann der Einsatz einer übergeordneten Instanz das bestehende Management unterstützen. Diese Instanz käme jedoch einem Ersatz des bisherigen Managements gleich. Ein anderer Weg verspricht dagegen mehr Erfolg. Die LayoutManager erledigen ihre Aufgaben prinzipiell gut, sind aber etwas stur, sobald ihnen "ins Handwerk gepfuscht" wird. Einer der beiden Manager muss sich immer unterordnen, was seinem Leistungsprofil total widerspricht. Die Lösung kann also nur in völliger Gleichberechtigung beider LayoutManager bestehen. Eine Art "Outsourcing" muss stattfinden.
Zunächst werden aus dem bestehendem Panel die Komponenten und das Management entfernt und in ein neues Panel integriert. Der Name des neuen Panels sei "fields".
JPanel fields = new JPanel(); fields.setLayout( new BorderLayout()); fields.add( betragFeld, BorderLayout.NORTH); fields.add( rabattFeld, BorderLayout.CENTER); fields.add( ausgabeFeld, BorderLayout.SOUTH); JPanel labels = new JPanel(); labels.setLayout( new BorderLayout()); labels.add( new JLabel( "Betrag"), BorderLayout.NORTH); labels.add( new JLabel( "Rabatt"), BorderLayout.CENTER); labels.add( new JLabel( "Endbetrag"), BorderLayout.SOUTH); |
Beide Panels verfügen nun über eigenes LayoutManagement und sind völlig unabhängig voneinander. Nach außen hin können sie sich bekanntlich wie Components verhalten, was den gemeinsamen Anforderungen sehr entgegen kommt. Beide (Panel)Komponenten müssen nur noch nebeneinander in das bestehende RabattPanel positioniert werden. Nun hat dieses (this) Panel keinen LayoutManager mehr (zumindest keinen der explizit eingesetzt wurde), weshalb this Panel einen Neuen benötigt. Aufgrund der gemachten Erfahrungen mit dem BorderLayout stellt sich das Management erneut den Anforderungen. Es wird Ausgewogenheit wahren und daher versuchen, eine Komponente im Westen und eine im Osten unterzubringen.
this.setLayout( new BorderLayout()); this.add( labels, BorderLayout.WEST); this.add( fields, BorderLayout.EAST); |
Das Ergebnis ist, abgesehen von Feinheiten wie fehlenden Doppelpunkten, Farbgebung, Feinjustierung der Positionen, ein voller Erfolg. In der Datei "RabattPanel5.java" ist der bis hier entwickelte Quelltext vorhanden.
Der Anwender ist nun über die Inhalte der einzelnen Felder informiert. Ihm fehlt aber immer noch die Möglichkeit seine eigenen Werte berechnen zu lassen. Bisher können zwar Zeichenfolgen jedweder Art eingegeben werden, aber die Anwendung reagiert nicht. Der Grund wurde zwar bereits angesprochen, soll aber hier noch einmal in Erinnerung gerufen werden. Die gesamte Berechnung wird während der Erzeugung des RabattPanels mit fest vorgegebenen Werten durchgeführt. Änderungen der Feldinhalte werden weder erkannt noch in können sie in die einmalige und bereits abgeschlossene Berechnung einfließen. Dem Anwender allein muss die Kontrolle darüber, wann gerechnet wird, obliegen. Es ist demnach die Frage
Wie kann der Zeitpunkt der Berechnung bestimmt werden?
zu beantworten. Die Antwort ist zwar einfach, die Realisation dagegen nicht. Ein Ereignis (event) bestimmt den Zeitpunkt. Es muss ein Äquivalent für das event "Betätigen der =-Taste eines Taschenrechners" her, das vom Benutzer entweder gezielt ausgelöst wird oder, aufgrund einer bestimmten Reihenfolge der Eingaben, automatisch vom Programm generiert wird.
Die Alternative der automatisierten event-Erzeugung soll zunächst außer acht gelassen werden, denn zunächst ist die Behandlung eines Ereignisses an sich zu klären. In der Datei "MainFrame.java" wurde bereits ein EventHandler für das schließen des Fensters eingesetzt. Dieser "Zuhörer" (EventListener) ist jedoch auf Fensterereignisse spezialisiert und kann daher nur als Ideenlieferant für das neue Ereignis dienen.
Das event war ein Klick auf eine Schaltfläche. Demnach soll auch jetzt eine solche (JButton) eingesetzt werden. der Knopf soll ganz unten (also im Süden) vorhanden sein, womit einfach die entsprechende Sequenz
JButton button = new JButton( "berechnen"); this.add( button, BorderLayout.SOUTH); |
an die bisherigen Zeilen der Creator-Methode angehängt wird. Natürlich bleibt ein Klick auf diese Schaltfläche wirkungslos, denn sie hat niemanden der ihr zuhört. Ein Listener muss her. Aber worauf soll er hören?
Buttons erzeugen sog. ActionEvents, die in einer Methode namens "actionPerfomed(...)" des Listeners hier also eines ActionListeners bearbeitet (performed) werden. Dieser ActionListener kann nun für jede Schaltfläche, völlig individuell, realisiert werden, oder für mehrere ActionEvents die Zuständigkeit übernehmen. Im aktuellen Beispiel des RabattPanels ist zwar nur eine Schaltfläche vorhanden, weshalb sich dieses Problem überhaupt nicht ergibt, aber Kunden sind zu verblüffenden Überlegungen imstande. Kaum ist eine Sache abgeschlossen, kommt der Kunde mit der Idee einer "winzigen" Änderung: "Zwei neue Schaltflächen!" Zweckmäßig ist also der Aufbau eines Listeners für alle ActionEvents. Maximale Flexibilität wird in Java über Klassen und deren Instanzierung erzielt, womit gesagt werden soll, dass eine neue Klasse definiert werden soll.
Eine neue Klasse nur für den ActionListener einer Schaltfläche? Gegenfrage: "Warum in funktionierendem Code 'rumbasteln, um ein lächerliches event abzufangen?" Java gewährt hier alle denkbaren Freiheiten und wegen der einleitend gemachten Aussage zur "beherrschbaren Teilen" wird eine eigene Klasse definiert. In Anlehnung an die zu bewältigende Aufgabe trägt sie den Namen "Actions.java".
Aus der API-Dokumentation geht hervor, dass ActionListener ein Interface ist. Der neuen Klasse sind damit also die zu verwendenden Methoden vorgeschrieben. Glücklicherweise ist im Interface ActionListener nur die Methode actionPerformed(...) vorhanden, was die Aufgabe drastisch erleichtert.
class Actions implements ActionListener { public void actionPerformed( ActionEvent ae) { System.out.println( "Nicht so laut!"); } } |
Der Rohbau steht. Beim Einsatz von Listenern ist es sinnvoll, zunächst die Fähigkeit des Zuhörens an sich zu überprüfen. Deshalb wird in der Methode actionPerformed(...) erst einmal eine simple Ausgabeanweisung untergebracht. Der nächste Schritt besteht nun darin, der Schaltfläche diesen geduldigen Zuhörer anzuempfehlen.
Wie vermutet findet sich in der API-Dokumentation eine entsprechende Methode namens "addActionListener(...)". Unmittelbar hinter der Instanzierung der Schaltfläche kommt diese Methode zum Einsatz, allerdings nicht ohne vorher ein Objekt der Klasse Actions instanziert zu haben.
Actions actions = new Actions(); JButton button = new JButton( "berechnen"); button.addActionListener( actions); this.add( button, BorderLayout.SOUTH); |
In der Datei "RabattPanel7.java" ist die Erweiterung vorhanden und nachdem nun die Zuhörerschaft vorhanden ist, muss ihre Aufmerksamkeit auf die "Rechenkünste" gelenkt werden. Leider ist diese Manipulation der Zuhörer alles andere als einfach, denn wie sollen sie über die anderen Komponenten informiert werden? Das Problem dabei ist: Zwar wird ein event erkannt und es wird auch reagiert, aber wer war der Auslöser und wer ist der Empfänger der neuen Botschaft, die aufgrund des Zuhörens entsteht?
Aufschluss gibt eine Methode aus der Klasse EventObject namens getSource(). Weil ActionEvent eine Instanz von EventObject ist, kann diese Methode zumindest Aufschluss über die Frage nach dem Auslöser geben. Ein entsprechender Aufruf wird nun in der Methode actionPaerformed(..) in der Form
System.out.println( "Ereignisquelle:\n" + ae.getSource());
untergebracht und liefert so aufschlussreiche Informationen wie:
Ereignisquelle: javax.swing.JButton[,0,63,135x27, Layout=javax.swing.OverlayLayout, alignmentX=0.0,alignmentY=0.5, Boarder=javax.swing.plaf.BorderUIResource$CompoundBorderUIResource@492535, flats=1200, maximumSize=, minimumSize=, preferredSize=, defaultIcon=, disabledIcon=, disabledSelectedIcon=, maigrün=javax.swing.plaf.InsetsUIResource[top=2,lest=14,boomt=2,right=14], paintBorder=true, paintFocus=true, pressedIcon=, rolloverEnabled=Fasel, rolloverIcon=, rolloverSelectedIcon=, selectedIcon=, Text=berechnen, defaultCapable=true] |
Diese führen aber im Augenblick nicht wirklich weiter. Es besteht natürlich die Möglichkeit, über die angeklickte Schaltfläche an die TextFields für die Berechnung zu gelangen. Sinnvoll ist ein derartiges Vorgehen jedoch nur in seltenen Fällen. Die Entscheidung für eine separate Klasse eines ActionListeners war falsch.
Wie kann der Listener in das RabattPanel integriert werden, ohne eine eigenständige Klasse zu beanspruchen? Durch einfachesVerfassen der Methode actionPerformed(...), jedoch nicht ohne die Klassendefinition des RabattPanels vorher zu erweitern. Dem Compiler muss nur mitgeteilt werden, dass diese Klasse ihren eigenen ActionListener implementiert. Die Einleitung der Klassendefinition wird erweitert zu
class RabattPanel8 extends JPanel implements ActionListener {
Die Methode actionPerformed(...) wird aus der Datei Actions.java kopiert und in die Datei "RabattPanel8.java" übernommen. Der Ort spielt innerhalb des Klassifizierungsblocks eigentlich keine Rolle, aber um chronologisch fortzufahren, soll die Methode unmittelbar vor dem Constructor liegen. Jetzt wird noch das Objekt actions entfernt und ... Was ist mit der Zeile
button.addActionListener( actions);
Welches Objekt der Klasse ActionListener soll hier als Argument dienen? Wo soll die Instanz sein? Nun, die Sache ist ganz einfach dieses (this) Objekt ist die Instanz, denn this object implementiert den ActionListener. Damit kann der Schaltfläche auch getrost dieses Objekt als Argument für den Listener übergeben werden.
button.addActionListener( this);
Ein Testlauf ergibt denn auch ein entsprechendes Ergebnis. Jetzt ist es ein Leichtes, die Referenzen der TextFields zu erhalten, denn sie befinden sich ja in dieser Klassendefinition. Die jetzt vorzunehmenden Änderungen sind:
Entfernen der Vorbelegung von Betrag und Rabatt.
Entfernen des Methodenaufrufs zur Berechnung.
Methoden zur Berechnung innerhalb der Methode actionPerformed(...) aufrufen.
Die Datei "RabattPanel9.java" enthält diese Änderungen. Eine Überprüfung ergibt denn auch zunächst leere TextFields nach erfolgtem Start des Programms. Die Eingabe der ursprünglichen Werte (1234.56 und 7) in die ersten beiden Felder ergibt nach Klick auf die Schaltfläche auch noch das korrekte Ergebnis.
Um das jetzt vielleicht aufkommende Gefühl der Zufriedenheit im Keim zu ersticken, soll einmal folgende Eingabe ausprobiert werden:
Betrag = xyz und Rabatt = 0
Sobald sich das System wieder beruhigt hat, sollte an die 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 die Zeichenfolge "wrzlprm" hineininterpretiert werden? Hier ist also der Fehler zu suchen.
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.
Sequenzen die "umherfliegende" Exceptions nach sich ziehen können, werden nach einem bestimmten Schema verfasst. Dieses Scheme 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 diese 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 mit
Float zahl = null; try { zahl = new Float( text); } catch (NumberFormatException nfe) { System.out.println( "Unkorrekte Eingabe."); System.exit( 1); } |
gegeben. 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.
Die Berechnung darf nur dann durchgeführt werden, wenn in den beteiligten TextFields korrekte Eingaben enthalten. Was aber soll im anderen Fall geschehen? Der Focus kehrt auf das Feld mit der fehlerhaften Eingabe zurück. Das Feld soll außerdem rot hinterlegt sein und der fehlerhafte Text soll 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; } |
Nun geht es an 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 prinzipiell bleiben wie sie ist, jedoch kann die interne Überprüfung auch entfallen, wenn gewährleistet ist, dass die Methode inputOk(...) unmittelbar vorher aufgerufen und deren Ergebnis entsprechend berücksichtigt wird. Weil immer noch die Phase des Entwurfs ansteht, erfolgt keine Änderung der Methode valueOf(...).
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 keine Berechnung darf erfolgen. Innerhalb der Methode calculate(), die ohnehin als Argument dient, sollte also die Überprüfung nicht stattfinden. Damit bleibt nur die Methode actionPerformed(...) des EventListeners, in der sich ja bereits die Überprüfung auf leer befindet. Zunächst also die Überprüfung des betragFelds und ggf. Reaktion auf die fehlerhafte Eingabe.
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.
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 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 soll immer dann "normal" erscheinen, wenn keine fehlerhaften Eingaben vorhanden sind. Nicht fehlerhaft im Sinne des Anwenders! Also müssen sie "normal" gefärbt immer dann 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äßt. 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 dem entsprechend 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.
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.
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 "FocuListener". Seine Fähigkeiten reichen weit über das hier angesprochene hinaus, sofern er richtig eingesetzt wird.
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.
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.
Für das Programmierteam wäre es wahrscheinlich angenehm, die ermittelten Ergebnisse nicht immer ablesen zu müssen, um sie in das Gesamtsystem zu übernehmen. Deshalb sollten ein paar get-Methoden in dieser Klasse vorhanden sein. Es drängt sich förmlich auf, die bereits erstellten setBetrag(...) und setRabatt(...) Methoden in entsprechende gets umzuwandeln. Jedoch wäre das nicht konsequent.
Noch einmal zurück zu den Überlegungen hinsichtlich Manipulation der Daten. Echte Konsistenz liegt nur unmittelbar nach dem Klick auf die Schaltfläche "berechnen" vor. Danach kann jedes der beiden Eingabefelder erneut beschrieben werden, was zu völlig sinnlosen Daten führt, wenn die Inhalte der Felder zu diesem Zeitpunkt gelesen werden. Es muss demnach sichergestellt sein, dass die get-Methoden nur korrekte oder keine Daten übermitteln. Erneutes Überdenken der Konzeption ist also erforderlich.
Die gesamte GUI ist nur ein Hilfsmittel für den Anwender. Sie hat mit den Daten prinzipiell nichts zu tun, sondern dient nur deren Sichtbarmachung. Wenn dieser Schritt getan ist, ist die weitere Vorgehensweise sehr einfach. Die Daten sind Properties, die GUI ist methodisch. Im vorhandenen Programm sind als Properties nur die TextFields vorhanden, aus denen ja die Daten erst extrahiert oder in denen sie angezeigt wurden.
Zunächst werden die Schnittstellen definiert. Interessant sind ausschließlich die ermittelten Werte, weshalb die get-Methoden ausnahmslos den primitive-Typ float übergeben. Es fehlen noch die Properties, die sogleich als Variablen vom Typ float und einem begrenzten view namens private erstellt werden.
private float betrag = 0; private float rabatt = 0; private float result = 0; public float getBetrag() { return betrag; } public float getRabatt() { return rabatt; } public float getResult() { return result; } |
Wann müssen die Werte in die Variablen eingetragen werden? Weil die Korrektheit nur unmittelbar nach der Berechnung gewährleistet ist, sollten die Werte auch zu diesem Zeitpunkt in den Variablen abgelegt werden. Damit ändert sich die Methode calculate() und die Anweisung der Ergebnisausgabe.
private void calculate() { betrag = valueOf( betragFeld.getText()); rabatt = valueOf( rabattFeld.getText()); result betrag * (1 - rabatt / 100); } |
Die lokalen Variablen sind überflüssig, sie werden einfach durch die Properties ersetzt. Nun noch die Änderung der Ergebnisausgabe. aus der Anweisung
ausgabeFeld.setText( ""+calculate());
muss der Aufruf von calculate() zunächst herausgenommen werden, denn die neue Methode übergibt keinen Wert mehr. Der Aufruf wird durch das Property result ersetzt, womit siech die neue Ausgabeanweisung ändert zu
calculate(); ausgabeFeld.setText( ""+result); |
Die Datei RabattPanel14.java zeigt denn auch diese Änderungen und enthält außerdem die get-Methoden. Es könnte noch viel an diesem kleinen Programm gearbeitet werden, aber es soll ja nur als Sprungbrett für den eigentlichen Einstieg in Java dienen.
Die Datenkonsistenz ist gesichert, eventuelle "Manipulationen" in der Anzeige werden nicht publiziert, kurzum Alles steht zum Besten in der besten aller denkbaren Welten (Blaise Pascal). Es bestehen Zweifel. ist der Quelltext auch nach 1 bis 2 Jahren noch einleuchtend? Wie war das noch mit den uneditierbaren Textfeldern im alten JDK? Wie und wo könnte Dies oder Das oder was...
Es fehlt die Kommentierung und die elementare Dokumentation. Kommentare sind in Java die erste Voraussetzung für eine durchgehend einheitliche Dokumentation. So kennt Java gleich drei verschiedene Formen der Kommentareinleitung, die sich wieder in zwei funktionelle Kategorien unterteilen lassen.
//
ist die einfachste Form und besagt weiter nichts
als:
Ignoriere alle Zeichen bis zum Zeilenende.
/*
bietet die Möglichkeit durchgehende
Kommentierungstexte über mehrere Zeilen einzufügen. Es
besagt:
Ignoriere alle Zeichen bis */ einschließlich.
/**
ist die Einleitung eines Kommentars, der in die
Dokumentation übernommen wird. Es bedeutet: wieder
Ignoriere
alle Zeichen bis */ einschließlich.
Die Dokumentation wird von Java durch ein Tool (Werkzeug) in einer bisher kaum dagewesenen Form erleichtert. Die Möglichkeiten sin äußerste vielfältig und deshalb wird hier nur der Einsatz der "Standard Doclet" beschrieben. Detaillierte Informationen über die Verwendung von javadoc sind in der Java-Dokumentation (da wo auch die API-Dokumentation ist) unter dem Verzeichnis tooldocs zu finden.
Der erste Kontakt mit diesem Werkzeug kann durch Eingabe von im der Kommandozeile
javadoc -d doku -private RabattPanel14
hergestellt werden. Natürlich muss vorher in den Pfad mit dem zu dokumentierenden Dateien gewechselt werden. Die Elemente dieser Zeile sind
javadoc
das
Programm zur Erzeugung von Dokumentationen.
-d
eine
Option, um die folgende nicht leere Zeichenkette als Pfadangabe für
die Dokumentationsdateien zu erklären.
doku
der
relative (es kann auch ein absoluter sein) Pfad, in dem die
erzeugten Dateien abgelegt werden.
-private
eine
Option, um alle (also auch die privaten) Definitionen (Properties
und Methoden) in die Dokumentation aufzunehmen.
RabattPanel14
die
zu dokumentierende(n) Datei(en). Hier sind auch Angaben wie "*.*"
(das sind alle Dateien im aktuellen Pfad) usw. möglich.
Das Ergebnis dieser Eingabe mag begeistern, aber es besteht praktisch nur aus einer Auflistung aller vorhandenen Properties und Methoden. Der beschreibende Inhalt fehlt. Wie dieser beschreibende Inhalt in den Quellcode integriert wird und wie er gleichzeitig den Programmierer Hinweise gibt ohne dass Letzterer den Kommentar komplett lesen muss, wird hier kurz erörtert.
Jeder Betrieb, jedes Unternehmen hat seine Eigenheiten. Dazu zählen auch Kommentierungsstyles. Um javadoc zu nutzen, ist es unabdingbar geworden die beschriebenen Elemente in den Quelltext einzubinden. Dabei gehen manch alte Gewohnheiten "über Bord". Geschickte Anwendung der gebotenen Möglichkeiten gestatten jedoch oft die Kommentierungen der "Altvorderen" zu übernehmen.
Kommentarform |
Bedeutung |
/*--------------------*/ /* Sequenz / Schleife */ /*--------------------*/ |
Kommentar über einem Abschnitt aus mehreren
Zeilen. |
//*============================*/ /** Private / Protected Method */ //*============================*/ |
Kommentar zu Methoden des eingeschränkten views. |
//*****************/ /** Public Method */ //*****************/ |
Kommentar zu Methoden der uneingeschränkten, also public views. |
Kommentare der "/**"-Form, die unterhalb der Klassendefinitionsebene (z.B. innerhalb von Methoden) liegen, bleiben von javadoc unberührt. Sie entsprechen dann den einfachen, nicht dokumentierenden Kommentaren. In den gängigen Beispielen von Javaprogrammen werden keine Unterschiede zwischen den views gemacht. Auch die einschlägigen IDEs machen keine derartigen Unterscheidungen. Die Vorgaben der IDEs können jedoch problemlos erweitert werden. Ein "normaler" Kommentar für die Dokumentation hat die Form:
/** * Das hier ist die ultimative Methode für * Internetverbindungen. * * params: * URL url: die Internetadresse * int count: der Verbindungszähler */ |
Wie das bisherige Programm durch Kommentare sinnvoll ergänzt werden kann, zeigt die Datei RabattPanel15.java. Die Anwendung von javadoc auf dieses kommentierte Programm sind im Ordner docjav abgelegt und kann dort index.html. angesehen werden. Es zeigt sich schnell, dass eine vollständige Dokumentation des Quelltextes mit diesem Werkzeug möglich ist, sofern die Kommentare mehrzeilig sind und Angaben über Argumente und Rückgabewerte enthalten. Die o.a.Form der "uralt-Kommentierung" ist somit eigentlich hinfällig, es sei denn die "Firmenphilosophie" verlangt es (Als Java-Spezialist hat man die tollsten Gründe den Arbeitgeber zu wechseln).
Eine kurze Zusammenfassung des bisher Erreichten:
Unterschiede zwischen den
Begriffen von Zeichenfolge, Zahl und
Wert.
Notwendigkeit von
Typen, Bezeichnern und Referenzen.
Einsatz
von Creator-Methoden zur Umwandlung von Zeichenketten in
Zahlenobjekte.
Definition
einer Klasse und der Einsatz der main-Methode.
Aufbau
einer GUI mit Frame, TextFields.
Konzept der
EventListener in Form des WindowAdapters.
Aufteilung
von Projekten in mehere Klassen und Optimierung durch den Einsatz
eigener Methoden..
Das view (Sichtbarkeit von außen)
in Form von public und private.
Unterschiede
zwischen Variablen und Properties.
Optimierung durch
Anwendung von Substitutionen und elementarer
Mathematik.
Einsatz von LayoutManagern und die Kapselung
von Komponenten in Container.
Anwendung externer und
interner EventListener durch den Einsatz des
ActionListeners.
Ausnahmebehandlung und Anwendung eines
eigenen ExceptionHandlers.
Äderung von Properties
vorhandener Objekte zum Zwecke der Fehlerbehandlung.
Focussierung
bestimmter Komponenten.
Elementare Schutzvorrichtungen vor
Datenmanipulation.
Kommentierung Dokumentation von Java-Programmen
mit javadoc.
Wer hätte gedacht, in so kurzer Zeit so viel über eine Programmiersprache zu lernen? Die abschreckenden Bemerkungen zu Beginn sind nicht mehr so abschreckend, die Suche nach Methoden in der API-Dokumentation wird immer häufiger zu einer Bestätigung der vermuteten Schreibweise. Objekte haben ihre Abstraktheit weitgehend eingebüßt und werden behandelt, wie andere Referenzen auch.
Wie jede andere Sprache auch, verfügt Java über essentielle Elemente für die Verständlichkeit. Zu diesen Elementen zählen Grammatik und Semantik, die ihrerseits wieder die Syntax bestimmen. Hier kann kein Informatikkurs über die Theorie der Syntax und anderer linguistischer Gegebenheiten modernen Programmiersprachen abgehalten werden. Deshalb werden die elementaren Bestandteile nur kurz angesprochen und über Links wird auf deren Konsequenz verwiesen.
Dieses Teilgebiet der Linguistik setzt sich mit der Bedeutung der Wörter auseinander. Bei Programmiersprachen ist die Semantik ein weitgehend statisches Gebilde, denn es existieren sog. Schlüsselwörter, deren Bedeutung nicht wandelbar ist. In Java zählen z.B. if, while, case zu diesen Schlüsselwörtern. Sie steuern das Ablaufverhalten von Sequenzen und geben dem Programmentwickler so die Möglichkeit, den gesamten Ablauf zu kontrollieren. Diese Worte zählen zu den Controls.
Schlüsselwörter sind auch die Zeichen +, -, *, ... Hier ist das "+"-Zeichen bereits eine Ausnahme, denn es kann als Additionsoperator, Vorzeichen oder concatenation-Anweisung dienen. Es kommt eben darauf an, in welchem Zusammenhang dieses Zeichen benutzt wird. Normalerweise werden diese Zeichen als Operatoren angesehen und auch so eingesetzt.
Grammatiken sind wohl eins der komplexesten Gebilde in Programmiersprachen. Sie bilden das Gerüst der Sprache und sind damit verantwortlich für die Interaktionen des Programmentwicklers und der Maschine. Mathematische Gegebenheiten (Punkt- vor Strichrechnung, Klammern, ...) sind ebenfalls zu berücksichtigen. Eine weitere Aufgabe ist die Wahrung struktureller Zusammenhänge. In natürlichen Sprachen werden dafür oft Satzzeichen wie Punkt und Komma eingesetzt, nicht so bei Programmiersprachen. Die Aufgabe des Kommas (ursprünglich eine Art der Klammerung) übernehmen hier die geschweiften Klammern. Sie kennzeichnen zusammenhängende Sequenzen und bilden sog. Blocks.
Sehr wichtig ist bei einer Grammatik auch, zwischen Aussagen und Ausdrücken zu unterscheiden. In beiden Fällen handelt es sich um syntaktische Gebilde, die auch untereinander "vermischt" werden können. So kann eine Aussage oft erst dann getroffen werden, wenn ein bestimmter Ausdruck bekannt ist. Es ist also sehr wichtig, den Unterschied zwischen Aussage (statement) und Ausdruck (expression) zu kennen.