Kurs:Java – ein schneller Einstieg/Übersicht behalten

Übersicht behalten

Bearbeiten

Teilen, unter dem Gesichtspunkt herrschen zu wollen, verlangt nach Übersicht. Damit alle Teile auch dort sind wo sie hingehören und das tun was von ihnen verlangt wird, ist dieser Abschnitt vorhanden. Es ist immer eine Frage des eigenen Geschmacks, wie weit ein Projekt zerlegt wird. Hier ist aber nicht der Ort auf die Vor- und Nachteile einzugehen. Es sollen nur die Möglichkeiten gezeigt werden.

Aufgaben delegieren

Bearbeiten

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 LINK: "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.

Die Klasse JPanel

Bearbeiten

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().

Aufgabe:

Was ist ein ContentPane

  1. Der Bereich eines Frames ohne Rahmen und Menüs.
  2. Eine JContainer-Instanz für Frames.
  3. Das Hauptpanel aller Fenster.

Bestehende Klassen für eigene Klassendefinitionen verwenden

Bearbeiten

Eine der herausragendsten Eigenschaften in der objektorientierten Programmierung ist die Vererbung. Genau darum geht es jetzt. 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 betragWert = Float.parseFloat( betragText);
 float rabattWert = Float.parseFloat( rabattText);
 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. Eine erste Teilung ist vollzogen. Es sieht so aus, als wurde sie nur nach "RaballPanel" verschoben, aber eben nicht ganz. Die Anzeige bleibt allein dem Frame-Window und der einzigen main-Methode vorbehalten. Ein kleiner, aber ein erster Schritt.

Aufgabe:

Modifizieren der Klasse MyFrame, so dass eine Instanz der Klasse RabattPanel verwendet wird.

Eigenschaften und Hilfsvariablen trennen

Bearbeiten

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 --(Hier fehlt doch etwas!)-- Ereignisse haben die "Eigenschaft" unverhofft aufzutreten. Programme, die nach viel Mühe endlich arbeiten, zeigen sich besonders überrascht wenn sie beendet werden. So stellt das Ende einer Berechnung und die Anzeige des Ergebnisses keinesfalls auch immer ein Ende des Programms dar. Im vorliegenden Fall gehört die GUI zum Programm. Wird das Programm beendet, verschwindet die GUI und damit natürlich auch die Anzeige. Die Anzeige des Ergebnisses ist nicht wahrzunehmen, denn unmittelbar nach der Anzeige verschwindet sie mitsamt den Eingaben.

Besser ist es also alles so zu lassen wie es ist und ein bestimmtes Ereignis zu benutzen das Programm zu beenden. Hier ist dieses Ereignis ein Klick auf den Button zum schließen des Fensters. Das Verhalten entspricht zwar dem Erwarteten, aber Java denkt nicht daran das Programm selbst zu beenden. Eine entsprechende Anweisung wurde nicht programmiert. 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.

Eigene Methoden aufbauen

Bearbeiten

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 sind gewiss keine Strings, sondern Werte 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 Methoden 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 sich durch die "Verschiebung" der Berechnungssequenz aus der Constructor-Methode in die Berechnungsmethode.

float calculate() {
String betragText = "1234.56";
String rabattText = "7";
float betragWert = Float.parseFloat( betragText);
float rabattWert = Float.parseFloat( rabattText);
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 betragWert = Float.parseFloat( betragText);
float rabattWert = Float.parseFloat( rabattText);
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.

Aufgabe:

In der Methode calculate werden schon bei der Deklaration von Variablen Berechnungen ausgeführt.

  1. Deklarationen so umschreiben, dass hier keine Berechnungen vorhanden sind.
  2. Optimieren der Methode nach eigenen Gesichtspunkten.

Das Ergebnis behalten und mit der "Optimierung" am Ende dieses Abschnitts vergleichen.

Optimierung durch Substitution

Bearbeiten

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 betragWert = Float.parseFloat( betragText);
float rabattWert = Float.parseFloat( rabattText);
*/
float betragWert = Float.parseFloat( betragFeld.getText());
float rabattWert = Float.parseFloat( rabattFeld.getText());

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 betragWert = Float.parseFloat( betragFeld.getText());
float rabattWert = Float.parseFloat( rabattFeld.getText());
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) {
 return Float.parseFloat( text);
}

Die Berechnung wird zur Privatangelegenheit des RabattPanels erklärt. und wieder sind zwei Zeilen in der Methode calculate ü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;
}

Optimierung durch Nachdenken

Bearbeiten

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.

Aufgabe:

Unterschiede der eigenen Lösung aus der vorherigen Aufgabe mit der hier als optimiert dargestellten Lösung vergleichen.

  • Gibt es überhaupt Unterschiede?
  • Warum wurden nicht auch die "wertgebenden" Methoden (valueOf(...)) in die return-Anweisung übernommen? Nicht unmittelbar ersichtlich. Ein paar Gedanken an den Anwender verschwenden und an die Realisation seiner Wünsche.