Stellen wir uns ein Bibliotheksprogramm vor, das verschiedene Listen ausgeben kann:
Das Bibliotheksprogramm wird eine Klasse Listenausgabe
haben, die für jede der obigen Listen eine Methode besitzt.
Von einem guten Programm würden wir erwarten, dass es diese Listen auf dem Bildschirm, als pdf-Datei, als Html-Datei oder als csv-Datei (zur Verwendung in Tabellenkalkulationen) ausgeben kann. Es müsste also jede der obigen Methoden mehrfach geben:
usw.
Ist es wirklich nötig, für jede Ausgabemöglichkeit jede der Listenmethoden erneut zu schreiben? Der Programmcode der ersten vier Methoden wird doch größtenteils identisch sein, ebenso der der nächsten vier Methoden usw.!
Klar geht es einfacher: Für solche Zwecke verwenden wir Interfaces.
Die Lösung des obigen Problems besteht in der Trennung der Aufgaben (separation of concerns): Das Bibliotheksprogramm besitzt eine Klasse Listengenerator
, die zur Ausgabe jeder Listenart (Bücherliste, Ausleiherliste, …) je eine Methode besitzt. Dieser Klasse stellen wir ein Objekt (im folgenden kurz: Ausgabeobjekt) zur Verfügung, das sich darum kümmert, in welcher Form die Listen ausgegeben werden (auf dem Bildschirm, als pdf-Datei, …). Damit die Methoden der Klasse Listengenerator dieses Ausgabeobjekt nutzen können, müssen sie „wissen“, welche Methoden es besitzt. Diese Vereinbarung wird in einem Interface getroffen:
interface Ausgeber { void schreibeZellwert(String wert); void neueZeile(); }
Ein interface ist eine Vereinbarung darüber, welche Methoden eine Klasse mindestens besitzen soll. Im Interface werden die Methoden nur deklariert, d.h. es wird angegeben, welchen Bezeichner ( = Namen) sie tragen, welche Parameter sie benötigen und welchen Datentyp ihr Rückgabewert hat. Der Methodenrumpf (d.h. die Anweisungen in {
… }
) wird weggelassen. Jede Methodendeklaration in einem Interface wird durch ein Semikolon (;
) abgeschlossen.
In unserem Fall erwarten wir vom Ausgabeobjekt, dass es Tabellen ausgeben kann. Dazu besitzt es eine Methode zur Ausgabe von Text in einer Tabellenzelle und eine Methode, um eine neue Tabellenzeile zu beginnen.
Mit dem Interface alleine kann man nichts anfangen, denn es besitzt noch keinen Programmcode, der die Aufgaben wirklich erledigen kann. Wir müssen Klassen schreiben, die genau die Methoden besitzen, die das Interface definiert. Man sagt: Diese Klassen implementieren das Interface. Wir beginnen mit einer Klasse, die eine Tabelle auf dem Bildschirm ausgeben kann:
Die Festlegung implements Ausgeber
in Zeile 1 bedeutet: „Die Klasse Bildschirmausgeber
besitzt alle im Interface Ausgeber
definierten Methoden. Lösche im oberen Beispiel mal eine der beiden Methoden der Klasse Bildschirmausgeber
raus und schau' Dir an, was passiert!
Wir schreiben weitere Klassen, die das Interface implementieren:
Der Listengenerator soll mit jeder der obigen Ausgabeklassen zusammenarbeiten können. Wir geben ihm daher ein Attribut ausgeber
vom Typ Ausgeber
. Dieses Attribut kann auf beliebige Objekte zeigen, die das Interface Ausgeber
implementieren. Daher lassen sich mit diesem Attribut nur diejenigen Methoden aufrufen, die das Interface Ausgeber
definiert.
Be
Wir lassen das Attribut ausgeber
zuerst auf ein Objekt der Klasse Bildschirmausgeber
zeigen und rufen die Methode schreibeBücherListe()
auf. Anschließend wiederholen wir dasselbe mit den beiden anderen Ausgeber-Klassen.
Im Beispiel oben gibt es nur eine einzige Methode schreibeBücherListe()
, die sich nicht darum kümmern muss, in welcher Form ihre Ausgabe ausgegeben wird. Wir rufen sie dreimal auf und erhalten drei unterschiedlich formatierte Ausgaben, indem wir einfach nur jedesmal ein anderes Objekt zur Verfügung stellen, das das Interface Ausgeber
implementiert.
Ein Interface sieht im UML-Diagramm aus wie eine Klassenkarte, der Bezeichner ist jedoch kursiv geschrieben und durch ein vorangestelltes «interface» als solches gekennzeichnet. Dass eine Klasse ein Interface implementiert, kennzeichnet man im UML-Diagramm durch eine gestrichelte Verbindungslinie, die beim Interface in ein Dreieck mündet.
Schreibe eine Klasse JSonAusgeber
, die das Interface Ausgeber
implementiert und Tabellen im JSon-Format als Liste ausgibt, also so:
[ 'Zeile1, Spalte1', 'Zeile1, Spalte2' ], [ 'Zeile2, Spalte1', 'Zeile2, Spalte2' ]
Wenn Du möchtest, dass eine Methode periodisch nach immer gleichen Zeitabständen aufgerufen wird, kannst Du dafür die Timer
-Klasse benutzen. Die Methode
Timer.repeat(runnable, deltaTimeInMs);
bekommt als Parameter ein Objekt übergeben, das das Interface Runnable
implementiert und damit gesichert eine Methode run()
besitzt:
interface Runnable{ public void run(); }
Diese Methode wird von der Klasse Timer
nach jeweils deltaTimeInMs
Millisekunden immer wieder aufgerufen.
Schreibe ein Programm, dass ständig die seit dem Programmstart vergangene Zeit in Minuten und Sekunden (Beispiel: 03:12) anzeigt.
Tipp: Die Methode SystemTools.clearScreen()
löscht den Textausgabebereich.
Wir wollen Inhalte ebener, begrenzter Flächen mithilfe der Monte-Carlo-Simulation berechnen. Am Beispiel der Fläche des Einheitskreises (Kreis um den Punkt (0,0) mit Radius 1) erkläre ich Dir, wie das geht.
In diesem Fall wählen wir das Rechteck, das von $x_{min} = -1$, $x_{max} = 1$, $y_{min} = -1$ und $y_{max} = 1$ begrenzt ist.
Wie das aussieht, siehst Du an der Grafik rechts oben. Ein Teil der Punkte liegt in der zu bestimmenden Fläche (grün), ein Teil außerhalb (rot).
Angenommen, wir haben 10000 Punkte gesetzt, davon liegen 7894 im Einheitskreis. Dann lässt sich ein guter Näherungswert für seinen Flächeninhalt so berechnen: $$ Fläche_{Kreis} = \frac{Fläche_{Rechteck}}{10000} \cdot 7894 = \frac{(x_{max} -x_{min}) \cdot (y_{max} -y_{min})}{10000}\cdot 7894 = \frac{4}{10000}\cdot 7894 \approx 3,1576$$
Da die Punkte zufällig gewählt sind, ergibt sich bei jedem Programmablauf ein anderer Wert für die Kreisfläche. Dieser nähert sich bei zunehmender Punktezahl aber mit immer größerer Wahrscheinlichkeit immer mehr dem exakten Wert ($\pi\cdot r^2 = \pi\cdot 1^2\approx 3,141592$) an. Meldung eines beispielhaften Programmablaufs:
Die Klasse Flächenberechner
verfügt über eine Methode berechneFlächenInhalt(Fläche fläche)
, die Mittels der Monte-Carlo-Methode den Flächeninhalt der als Parameter übergebenen Fläche berechnen kann. Aber wie soll so ein Fläche
-Objekt beschaffen sein, d.h. welche Methoden soll es haben?
Überlegen wir, was wir zur Durchführung der Monte-Carlo-Methode benötigen:
Diese Anforderungen formulieren wir als Interface:
interface Fläche { public boolean punktLiegtDrin(double x, double y); public double getXMin(); public double getXMax(); public double getYMin(); public double getYMax(); }
Wir schreiben eine Klasse Einheitskreis, die das Interface implementiert und dem Einheitskreis entspricht.
Erinnerung:
Ein Punkt $(x, y)$ liegt genau dann im Einheitskreis, wenn $\sqrt{x^2 + y^2} \le 1$, also wenn $x^2 + y^2 \le 1$.
Tipp: Lass' das Programm mit sehr höher Geschwindigkeit ablaufen, sonst dauert es ewig, bis Du etwas siehst.
Übernimm den Code für das Interface Fläche
und die Klasse Flächenberechner
und programmiere dazu
Flächenberechner
zu verstehen!Das Monte-Carlo-Verfahren verwendet man in der Praxis natürlich nicht für so einfache ebene Flächen wie die Beispiele oben, sondern nur zur Berechnung des Flächeninhalts komplex gestalteter Flächen in mehrdimensionalen Räumen.