8  Weiterführende Konzepte der OOP

Im Folgenden werden einige Konzepte vorgestellt, die einen entscheidenden Einfluss auf die Programmierung nehmen und komplexere Programmstrukturen ermöglichen.

8.1  Schnittstellen

Schnittstellen, auch als Interfaces bezeichnet, werden dazu verwendet, eine Trennung zwischen Spezifikation und Implementierung herzustellen.

Mit Schnittstellen kann bereits im Vorhinein festgelegt werden, welche Funktionalität von den implementierenden Klassen zur Verfügung gestellt werden soll. Bei der Konzeption einer Schnittstelle muss die Funktionsweise der verwendeten Methoden noch nicht bekannt sein. Schnittstellenklassen beinhalten ausschließlich abstrakte Methoden und Konstanten, die implizit öffentlich sind. Die Klassen selbst enthalten keine eigenen Variablen.

Anstatt des Schlüsselworts class wird eine Schnittstelle mit interface deklariert.

public interface DistanceSensor

Klassen, die eine solche Schnittstelle implementieren wollen, müssen das Schlüsselwort implements in ihren Header mit aufnehmen, gefolgt vom Namen der Schnittstelle.

public class SonarSensor implements DistanceSensor

Dies ermöglicht es, Objekte zu konstruieren, die eine gemeinsame Schnittstelle aufweisen, obwohl die Klassen nicht voneinander erben. Nützlich ist eine Schnittstellen-Klasse immer dann, wenn Eigenschaften einer Klasse überschrieben werden sollen, die nicht direkt in ihrer normalen Vererbungshierarchie abgebildet werden können oder sollen. Soll zum Beispiel eine Verwaltung von Distanzsensoren entwickelt werden, so fällt schnell auf, dass alle eine gibDistanz-Methode benötigen. Je nach Sensor können diese Methoden unterschiedlich implementiert sein, was der Benutzer der Schnittstelle aber nicht zu wissen braucht.

pict

Abbildung 8.1.: UML-Diagramm einer Schnittstelle

Somit können die Module, die die gleiche Schnittstelle implementieren, ausgetauscht werden. Das Konzept der Schnittstelle dient also auch der Modularisierung. Das folgende Beispiel zeigt die Implementierung der oben genannten Schnittstelle und der Klassen, die diese Schnittstelle implementieren. In der benutzenden Klasse (FahreBisWand) könnte der SonarSensor durch LaserScanner ersetzt werden, ohne dass sich in der weiteren Funktionsweise etwas ändern würde. Den Benutzer der Schnittstelle brauchen die unterschiedlichen Implementierungen in den jeweiligen Klassen nicht zu interessieren.


Programm 8.1: Verwendung von Schnittstellen
 
1/∗∗ 
2 Interface for distance measuring classes. 
3/ 
4interface DistanceSensor { 
5    public double readDistance(); 
6} 
7/∗∗ 
8 This class describes a sonar sensor. 
9 readDistance() must be implemented here 
10/ 
11class SonarSensor implements DistanceSensor { 
12    public double readDistance() { 
13        //specific implementation of readDistance() 
14    } 
15} 
16/∗∗ 
17 This class describes a laser scanner. 
18 readDistance() must be implemented here as well 
19/ 
20class LaserScanner implements DistanceSensor { 
21    private int degree = 0; 
22    public double readDistance() { 
23        //specific implementation of readDistance() 
24    } 
25    public void changeDirection(int degrees) { 
26        degree = degrees; 
27    } 
28} 
29 
30public class GoToWall { 
31    public static void main(String[] args) { 
32        DistanceSensor sensor; 
33        sensor = new SonarSensor(); 
34        while (!(sensor.readDistance() < 3.0)) { 
35            /add implementation here 
36            e.g. robot moves forwards/ 
37        } 
38        //and stops motors afterwards 
39    } 
40}

8.2  Das Exception-Modell

Unter einer Ausnahme (engl. exception) versteht man in der Objektorientierten Programmierung das Auftreten bestimmter fehlerhafter Programmzustände. Bei ihrer Behandlung werden Informationen über diese Zustände strukturiert zur Laufzeit weitergegeben und gegebenenfalls an bestimmten Stellen im Programm darauf reagiert. Kann in einem Programm z.B. eine Speicheranforderung nicht erfüllt werden, wird eine Speicheranforderungsausnahme ausgelöst. Mit Hilfe von Exceptions können somit Fehler ignoriert, behoben oder angezeigt werden.

