Dies ist eine alte Version des Dokuments!
In Java läuft nichts ohne objektorientiertes Programmieren. Es hilft euch nicht nur, euren eigenen Code in übersichtliche Einheiten zu strukturieren - auch viele Bibliotheken stellen ihre Funktionalität in Form von Klassen zur Verfügung - mit diesen müsst ihr arbeiten, um von der darin verpackten Intelligenz zu profitieren…
Wir werden das alles am Beispiel eines zweirädrigen Roboters durchexerzieren…
In einer Klassendefinition legen wir fest, welchen Namen diese funktionelle Einheit hat, über welche Informationen sie verfügt und was sie alles tun kann.
Wir werden hier erstmal eine leere „Roboter“ Klasse anlegen, die wir nach und nach mit Leben füllen:
// das Stichwort "class" leitet die Definition einer Klasse ein. Danach folgt ihr Name ("Robot") class Robot { // hier (in den "Klassenrumpf") kommen gleich die Variablen und Methoden der Klasse rein. }
Nachdem eine Klasse definiert ist, kann ihr Name genau wie einer der eingebauten Datentypen (int, float, etc.) verwendet werden.
Um im Programm eine Variable, die einen ganzen Roboter (und nicht nur eine einzelne Zahl) speichert anzulegen könnnen also schreiben:
Robot myRobot; // Deklariere die Objektvariable "myRobot" vom Typ "Robot"
Eine Klasse kann sogenannte Membervariablen enthalten, die Informationen über die durch sie repräsentierte funktionelle Einheit speichern. Sie werden einfach im Stile einer Variablendeklaration in den Klassenrumpf (also zwischen die geschweiften Klammern) geschrieben.
Mit dem folgenden Code geben wir dem Roboter ein Gedächtnis für seine Lage im Raum (x- und y-Koordinaten sowie Winkel zur x-Achse):
class Robot { // Deklaration der Klasse Robot float posX = 100; //Die x-Position des Roboters in Pixel float posY = 100; //Die y-Position des Roboters in Pixel float direction= PI; //Die Richtung in die der Roboter schaut, in Bogenmaß, gegen den Uhrzeigersinn, relativ zur X-Achse. // (PI => der Roboter schaut nach links) }
Schön wäre auch, wenn wir den Roboter mit einem einzigen Funktionsaufruf ein kleines Stück nach vorne gehen lassen könnten.
Das erreichen wir, indem wir der Klassendefinition eine entsprechende Methode (Funktion) hinzufügen:
class Robot { // Deklaration der Klasse Robot float posX = 100; //Die x-Position des Roboters in Pixel float posY = 100; //Die y-Position des Roboters in Pixel float direction= PI; //Die Richtung in die der Roboter schaut //Die moveForward-Funktion verändert die Position so, wie es einen Schritt nach vorne entspricht. void moveForward(float distance){ posX = posX+cos(direction)*distance; // Innerhalb der Funktion kann direkt auf die Klassenvariablen posY = posY+sin(direction)*distance; // zugegriffen werden. (Sie sind im Objekt quasi "lokal") } }
Klassenmethoden funktionieren genauso wie Funktionen:
Mit unserer Klassendefinition haben wir beschrieben, welche Informationen ein Roboter grundsätzlich enthält und was er tun kann. Diese Beschreibung bietet praktisch eine Schablone, mit der wir in unserem Programm eine beliebige Anzahl von konkreten Manifestationen (Objekte genannt) erzeugen können.
Im Gegensatz zur Deklaration von Variablen mit „primitivem“ Datentyp wie „int“ oder „float“ müssen Objekte explizit erzeugt werden:
// Deklariere die Objektvariable "myRobot" vom Typ "Robot" (die danach erst einmal leer ist) Robot myRobot; myRobot = new Robot(); // erzeuge ein Objekt com Typ "Robot" und speichere eine Referenz darauf in der "myRobot" Variable
Bei Arduino (C++) ist das anders: hier wird bereits bei der Deklaration eines Objekts eine Instanz erzeugt!
Unser frischgebackenes Roboterobjekt können wir jetzt verwenden, um einen Roboter in Bewegung zu simulieren.
Die entsprechende Funktion rufen wir auf, indem wir an den Namen des Objekts einen Punkt anhängen:
myRobot.direction=myRobot.direction+PI/2; // drehe den Roboter um 90° gegen den Uhrzeigersinn myRobot.moveForward(1); // sag ihm, dass er eine Bewegung um einen Schritt nach vorne simulieren soll
Da jedes Objekt seine eigenen Membervariablen enthält, können wir ganz einfach mehrere Roboter erzeugen und in unterschiedliche Richtungen fahren lassen:
Robot robotOne=new Robot(); // ein erster Roboter Robot robotTwo=new Robot(); // ein zweiter Roboter robotOne.direction=PI; // der erste Roboter schaut nach links robotTwo.direction=0; // der zweite Roboter schaut nach rechts
So kann z.B. die moveForward
Methode genau die Positionsvariablen des Objekts verändern, mit dem sie aufgerufen wurde:
robotOne.moveForward(1); // verändert die Variablen robotOne.posX und robotOne.posY robotTwo.moveForward(1); // verändert die Variablen robotTwo.posX und robotTwo.posY
Wenn ihr eine Variable mit einem Klassentyp deklariert (also z.B. unseren Roboter), dann wird in dieser Variable nicht etwa „das Objekt selbst“ (also ein Wert) gespeichert, sondern nur „welches Objekt mit dieser Variable gemeint ist“ (also eine Speicheradresse).
In der Praxis macht das insbesondere bei Zuweisungen von Variablen einen Unterschied:
//Primitive Variablen enthalten einen Wert, der bei Zuweisungen kopiert wird: int a=1; int b=2; a=b; // Der Inhalt von b (2) wird in a kopiert. println(a); // Ausgabe "2" println(b); // Ausgabe "2" b=3; // Der Inhalt von b wird mit "3" überschrieben, a bleibt davon unberührt. println(a); // Ausgabe "2" (unverändert) println(b); // Ausgabe "3" //Klassenvariablen enthalten eine Referenz auf ein Objekt: Robot robotOne=new Robot(); // ein erster Roboter Robot robotTwo=new Robot(); // ein zweiter Roboter robotOne.direction=PI; // der erste Roboter schaut nach links robotTwo.direction=0; // der zweite Roboter schaut nach rechts robotOne=robotTwo; // RobotOne zeigt auch jetzt auf das Objekt, auf das vorher nur RobotTwo gezeigt hat. // Das Objekt, auf das robotOne vorher gezeigt hat, wird vergessen. (und vom Garbage Collector entsorgt) robotTwo.direction=PI/2; // Ändere die Richtung des Objekts, auf das die Referenz in robotTwo zeigt. println(robotOne.direction); // Da beide Variablen das gleiche Objekt referenzieren, kommt auch hier PI/2 heraus!
Bei der Benennung von Methoden sollte man sich an zwei grundsätzliche Konventionen halten:
Unsere Klasse bekommt zwei weitere Methoden, eine um den Roboter an seiner aktuellen Position zu zeichnen und eine um den Roboter um einen gewissen Winkel zu drehen.
class Robot { // Deklaration der Klasse Robot int dRobot = 50; //Der Durchmesser des Roboters in Pixel float direction = PI; //Die Richtung in die der Roboter schaut //(PI (links), da der Winkel in Bogenmaß angegeben wird) float posX = 100; //Die x-Position des Roboters in Pixel float posY = 100; //Die y-Position des Roboters in Pixel //Diese Methode zeichnet einen Roboter //an seiner aktuellen Position und Ausrichtung. void drawRobot(){ //"void" bedeutet dass es keinen Rückgabewert gibt // Zeichne einen Kreis an der Position des Roboters. ellipse(posX,posY,dRobot,dRobot); // Zeichne eine Linie welche die Orientierung des Roboters angibt. line(posX,posY,posX+cos(direction)*50, posY+sin(direction)*50); } //Die moveForward-Funktion verändert die Position so, wie es einen Schritt nach vorne entspricht. void moveForward(float distance){ posX = posX+cos(direction)*distance; // Innerhalb der Funktion kann direkt auf die Klassenvariablen posY = posY+sin(direction)*distance; // zugegriffen werden. (Sie sind im Objekt quasi "lokal") } //Diese Methode dreht den Roboter um einen gewissen Winkel //welcher von "winkelaenderung" vorgegeben wird. // Die Methode bekommt einen Übergabeparameter vom Typ "float". void turnRobot(float angle){ this. direction += angle; } }
Jetzt können wir die fertige Klasse verwenden. Um dies in Processing auf eine möglichst übersichtliche weise zu tun sollte ein neuer Tab geöffnet werden:
In diesem neuen Tab speichert ihr eure Klasse.
Klassen geben den Rahmen vor für unsere Objekte. Wie ihr bereits kennengelernt habt, kann man aus einer Klasse ein Objekt erzeugen durch den „new“ -Operator. Ein Programm welches in Processing geschrieben wurde kann die „Robot“-Klasse z.B. folgendermaßen einsetzen:
//Erstellt ein global verfügbares Objekt vom Typ "Robot", //wie "int" oder "double", und geben ihm den Namen "myRobot". Robot myRobot; void setup() { // Festlegen der Fenstergröße size(500,500); // Festlegen der Hintergrundfarbe background(235); //Initialisiert das "Robot" Objekt myRobot = new Robot(); } void draw() { //Zu beginn jeder draw-Iteration wird das Fenster zurückgesetzt. background(235); //Das Robot-Objekt verfügt über eine Funktion die den Roboter malt. myRobot.drawRobot(); //Diese Funktion bewegt den Roboter ein wenig. myRobot.moveRobot(); //Dieser Funktionsaufruf sorgt dafür das der Roboter nach jeder translation //um PI/200 gedreht wird. myRobot.turnRobot(PI/200); }
In diesem Abschnitt ist das Beispiel von oben nochmals aufgeführt in einer „best-practise“-Version. Hierbei ändern sich zwei wesentliche Dinge:
class Robot { // Deklaration der Klasse Robot /** Der Durchmesser des Roboters. */ int dRobot; /** Die Richtung in die der Roboter schaut. */ float winkel; /** Die x-Position des Roboters. */ float posX; /** Die y-Position des Roboters. */ float posY; /** * Der "default"-Konstruktor erstellt ein Roboter-Objekt mit Standardwerten für alle notwendigen Parameter. */ public Robot() { this.dRobot = 50; this.winkel = PI; this.posX = 100; this.posY = 100; } /** * Dieser Konstruktor erstellt ein Roboter-Objekt mit allen notwendigen Parametern, * entsprechend der Vorgabe durch die Eingabeparameter. */ public Robot(int dRobot, float winkel, float posX, float posY) { this.dRobot = dRobot; this.winkel = winkel; this.posX = posX; this.posY = posY; } /** * Diese Methode zeichnet einen Roboter an seiner aktuellen Position und Ausrichtung. */ void drawRobot(){ // Zeichne einen Kreis an der Position des Roboters. ellipse(posX,posY,dRobot,dRobot); // Zeichne eine Linie welche die Orientierung des Roboters angibt. line(posX,posY,posX+cos(winkel)*50, posY+sin(winkel)*50); } /** * Diese Methode bewegt einen Roboter ein kleines Stück in die Richtung in die er gerade schaut. */ void moveRobot(){ this.posX = this.posX+cos(this.winkel)*2; this.posY = this.posY+sin(this.winkel)*2; } /** * Diese Methode dreht den Roboter um einen gewissen Winkel welcher von "winkelaenderung" vorgegeben wird. */ void turnRobot(float winkelaenderung){ this.winkel = winkel + winkelaenderung; } }
Variablen welche stellvertretend für ein Objekt verwendet werden, z.B. myRobot aus dem vorherigen Bsp., stellen lediglich eine Referenz auf das Objekt dar. Man kann die Variable also auch als die Speicheradresse des Objektes verstehen. Dieser Sachverhalt ist insbesondere dann von Bedeutung wenn ein Objekt kopiert werden soll. Die „best-practise“ ist das Anlegen eines „copy“-Konstruktors in jeder Klasse, dieser überträgt alle Attribute des zu kopierenden Objektes in ein neues Objekt:
class DummyBean { String dummy; int yummyDummy; DummyBean(DummyBean dummyBeanToCopy) { this.dummy = dummyBeanToCopy.dummy; this.yummyDummy = dummyBeanToCopy.yummyDummy; } }
Und können diesen Konstruktor entsprechend aufrufen:
//A Object of my DummyBean class DummyBean myFirstDummy = new DummyBean(); //make a copy of the myFirstDummy object DummyBean mySecondDummy = new DummyBean(myFirstDummy);
Kopiert man Objekte nicht auf diese Weise sondern durch:
//A Object of my DummyBean class DummyBean myFirstDummy = new DummyBean(); //silly copy DummyBean mySecondDummy = myFirstDummy;
, dann wirken sich Änderungen an „myFirstDummy“ auch auf „mySecondDummy“ aus da beide Variablen auf das selbe Objekt verweisen.
In Java kann unter Vererbung eine Spezialisierung einer bestehenden Klasse verstanden werden. Wir unterscheiden dazu in Super- und Subklassen. Die Superklasse (oder Elternklasse) beinhaltet eher allgemeine Attribute und Methoden. Die Klasse „Stift“ könnte z.B. Attribute für Stiftlänge, Stiftfarbe, usw. enthalten. Die Subklasse bekommt von der Superklasse alle Attribute und Methoden vererbt, erweitert dieses Set aber um eigene Attribute und Methoden. Z.B. könnte die Subklasse „Kugelschreiber“ das Attribut Stift eingefahren/Stift ausgefahren hinzufügen und eine Methode die diesen Zustand verändert.
Eine ausführliche Beschreibung wie Vererbung funktioniert findet ihr hier: http://www.java-tutorial.org/vererbung.html