Benutzer-Werkzeuge

Webseiten-Werkzeuge


klassen2:interfaces:start

Interfaces

Stellen wir uns ein Bibliotheksprogramm vor, das verschiedene Listen ausgeben kann:

  • eine Liste aller Bücher der Bibliothek
  • eine Liste aller Ausleiher/innen
  • eine Liste aller säumigen Ausleiher/innen
  • usw.

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:

  • eine Liste aller Bücher der Bibliothek auf dem Bildschirm ausgeben
  • eine Liste aller Bücher der Bibliothek als pdf-Datei ausgeben
  • eine Liste aller Bücher der Bibliothek als Html-Datei ausgeben
  • eine Liste aller Bücher der Bibliothek als csv-Datei ausgeben
  • eine Liste aller Ausleiher/innen auf dem Bildschirm ausgeben
  • eine Liste aller Ausleiher/innen als pdf-Datei ausgeben
  • eine Liste aller Ausleiher/innen als Html-Datei ausgeben

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.

Bibliotheksprogramm: interface Ausgeber

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.

Bibliotheksprogramm: Implementierungsvarianten des Interfaces

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!

Weitere Implementierungsvarianten

Wir schreiben weitere Klassen, die das Interface implementieren:

Bibliotheksprogramm: Listengenerator

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.

Klassendiagramm

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.

Aufgabe 1

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' ]

Anwendung: Timer

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.

Aufgabe 2

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.

Flächenberechnung mittels Monte-Carlo-Simulation

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.

1. Schritt: Wir umgeben die Fläche mit einem achsenparallelen Rechteck

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.

2. Schritt: Wir platzieren sehr viele zufällige Punkte ins Rechteck

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

3. Schritt: Flächenberechnung

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$$

4. Schritt: Punktezahl erhöhen

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:

  • Nach 10000 Schritten liegen 7839 Punkte in der Fläche. Näherungswert nach 10000 Schritten: 3.1356
  • Nach 20000 Schritten liegen 15674 Punkte in der Fläche. Näherungswert nach 20000 Schritten: 3.1348000000000003
  • Nach 30000 Schritten liegen 23431 Punkte in der Fläche. Näherungswert nach 30000 Schritten: 3.1241333333333334
  • Nach 40000 Schritten liegen 31303 Punkte in der Fläche. Näherungswert nach 40000 Schritten: 3.1303
  • Nach 50000 Schritten liegen 39170 Punkte in der Fläche. Näherungswert nach 50000 Schritten: 3.1336000000000004

Programm

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:

  • Wir müssen ein achsenparalleles Rechteck um die Fläche ziehen, brauchen also die Werte $x_{min}$, $x_{max}$, $y_{min}$ und $y_{max}$.
  • Für jeden der zufällig bestimmten Punkte im Rechteck müssen wir wissen, ob er in der zu messenden Fläche liegt oder nicht.

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.

Aufgabe 3 (freiwillig)

Übernimm den Code für das Interface Fläche und die Klasse Flächenberechner und programmiere dazu

  • a) Eine Klasse Dreieck, die das Dreieck mit den Eckpunkten $(0,0)$, $(0,1)$ und $(1,0)$ repräsentiert.
  • b) Eine Klasse Sinus, die dem Flächenstück unter dem ersten "Buckel" der Sinusfunktion entspricht.
    Berechne mit Hilfe dieser beiden Klassen näherungsweise den jeweiligen Flächeninhalt.
  • c) Für die Interessierten: Versuche, die Klasse 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.

Hier geht's zur Lösung.

klassen2/interfaces/start.txt · Zuletzt geändert: 2021/05/09 00:22 von Martin Pabst