8.2.1  Grundprinzip

Das Grundprinzip des Exception-Modells in Java lässt sich wie folgt beschreiben [Kr\IeC {\"u}ger2014]:

Als Beispiel für Ausnahmen kann man das Öffnen einer Datei heranziehen. Das Öffnen einer Datei kann aus verschiedenen Gründen fehlschlagen (keine Rechte, Datei nicht vorhanden etc.). Deshalb muss der Programmierer explizit angeben, was geschehen soll, wenn eine Datei nicht geöffnet werden kann (siehe Programm 8.3).

Ausgenommen hiervon sind Ausnahmetypen, die jederzeit auftreten können, wie zum Beispiel Indexfehler bei Indizierung eines Arrays. Diese sind meistens die Folge von Fehlern im Programm, sodass es hier nicht sinnvoll ist, die Ausnahme zu behandeln (stattdessen sollten derartige Fehler behoben werden).

8.2.2  Der try-catch-Block

Um Funktionen, die eine Ausnahme erzeugen benutzen zu können, müssen diese durch einen try-catch-Block umschlossen sein. Im try-Teil wird die Funktion, deren Ausführung gewünscht ist, eingetragen. Es wird also versucht, die Funktion aufzurufen. Schlägt der Aufruf fehl, wird eine Ausnahme ausgelöst, die dazu führt, dass die Anweisungen im catch-Block ausgeführt werden. Zusätzlich gibt es die Möglichkeit noch einen finally- Block einzubauen. Unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht, werden die Anweisungen hier immer ausgeführt. Der Block eignet sich dazu, » Aufräumarbeiten« durchzuführen.

Eine Exception wird beispielsweise auch geworfen, wenn probiert wird einen Sensor zu initialisieren, obwohl er nicht am angegebenen Sensorport angeschlossen ist.


Programm 8.2: Sensorinitialisierung
1import lejos.hardware.port.SensorPort; 
2import lejos.hardware.sensor.EV3ColorSensor; 
3 
4public class TryCatch { 
5 
6    public static void main(String args[]) { 
7        EV3ColorSensor sensor = new EV3ColorSensor(SensorPort.S1); 
8        / further implementation / 
9    } 
10 
11}

Programm 8.2 führt ohne angeschlossenen Sensor dann zu folgender Ausgabe auf dem Display des EV3:

pict

Abbildung 8.2.: IllegalArgumentException bei nicht angeschlossenem Sensor

Um den Anwender des Programms nicht mit dieser sperrigen (und für ihn vielleicht unaussagekräftigen) Ausgabe zu konfrontieren, kann ein try-catch- Block verwendet und damit auf die Exception reagiert werden:


Programm 8.3: Verwendung von Try-Catch
 
1import lejos.hardware.Button; 
2import lejos.hardware.lcd.LCD; 
3import lejos.hardware.port.SensorPort; 
4import lejos.hardware.sensor.EV3ColorSensor; 
5 
6public class TryCatch { 
7 
8    public static void main(String args[]) { 
9 
10        try{ 
11            EV3ColorSensor sensor = new EV3ColorSensor(SensorPort.S1); 
12            / further implementation / 
13        } 
14        catch (IllegalArgumentException exc) { 
15            LCD.drawString("Sensor missing!", 0, 0); 
16            LCD.drawString("Plug it in and", 0, 1); 
17            LCD.drawString("restart program.", 0, 2); 
18            LCD.drawString("ESCAPE to exit", 0, 4); 
19            Button.ESCAPE.waitForPress(); 
20        } 
21 
22    } 
23}

Dies führt dann zu folgender Ausgabe:

pict

Abbildung 8.3.: Ausgabe des EV3 nach gefangener Exception

pict
Ein nicht angeschlossener EV3-Berührungssensor wird keine Exception werfen, da es sich bei ihm um einen analogen Sensor handelt. Das Programm läuft fehlerfrei und der Sensor liefert bei jeder Abfrage eine »0« für »nicht gedrückt«.

8.2.3  Throws Exception

Neben dem Strukturieren von problematischen Blöcken durch einen try-catch-Block gibt es eine weitere Möglichkeit auf Exceptions zu reagieren. Hierzu wird im Methodenkopf die sogenannte throws-Klausel eingeführt.

throws IOException, FileNotFoundException

Im Folgenden zu sehen in einer Methode, die eine Zeile aus einer angegebenen Datei als String zurückgeben soll.

String readLineFromData(String data) throws IOException { 
    RandomAccessFile d = new RandomAccessFile(data, "r"); 
    return d.readLine(); 
}

Durch throws gibt die Methode an, dass sie eine bestimmte Exception nicht selbst behandelt, sondern diese an die aufrufende Methode weitergibt. Ist es egal, wo die Fehlerbehandlung in einem Hauptprogramm behandelt wird, so können alle Fehler auch an die Laufzeitumgebung weitergeleitet werden, die dann das Programm im Fehlerfall abbricht. Dafür wird die main-Methode um eine Exception ergänzt.

throws Exception

Programm 8.4: Verwendung von throws in der main-Methode
 
1/∗∗ 
2 This class doesnt handle exceptions itself. 
3 It returns exceptions to runtime. 
4/ 
5public class  ThrowsEverything { 
6    public static void main(String args[]) throws Exception { 
7        //Implementation 
8    } 
9}

Im Gegensatz zur try-catch-Konstruktion, in der ganz konkret eine Ausnahmebehandlung für wenige Anweisungen implementiert wird, deutet throws Exception nur an, dass in dieser Methode generell eine Ausnahme auftreten könnte. Somit wird der try-catch-Block überflüssig.

Im ersten Programm (es handelt sich um genau zu sein um einen Programmauszug, da auf die Klassenbezeichnung und die main-Methode verzichtet wurde) wird der throws-Befehl zusammen mit der Angabe einer konkreten Exception verwendet (hier: FileNotFoundExecption). Es lassen sich beliebig viele durch Kommata getrennte Exceptions angeben.

Das zweite Beispielprogramm verdeutlicht die Weitergabe einer Fehlerbehandlung an die Laufzeitumgebung (Die main-Methode wurde um throws Exception erweitert.)

8.3  Threads

8.3.1  Nebenläufigkeit

Threads schaffen in Java die Möglichkeit, Nebenläufigkeit innerhalb eines Programmablaufs zu erzeugen. Nebenläufigkeit bezeichnet die Fähigkeit, mehrere Vorgänge gleichzeitig ausführen zu können (siehe Abbildung 8.4).

pict

Abbildung 8.4.: Programmablauf mit und ohne Threads

Bei einem Programm mit sequentiellem Ablauf werden alle Programmteile nacheinander durchlaufen. Der Ablauf könnte mit einem Stift nachvollzogen werden. Dabei befindet sich die Spitze des Stiftes zunächst im Programmkopf, durchläuft danach Schleifen und Verzweigungen und landet schließlich am Ende des Programms. Nachteil: Soll ein Roboter während der Ausführung für fünf Sekunden vorwärts fahren, muss das Programm für diesen Zeitraum angehalten werden. In dieser Zeit kann es aber keine anderen Aufgaben mehr erledigen. Ein einzelner Pfad reicht also in manchen Fällen nicht aus. Stattdessen müssen mehrere Pfade gleichzeitig durch das Programm führen. So können beispielsweise auf einem Pfad die Motoren bewegt werden, während auf einem weiteren der Notstopp-Knopf ständig geprüft wird. Die einzelnen Pfad werden als »Thread« bezeichnet.

Nehmen wir einen autonomen Sicherheitsroboter, der nachts über ein Werksgelände fährt und Einbrecher mittels verschiedener Sensoren aufspüren soll. Hier müssen Threads verwendet werden, um die verschiedenen Aufgaben gleichzeitig ausführen zu können. In diesem Fall laufen die Abfragen der Sensoren in jeweils einem eigenen Thread für jeden Sensor, damit immer alle aktuellen Sensordaten zur Verfügung stehen und z.B. die Informationen der normalen Kamera nicht verloren gehen, wenn gerade die Infrarotkamera abgefragt wird. Außerdem muss sich der Roboter zeitgleich noch bewegen können, da er ja über das Gelände patrouillieren soll. Mit mehreren Threads ist es also möglich, verschiedene Aufgaben innerhalb eines Programms gleichzeitig zu erledigen.

Dabei wird ein Thread als Verwalter eingesetzt, der andere Threads starten oder beenden kann. Das Programm 8.5 auf Seite 201 besteht aus zwei Klassen. Die Klasse Parent übernimmt die Rolle des Verwalters. In der main()-Methode der Klasse Parent, wird zunächst ein neues Objekt mit dem Namen child vom Typ Child erzeugt. Die nächste Anweisung startet den Thread, indem es auf dem Objekt child die Methode start() aufruft. Die Methode start()ist in der Klasse Thread implementiert, von der die Klasse Child erbt.

Nach dem Aufruf von start() wird die Ausführung an die in der Klasse Child implementierte Methode run() übergeben. Von diesem Zeitpunkt an laufen die beiden Threads (Eltern- und Kindthread) gleichzeitig und können parallel Aufgaben erledigen.

Typischerweise ist der Elternthread für die Mensch-Maschine-Interaktion auf der einfachsten Ebene (zum Beispiel beim Notstopp) verantwortlich, während andere Threads konkrete Aufgaben im Anwendungsbereich des Systems haben. Dies rührt daher, dass die Eingabe des Menschen höchste Priorität hat.

Im Programm 8.5 wartet der Elternthread nach dem Start des Kindthreads, bis die ESCPAE-Taste gedrückt wird, woraufhin das Programm beendet werden soll. In der Zwischenzeit sollen Im Kindthread die Werte eines Farbsensor zyklisch mit einer Sekunde Abstand ausgelesen und auf dem Display des EV3 ausgegeben werden. Dazu wird eine while-Schleife verwendet, die mittels Thread.currentThread().isInterrupted() den Zustand einer internen booleschen-Variable der Child-Klasse abfragt. Diese liefert nur dann true, falls zuvor die Methode child.interrupt() aufgerufen wurde (was nach Drücken der ESCAPE-Taste eintritt). Damit ist die Abbruchbedingung der while-Schleife erfüllt, diese wird beendet und die run()-Methode ist komplett durchlaufen. Das beendet den Kindthread und in diesem Fall auch das gesamte Programm.

Da die Farbwerte im Abstand von einer Sekunde ausgelesen werden sollen, muss der Kindthread nach jedem Durchlauf der while()-Schleife für eine Sekunde mittels Thread.sleep(1000) pausiert werden. Wenn nun aber in genau dieser Zeit die ESCAPE-Taste betätigt und damit child.interrupt() aufgerufen wird, muss der Kindthread seine Pause frühzeitig beenden, was Thread.sleep(1000) durch Werfen einer InterruptedException anzeigt. Diese Exception muss wie weiter oben beschrieben mit Hilfe eines try-catch-Blocks gefangen und behandelt werden. Im Beispiel wird die while-Schleife mittels break beendet und nochmals child.interrupt() aufgerufen, um die interne interrupted-Variable auf true zu setzen.


Programm 8.5: Eltern- und Child- Thread
 
1import lejos.hardware.Button; 
2import lejos.hardware.lcd.LCD; 
3import lejos.hardware.port.SensorPort; 
4import lejos.hardware.sensor.EV3ColorSensor; 
5/∗∗ 
6 This class starts a thread and interrupts it after Button ENTER is pressed. 
7/ 
8public class Parent { 
9    public static void main(String args[]) throws InterruptedException { 
10        Child child = new Child(); 
11        //start childthread 
12        child.start(); 
13        //wait until button ESCAPE is pressed 
14        Button.ESCAPE.waitForPress(); 
15        child.interrupt(); 
16    } 
17} 
18/∗∗ 
19 This class represents a Thread. 
20 It reads colorvalues and displays them as long as it isnt interrupted. 
21/ 
22class Child extends Thread { 
23 
24    public void run() { 
25        //declaration and definition of an EV3ColorSensor 
26        EV3ColorSensor sensor = new EV3ColorSensor(SensorPort.S1); 
27        sensor.setCurrentMode("RGB"); 
28        //array to store colorvalues in 
29        float[] color = new float[sensor.sampleSize()]; 
30 
31        while (!Thread.currentThread().isInterrupted()) { 
32            //read and store colorvalues in array 
33            sensor.fetchSample(color,0); 
34            //display colorvalues on LCD 
35            LCD.drawString("Red component: " + Float.toString(color[0]), 0, 0); 
36            LCD.drawString("Green component: " + Float.toString(color[1]), 0, 1); 
37            LCD.drawString("Blue component: " + Float.toString(color[2]), 0, 2); 
38            //wait for one second 
39            try {Thread.sleep(1000);} 
40            catch (InterruptedException intExc) { 
41                this.interrupt(); 
42                break; 
43            } 
44        } 
45    } 
46}

8.3.2  Parallelität

Wenn bisher von parallel ablaufenden Threads die Rede war, handelte es sich genau genommen um eine Scheinparallelität (oder auch virtuelle Parallelität). Aufgrund der Tatsache, dass der EV3 nur einen Prozessor hat, kann zu jedem Zeitpunkt immer nur eine Aufgabe ausgeführt werden. Aufgaben müssen daher grundsätzlich nacheinander ausgeführt werden, um eine quasi-parallele Ausführung von Aufgaben zu ermöglichen. Die Rechenzeit des Prozessors wird auf die ablaufenden Threads aufgeteilt. Für den Nutzer entsteht dadurch der Eindruck, dass alle Aufgaben gleichzeitig erledigt werden. Allerdings ist die Rechenleistung jedes Prozessors begrenzt. Laufen zu viele Threads gleichzeitig ab, kann auch eine quasi-Parallelität nicht mehr gewährleistet werden. Das Abfragen eines Sensors könnte unter Umständen derart verzögert werden, dass eine sinnvolle Auswertung der Sensorsignale nicht mehr möglich ist.

8.3.3  Scheduler

Um alle Threads gleichmäßig abzuarbeiten, gibt es intelligente Algorithmen, die den Threads die Rechenzeit des Prozessors zuteilen und nach einer gewissen Zeit wieder entziehen. Ein Programm, das einen solchen Algorithmus implementiert, wird Scheduler genannt. Zum Beispiel könnte er den verschiedenen Prozessen zyklisch die Rechenzeit des Prozessors zuteilen1. Dieses Verfahren wird »Round Robin« oder »Zeitscheibenverfahren« genannt.

Systemabhängig wird dabei jedem Thread für eine gewisse Zeit der Prozessor zur Verfügung gestellt. Während ein Thread gerade den Prozessor belegt, sind alle anderen angehalten. Wenn ein gestoppter Thread für das Auslesen eines Sensors zuständig ist, werden Veränderungen der Messwerte erst registriert, wenn dieser Thread wieder den Prozessor belegen darf. Treten Veränderungen nur sehr kurz auf, und liegen zufällig genau in dem Zeitrahmen, in dem der Thread unterbrochen ist, so werden diese gar nicht erfasst. In ungünstigen Fällen verursacht dieses Problem also ungenaue oder verzögerte Messergebnisse bei der Nutzung von Sensoren.

8.3.4  Garbage Collector

Es gibt einen weiteren verwaltenden Algorithmus: Den sogenannten Garbage Collector (englisch für: Müllabfuhr). Dieser Algorithmus ist dafür verantwortlich, nicht mehr benötigten Speicher wieder freizugeben. Dadurch wird sichergestellt, dass Programmteile, die nicht mehr benötigt werden, den Speicher nicht sinnlos belasten und die so freigemachten Ressourcen den noch laufenden Threads zur Verfügung gestellt werden können. Dies ist besonders interessant, wenn mit mehreren Threads gearbeitet wird. Sollte ein Thread nicht richtig beendet werden, so belegt er weiter Systemressourcen, obwohl er nicht mehr benötigt wird. Diese Programmreste werden dann automatisch nach einer gewissen Zeit vom Garbage Collector eingesammelt (damit sie keinen Speicherplatz mehr verbrauchen ). Der Garbage Collector kann aber auch manuell vom Entwickler aufgerufen werden, um zu bestimmten Zeiten für einen bereinigten Speicher zu sorgen.

1 Die Implementierung eines Schedulers (bzw. zeitlich gesteuerten Ablaufs) wird zum Teil von den Java- Klassen java.util.Timer und java.util.TimerTask übernommen.