Klassen
Im Kleinen (innerhalb einer Klasse) organisiert man Code mit Methoden. Im Grösseren (in einem Programm/Projekt) organisiert man Code mit Klassen.
MATERIALIEN
In der Programmiersprache Java steht Code zwingend in einer Klasse, Sie haben also im bisherigen Verlauf des Kurses ständig Klassen benutzt. Für grössere Projekte wird es zunehmend wichtiger, auch zu verstehen, was eine Klasse ist.
Klassen verstehen
Rein praktisch gesehen könnte man sagen: Eine (Unter-)Klasse ist das, was in der Greenfoot-Oberfläche rechts neben der Welt als Kasten angezeigt wird, also was den Code enthält (Doppelklick), respektive was im Editor durch die grün hinterlegte Klammer um (fast) alles ausgezeichnet wird.
Für das Verständnis wichtiger ist allerdings die folgende, etwas theoretischere Antwort: Eine (Unter-)Klasse ist die grundlegende Organisationseinheit des objektorientierten Programmierens (OOP). Die Grundidee ist, dass eine Klasse so etwas wie ein künstliches Objekt (eine sogenannte Entität) darstellt und den für das jeweilige Objekt wichtigen Code beinhaltet. Jedes Objekt erhält dadurch Eigenschaften (=Instanzvariablen) und Funktionsweisen (=Methoden), ähnlich wie reale Objekte ja auch Eigenschaften (z.B. eine Farbe, ein Gewicht, u.ä.) und Funktionsweisen (z.B. kann fahren, kann sich erwärmen, u.ä.) haben. Im eigentlichen Programm interagieren diese Objekte mit dem Benutzer oder untereinander durch das Ausführen von Methoden oder das Zuweisen neuer Werte für die Instanzvariablen.
Vielleicht haben Sie bemerkt, dass die obenstehende Erklärung bei Klassen (die den Code enthalten) anfängt und bei Objekten/Instanzen (die interagieren) aufhört. Doch genau genommen ist das nicht dasselbe:
Klasse vs. Instanz
Beim objektorientierten Programmieren wird der zu einer Software gehörende Code auf verschiedene Module, sogenannte «Klassen» verteilt. Üblicherweise entspricht eine Klasse einer Datei, die den entsprechenden Programmiercode enthält.
Genau genommen gibt es meist zwei Varianten dieser Datei – die eine enthält Text, nämlich genau den vom Programmierer formulierten Code in einer höheren Programmiersprache wie Java (z.B. kara.java), die andere enthält eine ausführbare Version desselben Codes in der Maschinensprache des Prozessors (z.B. kara.class). Den (automatisierten) Übersetzungsprozess, der die ausführbare Version erzeugt, nennt man compilieren.
Im Programmcode wird die Funktionalität einer Klasse definiert, also welche Instanzvariablen (auch Eigenschaften, Attribute oder Felder genannt) sie kennt und welche Methoden (auch Fähigkeiten oder Funktionen genannt) sie besitzt. An dieser Stelle geht es aber wirklich nur um die Definition – um den Code tatsächlich auszuführen, braucht es eine Instanz.
Eine Instanz (engl: object) ist eine konkrete Umsetzung der in der Klasse festgelegten Definitionen. Nur in einer Instanz kann eine Variable einen bestimmten Wert annehmen oder eine Methode ausgeführt werden.
Der Aufwand zur Erstellung einer Instanz ist minimal, man muss lediglich den Konstruktor aufrufen. Andererseits ergibt sich aus der Unterscheidung zwischen Klasse und Instanz ein wesentlicher Vorteil: Es können beliebig viele Instanzen von ein und derselben Klasse erstellt werden. Multiple Instanzen teilen sich natürlich die grundlegende Definition ihrer Funktionalität, sie sind aber dennoch nicht genau gleich: Weil sich die Werte der jeweiligen Instanzvariablen unterscheiden (können), ergeben sich auch Unterschiede in der Ausführung des Programmcodes – etwa so, wie in Massenproduktion hergestellte Waren zwar exakte Kopien voneinander sind, aber doch bei unterschiedlichen Besitzern landen und auf verschiedene Weisen benutzt werden.
Die inhärenten Vorteile der zumindest potentiellen Massenproduktion haben dazu geführt, dass die meisten modernen Programmiersprachen (mindestens optional) objektorientiert sind.
Daraus ergibt sich eine weitere Antwort: Eine Klasse ist der «Bauplan», bzw. die Definition für konkrete Instanzen (engl. objects); im Normalfall kann mit einer Klasse nicht direkt interagiert werden sondern nur mit den (beliebig vielen) aus einer Klasse abgeleiteten Instanzen. (Die Ausnahme sind statische Klassen und Methoden, s. weiter unten.)
1. Konstruktor
Konstruktor aufrufen
Um aus einer gegebenen Klasse eine konkrete Instanz zu erstellen (=instanziieren), muss man einen Konstruktor aufrufen, und zwar so:
new Klassenname();
//wenn statt "Klassenname" bspw. "Joe" steht, dann wird eine neue Joe-Instanz erschaffen, z.B.
Joe joe17 = new Joe(); //hier wird die neue Instanz gleich noch unter dem Variablennamen joe17 abgespeichert
Durch den Aufruf des Konstruktors wird also eine neue Instanz der Klasse erschaffen und zurückgegeben – wie üblich kann man diesen Rückgabewert in einer Variablen speichern oder direkt als Übergabewert in einem Methodenaufruf verwenden.
Aufgabe
Sie hatten das mit dem Kara-Szenario schon mal gemacht: im Kontextmenü jeder Klasse findet sich der Eintrag new Klassenname()
, einen neuen Joe kann man also mit new Joe()
erzeugen und die erzeugte Instanz kann man dann mit der Maus in der TurtleWorld platzieren.
Woher aber kommt der eine Joe, der beim Öffnen des Szenarios schon existiert? Er wird von TurtleWorld erschaffen und eingefügt, und zwar mit diesem Befehl:
addObject(new Joe(), 400, 300);
Da wir gerade dabei sind, schlagen Sie doch gleich die Methode addObject()
in der Dokumentation der Klasse World nach. Überprüfen Sie dabei gleich, dass Sie inzwischen wissen, wie genau die in einer Dokumentation/API angegebenen Informationen zu verstehen sind. addObject()
ist also dafür zuständig, die neue Instanz in der Welt zu platzieren – new Joe()
erschafft die neue Instanz, die dann platziert wird. Deutlicher wird das, wenn wir den obigen Code auf zwei Zeilen verteilen:
Joe joe1 = new Joe(); //neue Instanz erschaffen und in joe1 speichern
addObject(joe1, 400, 300); //joe1 in der Welt platzieren
Mini-Aufgabe: Sorgen Sie dafür, dass gleich zu Beginn (bzw. bei jedem Reset) insgesamt drei verschiedene Joes an drei verschiedenen Orten in der Welt platziert werden.
Erweiterung: Noch eine vierte Joe-Instanz soll platziert werden, und zwar an einen (bei jedem Reset neu) zufällig gewählten Ort innerhalb der Welt.
Konstruktor anpassen
Zum Aufrufen des Konstruktors genügt also das Schlüsselwort new und der Name der Klasse. Der Konstruktor selbst, also das, was aufgerufen wird, ist eigentlich nur eine Methode (innerhalb der Klasse), mit zwei Besonderheiten:
- Die Konstruktor-Methode hat denselben Namen wie die Klasse, zu der dieser Konstruktor gehört (also ausnahmsweise gross geschrieben!).
- Beim Konstruktor fehlt der Typ des Rückgabewerts, da per Definition die neu erschaffene Instanz zurückgegeben wird.
Joes Konstruktor sähe beispielsweise so aus:
public Joe(){...}
Wenn in der Klasse kein spezieller Konstruktor definiert ist, oder wenn der Konstruktor leer ist (so wie oben), dann passiert sonst nichts. Es ist allerdings möglich – und in manchen Fällen praktisch – Code in den Körper des Konstruktors zu schreiben. Dieser Code wird dann genau einmal ausgeführt, nämlich während der Erschaffung der Instanz.
Wozu das gut ist? Schauen Sie sich mal an, wo genau der Code steht, mit dem ein neuer Joe in die Welt gesetzt wird: im Konstruktor der TurtleWorld. Wenn also ganz zu Beginn (nach dem Kompilieren, bzw. nachdem der Benutzer auf «Reset» gedrückt hat) eine Welt erschaffen wird, kann sich diese – durch den Code in ihrem Konstruktor – selbst definieren (Grösse, Farbe, etc) und bevölkern. Das soll natürlich nicht in jeder Runde wieder passieren, deshalb gehört dieser Code in den Konstruktor, und nicht in die act()-Methode.
Sorgen Sie dafür, dass die in der vorherigen Aufgabe kreierten drei oder vier Joes jeweils eine zufällig gewählte Strichfarbe zugewiesen bekommen. Schreiben Sie dazu den Default-Konstruktor hin (s. oben) und fügen Sie die entsprechende Zeile Code (festlegen einer zufällig gewählten Farbe) in den Methodenkörper ein.
Konstruktor mit Übergabewerten
Ein Konstruktor kann, genau wie jede andere Methode, Übergabewerte haben. Ein gutes Beispiel ist die Klasse GreenfootImage, in deren Dokumentation finden Sie gleich vier unterschiedliche Konstruktoren:
Eigentlich macht das Sinn: Wenn ich ein neues Exemplar eines GreenfootImage erschaffe, dann will ich normalerweise gleich das Erscheinungsbild festlegen, oder zumindest die Grösse. Da es diese Konstruktoren gibt, kann ich die entsprechenden Informationen gleich in die Klammern hinter new GreenfootImage
packen.
Hinweis: Mehrere Konstruktoren in einer Klasse zu haben ist übrigens kein Problem, so lange sie sich in Anzahl und/oder Typ der erwarteten Übergabewerte unterscheiden – anhand von Anzahl und Typ der im Aufruf übergebenen Werte wird automatisch der richtige ausgewählt. Dasselbe gilt für normale Methoden; es gibt sogar einen Fachbegriff dafür: Man nennt das «überladen» einer Methode.
Ändern Sie Joes Konstruktor aus der letzten Aufgabe so ab, dass er die Übergabe einer Stiftfarbe erwartet und diese anwendet. Ändern Sie auch die Aufrufe der Joe-Konstruktoren entsprechend, so dass schon bei der Instanziierung festgelegt wird, welches ein blauer und welches ein roter Joe ist.
2. Mit Instanzen interagieren
Wenn man eine Methode (oder eine Variable) benutzen will, braucht man eigentlich nur ihren Namen zu nennen. Bei genauerer Betrachtung stellt sich das allerdings als Sonderfall heraus: Da es (potentiell) sehr viele Instanzen einer Klasse gibt, ist eigentlich gar nicht klar, wer genau sich drehen soll, wenn ich z.B. die turn()
-Methode aufrufe. Trotzdem funktioniert das aufgrund der eingebauten Annahme, dass die aufgerufene Methode zu derjenigen Instanz gehört, deren Code gerade abgearbeitet wird und der den Aufruf turn(10)
enthält. Wenn man möchte, kann man das auch explizit machen, indem man this.turn(10)
schreibt – das ist aber fast immer optional.
Vorausgesetzt, dass ihr Name bekannt ist, kann man auch jede andere Instanz drehen – indem man den Instanznamen vor den Methodenaufruf schreibt, z.B. joe17.turn(10);
Rufen Sie TurtleWorld auf, mit der Sie schon bei den vorherigen Aufgaben gearbeitet haben:
- Sorgen Sie dafür, dass die Welt sich die platzierten Joe-Instanzen merkt (als Instanzvariablen!).
- Auch in einer World kann es eine act()-Methode geben – fügen Sie eine ein.
- Sorgen Sie innerhalb der act()-Methode der Welt dafür, dass sich eine der Joe-Instanzen immer um 10 Grad dreht – eine andere soll sich immer um 3 Pixel vorwärts bewegen.
3. Modifikatoren
Das Aufrufen von Methoden anderer Instanzen funktioniert nur dann, wenn der Modifikator im Kopf der entsprechenden Methode public ist – andernfalls (private) ist diese Methode vor dem Zugriff von aussen geschützt.
Für sauberen Code sollte man eigentlich fast alle Modifikatoren (inkl. derjenigen für Instanzvariablen) auf private setzen und nur diejenigen öffentlich (public) lassen, auf die man auch wirklich externen Zugriff benötigt. Durch die Einschränkung (potentieller) Interaktionsmöglichkeiten reduziert sich das Risiko für Fehlfunktionen und Bugs.
4. Kommunikation zwischen Klassen bzw. Instanzen
Eigentlich ist es überhaupt nicht schwer, eine Methode einer anderen Instanz aufzurufen (s. oben) – allerdings geht das nur, wenn besagte Instanz bekannt ist. Wie also kommt man an einen Instanznamen, den man vor den Methodenaufruf schreiben kann?
a) Manche Greenfoot-Methoden liefern eine Instanz als Rückgabewert, z.B. getOneIntersectingObject()
. Das ist sehr praktisch, denn auf diese Weise kann man die Instanz, mit der es den Zusammenstoss gab, gleich adressieren.
Zur Verdeutlichung hier ein Codebeispiel aus einem Shooter-Game. Der Code steht innerhalb der act()-Methode von Bullet, er sorgt dafür, dass getroffene Target-Instanzen entfernt werden:
Actor target = getOneIntersectingObject(Target.class); //falls sich die Kugel mit einem Target überschneidet, wird die entsprechende Instanz zurückgegeben und in der Variable target gespeichert
if (target != null){ //testen, ob ein Target getroffen wurde
//(andernfalls enthält die Variable den Wert null)
getWorld().removeObject(target); //die Target-Instanz entfernen, durch Aufruf
//der entsprechenden Methode der Welt
}
Hinweis: Auch die im obigen Beispiel benutzte Methode getWorld()
gibt eine Instanz zurück, in diesem Fall die Welt, in der dieser Actor «lebt». Das ist sehr praktisch, weil man so aus jedem Actor heraus auf Methoden (oder Instanzvariablen) der Welt zugreifen kann.
b) Wenn man sich nicht auf praktische Greenfoot-Methoden verlassen kann oder will, um mit einer anderen Instanz zu kommunizieren, dann muss man die Verbindung selbst herstellen. Die üblichste Methode dafür ist, Instanz A als Übergabewert an Instanz B zu schicken, am besten gleich im Konstruktor.
Aufgabe
Im Szenario Kommunikation.zip finden Sie drei verschiedene Beispiele für eine solche Verbindung zwischen Klassen.
Ausprobieren: Versuchen Sie zunächst nachzuvollziehen (beachten Sie die Kommentare), wie es die drei farbigen Knöpfe jeweils schaffen, eine Nachricht an das MessageBoard zu schicken. Überprüfen Sie Ihr Verständnis dann, indem Sie einen neuen Actor erstellen (z.B. OrangeDot), der ebenfalls eine Nachricht an MessageBoard schickt, wenn er angeklickt wird. Suchen Sie sich dafür eine der drei Methoden aus – oder probieren Sie gleich alle drei aus.
Erweiterung: Fügen Sie noch einen Plus- und einen Minus-Knopf ein, mit denen man die Schriftgrösse der von MessageBoard angezeigten Nachricht beeinflussen kann.
5. Statische Klassen und Methoden
Weiter oben wurde gesagt, dass man mit Klassen nicht direkt interagieren kann, sondern nur mit Instanzen.
Mindestens eine Ausnahme von dieser Regel kennen Sie bereits:
Greenfoot.getRandomNumber(100);
Zwar benutzt man auch hier die Punktsyntax, aber Greenfoot ist der Name der Klasse, nicht eine Instanz dieser Klasse. Trotzdem funktioniert es hier, und zwar weil es sich um eine statische Klasse, bzw. Methode handelt:
Dieser Auszug aus der Klassendokumentation zeigt die Besonderheit: An der Modifikator-Stelle steht hier static
. Das bedeutet vor allem, dass in diesem Fall keine Unterscheidung zwischen Klasse und Instanz gemacht wird – man kann diese Methode direkt über die Klasse aufrufen. Deswegen ist der Name auch gross geschrieben.
Hinweis: Auch System
ist eine oft gebrauchte statische Klasse (z.B. System.out.println()
), dasselbe gilt für Math
(z.B. Math.sqrt()
).
6. Vererbung
Beim ObjektOrientierten Programmieren wird also der Code über Klassen verteilt. Dabei stellt sich häufig heraus, dass verschiedene Klassen bestimmte Eigenschaften (=Instanzvariablen) und Fähigkeiten (=Methoden) teilen, sich in anderen jedoch unterscheiden. Damit sich das effizienter organisieren lässt, gibt es das Prinzip der Vererbung:
Vererbung
Beim ObjektOrientierten Programmieren (OOP) ist jede Klasse eine spezialisierte Version einer Oberklasse. Das bedeutet, dass ihr nicht nur die eigenen Eigenschaften (= Instanzvariablen) und Fähigkeiten (= Methoden) zur Verfügung stehen, sondern auch diejenigen all ihrer Oberklassen.
Auf diese Weise lässt sich Code effizient über verschiedene Ebenen einer Klassenhierarchie verteilen, weil allgemeiner, für mehrere Unterklassen geltender Code nur einmal (in der Oberklasse) definiert werden muss. So erspart man sich u.a. das aufwändige und fehleranfällige Kopieren von Code in verschiedene Klassen – und jede Klasse muss nur die Dinge definieren, die sie speziell machen.
Analogie
Eine ähnliche Organisationsform findet sich bei biologischen Taxonomien. So haben beispielsweise Lebewesen bestimmte, sehr allgemeine Eigenschaften und Fähigkeiten, Unterklassen wie Pflanzen oder Tiere etwas speziellere, die dann in weiteren Unterklassen (Vögel, Säugetiere, Fische, usw.) noch spezifischer ausdifferenziert werden.
Ein ganz bestimmtes Lebewesen (z.B. mein Haustier Struppi) ist dann beispielsweise eine «Instanz» von Zwergterrier, hat aber auch all die Eigenschaften und Fähigkeiten von Hund, Raubtier, Säugetier und Lebewesen.
Eigentlich wissen Sie das schon: Wenn man Programmcode in einen speziellen Actor – z.B. Joe – schreibt, dann kann man alle Methoden von Actor einfach so benutzen, als wären sie direkt in Joe definiert. Die Eigenschaften und Fähigkeiten der Unterklasse Joe setzen sich also zusammen aus allen Instanzvariablen und Methoden, die in Joe selbst stehen, plus diejenigen, die in einer Elternklasse definiert sind. Hier steht nicht die Elternklasse, weil das Vererben über alle Ebenen der Klassenhierarchie funktioniert – genau genommen erbt Joe von Turtle, das wiederum von Actor erbt. Die Klassenhierarchie entspricht dem Schaubild ganz rechts in der Greenfoot-Oberfläche, die Pfeile zeigen an, welche Klasse von welcher erbt.
Die (organisatorische) Frage, wann es sich lohnt, eine neue Unterklasse zu erstellen, ist übrigens nicht einfach zu beantworten – in grösseren Softwareprojekten sind spezielle «Systemarchitekten» genau dafür zuständig. Grob gesagt geht es darum, in verschiedenen Klassen nicht denselben Code zu haben, und das löst man, indem man den von mehreren Klassen benötigten Code in eine gemeinsame Elternklasse verschiebt.
Am Beispiel des Joe-Szenarios: In der Klasse Actor sind grundlegende Methoden für alle Actors definiert, z.B. für das Bewegen oder Drehen. In der Klasse Turtle stehen verschiedenen Methoden, die sich um das Verwalten von Farben und das Zeichnen einfacher Figuren kümmern. In der Klasse Joe (oder anderen Unterklassen von Turtle) kann man dann Methoden schreiben, die – durch Nutzung der in den Oberklassen definierten Methoden – kompliziertere Figuren zeichnen. Eine Turtle IST also ein Actor, aber ein spezieller, denn sie hat zusätzlich zu denen des Actors noch die von ihr selbst definierten Eigenschaften und Fähigkeiten. Ein Joe wiederum IST ein Turtle (und damit auch ein Actor), kann aber noch etwas mehr – eben genau das, was der eigene Programmcode definiert.
Casting
Sie wissen bereits: Variablen haben einen Typ, der bestimmt, welche Arten von Werten sie speichern können. Zu Beginn hatten wir es nur mit simplen Variablentypen zu tun – z.B. int, char oder boolean. Inzwischen sollte klar geworden sein, dass auch komplexere Objekte, beispielsweise Instanzen, in Variablen gespeichert werden können – der zugehörige Typ ist dann einfach die entsprechende Klasse, z.B.:
World myWorld = getWorld(); //oder
Joe joe1 = new Joe();
Nun gibt es manchmal den Fall, dass man den Typ einer Variablen ändern will. Ein einfaches Beispiel:
int zahl = 3;
int haelfte = zahl/2;
System.out.println("Die Hälfte von " + zahl + " ist " + haelfte);
Wenn Sie diesen Code ausprobieren, werden Sie feststellen, dass der Computer behauptet, die Hälfte von 3 sei 1. Der Grund ist der folgende: Eine Ganzzahl geteilt durch eine Ganzzahl muss, nach der Logik von Java, wieder eine Ganzzahl ergeben – das richtige Ergebnis (1.5) wird also in eine Integer-Kiste gezwängt, und dabei fallen die Nachkommastellen einfach weg.
Dieses Verhalten verändert man durch einen sogenannten Cast:
int zahl = 3;
double haelfte = (double) zahl/2; //zahl wird zur double gecastet
System.out.println("Die Hälfte von " + zahl + " ist " + haelfte);
Da die Variable haelfte eine Zahl mit Nachkommastellen speichern soll, muss sie den Typ double haben. Damit auf der rechten Seite der Zuweisung aber auch tatsächlich eine Nachkommazahl herauskommt, muss man der Variable zahl (oder dem Divisor) den Typ double aufzwingen (casten), indem man den gewünschten Typ in Klammern davor schreibt.
Details zu diesem Beispiel:
Gecastet wird immer der Wert, vor dem der Cast steht – ggf. kann man mit zusätzlichen Klammern klar machen, was genau gekastet werden soll. Kommen in einer Rechnung verschiedene numerische Datentypen vor, richtet sich das Ergebnis nach dem umfassendsten, hier also double.
int zahl = 3;
double haelfte = ((double) zahl)/2; // casten von zahl
double haelfte = (double) zahl/2; // ebenfalls casten von zahl
double haelfte = zahl/2.0; // casten des Divisors
double haelfte = zahl/(double) 2; // ebenfalls casten des Divisors
double haelfte = (double) (zahl/2); // hilft nicht, da ja dann einfach das Resultat 1 zu 1.0 gecastet wird
Casting mit einfachen Variablen ist selten, meist lässt sich das Problem umgehen. Ausserdem geht es nicht immer – beispielsweise kann man einen String nicht dazu zwingen, eine Ganzzahl zu werden (mit chars, also einzelnen Buchstaben, geht es allerdings – Stichwort ASCII Code).
Häufiger braucht man Casting bei komplexen Variablentypen, also Klassen. Auch hier geht vieles nicht, weil die meisten Klassen inkompatibel sind und sich nicht so einfach ineinander umwandeln lassen. Die Ausnahme sind Unterklassen: z.B. ist Joe eine spezielle Turtle, die wiederum ein spezieller Actor ist (Stichwort: Vererbung). Innerhalb einer solchen Hierarchie unterscheiden sich Klassen eigentlich nur dadurch, dass die Kindklasse jeweils noch ein bisschen mehr kann als ihre Elternklasse – nämlich die Methoden ausführen und die Instanzvariablen kennen, die in ihr definiert sind. Innerhalb einer Hierarchie lassen sich Klassen daher durch Casting ineinander umwandeln – nur wozu?
Beispiel Kommunikation zwischen Klasse und Casting
Hier noch ein etwas ausführlicheres Beispiel, das sich möglicherweise für das Spieleprojekt direkt übernehmen lässt:
In den meisten Spielen gibt es einen Spielstand, diesen gilt es zu speichern (z.B. in einer Variable namens score) und dann im Verlauf des Spiels jeweils zu verändern. Score wird während des ganzen Spiels gebraucht, muss also eine Instanzvariable sein – aber von welcher Instanz? In manchen Spielen genügt es, wenn ein bestimmter Actor den score speichert und verwaltet, aber oft reicht das nicht, z.B. wenn verschiedene Instanzen an der Veränderung des Spielstands beteiligt sind, oder wenn Instanzen im Verlauf des Spiels gelöscht werden können. Praktisch wäre es also, den score in der Welt zu speichern, denn die ist von allen Actors aus zugänglich und existiert während des gesamten Spiels. Kreieren wir also eine Instanzvariable score in der Welt, und machen wir sie für alle Objekte zugänglich:
public int score = 0; //zu Beginn ist der Spielstand meist 0
Wenn jetzt etwas passiert, wofür der Spieler Punkte bekommt, soll der score verändert werden. Meist ist es einer der Actors, der weiss, dass es jetzt Punkte gibt – z.B. weil er sich mit einem anderen überschneidet. Falls also der Actor herausgefunden hat, dass es jetzt Punkte gibt, dann will man den score verändern, ungefähr so:
World myWorld = getWorld(); //die Welt, in der dieser Actor lebt, in einer Variablen namens myWorld speichern
myWorld.score = myWorld.score + 10; //die Variable score der Welt verändern
Dummerweise wird das so aber noch nicht funktionieren; der Compiler wird sich beklagen, dass er die Variable score nicht finden kann. Wenn man genau hinschaut, ist auch klar, wieso: Wir haben die Instanzvariable score nicht in World kreiert (Die Klasse World können wir gar nicht verändern, genauso wenig wie Actor), sondern in einer Unterklasse von World (z.B. GameWorld). Die Methode getWorld() gibt allerdings einen Wert des Typs World zurück, und diese Klasse enthält keine Instanzvariable score. Damit die von uns in GameWorld eingefügten Instanzvariablen oder Methoden zugänglich sind, müssen wir also den zurückgegebenen Wert dazu zwingen, eine spezielle World zu sein – mit einem Cast:
GameWorld myWorld = (GameWorld) getWorld(); //Rückgabewert zu GameWorld casten, Typ der Variablen entsprechend anpassen
myWorld.score = myWorld.score + 10; //die Variable score der Welt verändern
Dieser Code funktioniert, er wiederspricht allerdings der Konvention, dass Instanzvariablen eigentlich nie allgemein zugänglich (public) sein sollten. Darum hier noch die saubere Variante:
In GameWorld (oder wie auch immer die spezielle World in ihrem Spiel heisst):
private int score = 0; //private = nur für Methoden dieser Klasse zugänglich
//public setter-Methode zur Änderung der privaten Instanzvariable score
public void addToScore(int addScore){
score = score + addScore;
}
//getter-Methode zum Lesen von score - in diesem Beispiel nicht gebraucht
public int getScore(){
return score;
}
In einer Actor-Klasse, die den Punktestand ändern will:
GameWorld myWorld = (GameWorld) getWorld();
myWorld.addToScore(10); //Methode benutzen, anstatt die Variable direkt zu ändern
Oder kürzer:
((GameWorld) getWorld()).addToScore(10); //wie oben, nur in einer Zeile