Kapitel 7: Objekte und Klassen: Unterschied zwischen den Versionen
(→Assoziationen) |
|||
Zeile 273: | Zeile 273: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | |||
+ | |||
=== Klassenvariablen und Klassenmethoden === | === Klassenvariablen und Klassenmethoden === |
Version vom 30. April 2015, 12:54 Uhr
Zur Startseite - Kapitel 1 - Kapitel 2 - Kapitel 3 - Kapitel 4 - Kapitel 5 - Kapitel 6 - Kapitel 8
Bisher haben wir vornehmlich Datentypen verwendet, die in JAVA vordefiniert sind (z.B.int, double, String). Die große Stärke einer objektorientierten Programmiersprache besteht darin, dass wir als Programmierer eigene Datentypen in Form von Klassen nach unseren Vorstellungen und Bedürfnissen erstellen können. Eine Klasse beschreibt entsprechend den Aufbau eines komplexen Datentyps.
Beispiele für Objekte der Klasse Rechteck:
Wie wir bereits aus der 6.Klasse wissen, lassen sich diese Objekte der Klasse Rechteck auch in Form von Objektdiagrammen darstellen.
Diese Darstellung von Objekten beinhaltet neben dem Objektnamen, der nach einer Konvention immer klein geschrieben wird, die Attribute des Objekts und die jeweiligen Attributwerte. Sie definieren den Zustand des Objekts. Objekte mit den denselben Attributen sind Instanzen einer Klasse. Eine Klasse stellt einen Konstruktionsplan für bestimmte Objekte dar, der mit all seinen Informationen auch ohne diese Objekte existiert. Eine Klasse ist also keine Menge von Objekten!
Die Definition einer Klasse beinhaltet drei Bestandteile:
- Die Datenfelder bzw. Instanzvariablen speichern die Daten, die das jeweilge Objekt benutzt.
- Die Konstruktoren erlauben es, neue Objekte zu erzeugen und diese in einen bestimmten Anfangszustand zu versetzen.
- Die Methoden implementieren das Verhalten der Objekte.
Beispiel: Die Klasse Rechteck
Instanzvariablen und Methoden haben wir bereits kennengelernt. Neu sind die sogenannten Konstruktoren.
Inhaltsverzeichnis |
Konstruktor
Eine Klasse kann keinen, einen oder mehrere unterschiedliche Konstruktoren besitzen. Sie dienen dazu, ein neu gebildetes Objekt einer Klasse in einen definierten Anfangszustand zu versetzen. Welcher dies ist hängt davon ab, welcher Konstruktor bei der Objektbildung aufgerufen wird. Ein leerer (Standard-) Konstruktor muss nicht angegeben werden, er wird bei Fehlen von der JVM (Java Virtual Machine) automatisch erzeugt. Aus diesem Grund hatten wir in unseren bisherigen Aufgaben keinen expliziten Konstruktor.
Syntax:
public nameDerKlasse(Parameter) {}
Beispiel: Konstruktoren der obigen Klasse Rechteck:
public Rechteck() weist dem Rechteck standardmäßig Werte zu.
public Rechteck(){ breite = 10; hoehe = 7; }
Wir erhalten bei Aufruf des Konstruktors ein Rechteck der Größe 10x7. Die restlichen Werte werden von der JVM auf einen internen Standardwert gesetzt.
public Rechteck(breite, hoehe) gibt dem User die Möglichkeit, die Größe des Rechtecks selbst zu bestimmen.
public Rechteck(int breite, int hoehe){ this.breite = breite; this.hoehe = hoehe; }
Hier werden nach Aufruf des Konstruktors den Instanzvariablen die entsprechenden Werte der Parameter zugewiesen. Das Schlüsselwort this liefert innerhalb eines Objekts immer eine Referenz auf das Objekt selbst, d.h. es steht in der bekannten Punktschreibweise an der Stelle des Objektnamens einfach das Wort this.
public Rechteck(breite, hoehe, fuellfarbe) gibt dem User die Möglichkeit, neben der Größe des Rechtecks auch noch dessen Füllfarbe festzulegen.
public Rechteck(int breite, int hoehe, String fuellfarbe){ this.breite = breite; this.hoehe = hoehe; this.fuellfarbe = fuellfarbe }
Konstruktoren können also mit unterschiedlichen Parameterlisten deklariert sein. Man spricht hier vom Überladen des Konstruktors.
Konstruktoraufruf
Neue Instanzen einer Klasse werden mit dem Operator new erzeugt.
Rechteck rechteck1 = new Rechteck(); Rechteck rechteck2 = new Rechteck(12,8); Rechteck rechteck3 = new Rechteck(15,4,"rot");
Die drei erzeugten Rechtecke sind vom Typ Rechteck; d.h. sie sind Instanzen der Klasse Rechteck.
rechteck1 wird mit dem Standardkonstruktor erzeugt und hat die Größe 10x7.
rechteck2 wird vom User auf die Größe 12x8 festgelegt
rechteck3 bekommt vom User die Größe 15x4 und die Füllfarbe rot zugewiesen.
Hier sollte nun deutlich werden, dass das erzeugte Objekt eine Variable ist, die nach dem Plan der Klasse Rechteck aufgebaut ist. Erst wenn das Objekt erzeugt wurde, kann mittels Punktschreibweise auch auf die Eigenschaften des Objekts zugegriffen werden. Beispiel:
rechteck1.breite = 10;
rechteck3.fuellfarbe = "rot"
Getter- und Setter-Methoden
Will man von außen auf private gesetzte Attribute zugreifen, ist dies nur indirekt möglich. Wir nutzen dazu Methoden, die das betreffende Objekt veranlassen, Werte zurückzugeben bzw. zu verändern. Zur Rückgabe von Werten nutzt man sogenannte sondierende Methoden oder Getter-Methoden. Üblicherweise verwendet man als Namen für eine derartige Methode den Bezeichner getAttributname.
Soll der Benutzer auch schreibenden Zugriff auf die Attribute haben, dann erstellt man sogenannte Setter-Methoden, die von außen einen Wert übernehmen und dem auf private gesetzten Attribut zuweisen. Als Bezeichner verwendet man analog zu den Getter-Methoden setAttributname. Beispiel:
public class Rechteck { private int laenge; private int breite; public Rechteck(int laenge, int breite){ this.laenge=laenge; this.breite=breite; } public int getLaenge(){ return laenge; } public int getBreite(){ return breite; } public void setLaenge(int l){ laenge=l; } public void setBreite(int b){ breite=b; } }
Setter- und Getter-Methoden müssen natürlich auf public gesetzt werden, um die ihnen zugedachte Aufgabe zu erfüllen.
Bis hierher ist eigentlich noch nicht klar, warum wir den direkten Lese- bzw. Schreibzugriff auf die Attribute kontrollieren sollten. Die Stärke dieses Verfahrens zeigt sich dann, wenn es für den Zugriff Beschränkungen gibt. So kann beispielsweise die Eingabe auf einen bestimmten Wertebereich beschränkt werden und im Fall einer unerlaubten Eingabe die Eingabe verweigert werden. Beispiel: Um zu vermeiden, dass ein Objekt der obigen Klasse Rechteck einen bestimmten Wert für die Länge bzw. Breite überschreitet, kann man die Methode mit einer bedingten Anweisung versehen:
public void setLaenge(int l){ if(l>=100){System.out.println("Unzulaessiger Wert !");}else{ laenge=l;} } public void setBreite(int b){ if(b>=100){System.out.println("Unzulaessiger Wert !");}else{ breite=b; } }
Assoziationen
Eine wichtige Eigenschaft objektorientierter Systeme ist der Austausch von Botschaften zwischen Objekten. Dazu muss ein Objekt andere Objekte "kennen" und kann Botschaften weitergeben. Diese Verbindung nennt sich Assoziation. Bei Assoziationen ist zu unterscheiden, ob nur eine Seite die andere kennt oder ob eine Navigation in beiden Richtungen möglich ist:
- Eine unidirektionale Beziehung geht nur in eine Richtung.
- Eine bidirektionale Beziehung geht in beide Richtungen.
Daneben ist bei Beziehungen die Multiplizität zu beachten (siehe Exkurs: UML). Sie sagt aus, mit wie vielen Objekten eine Seite eine Beziehung haben kann. Übliche Beziehungen sind 1:1 und 1:n. Wir beschränken uns hier auf unidirektionale Beziehungen.
Implementierung einer unidirektionalen 1:1-Beziehung
Bei bekannten Computerspielen kann ein Spieler seine Fahrkünste auf einem Formel 1-Kurs testen oder als Pilot einen Flughafen anfliegen. Dabei muss jeweils ein Spieler mit einem Kurs/Flughafen assoziiert werden. Damit ein Spieler sich auf einem Kurs befinden kann, lässt sich in Spieler eine Referenzvariable vom Typ Kurs anlegen. In Java sähe das in etwa so aus:
public class Spieler { public Kurs k; } public class Kurs { }
Während des Spiels müssen dann natürlich noch entsprechende Referenzen gesetzt werden:
public class Spiel { Spieler uschi = new Spieler(); Kurs hockenheim = new Kurs(); uschi.k = hockenheim; }
Implementierung einer unidirektionalen 1:n-Beziehung
Immer dann, wenn ein Objekt auf mehrere andere Objekte verweisen muss, reicht eine einfache Referenzvariable vom Typ der anderen Seite nicht mehr aus. Dann werden Datenstrukturen benötigt, die mehrere Referenzen aufnehmen können. In Computerspielen kann ein Spieler häufig Gegenstände, Waffen etc. sammeln. Um dies in der Programmierung umzusetzen, müssen auf der 1-Seite eine Datenstruktur verwenden, die eine Anzahl anderer Objekte aufnimmt. Handelt es sich um eine feste Anzahl von Gegenständen, ist als Datenstruktur das bereits bekannte Array gut geeignet. Bei Spielen, in denen sich die Anzahl der Gegenstände dynamisch ändert, ist ein Array wenig elegant, da die manuelle Vergrößerung oder Verkleinerung des Arrays aufwendig ist. Hier kann man beispielsweise die Klasse ArrayList aus dem Package java.util.ArrayList zum Einsatz bringen.
Wenn wir von einem Spiel ausgehen, in dem ein Spieler Waffen sammeln kann, könnte die Umsetzung folgendermaßen aussehen:
public class Weapon { public String bezeichnung; public Weapon( String bezeichnung ) { this.bezeichnung = bezeichnung; } }
Die Klasse Spieler erhält nun ein Attribut weapons von Typ ArrayList:
private ArrayList<Weapon> weapons = new ArrayList<Weapon>();
Damit sieht die Klasse wie folgt aus:
public class Spieler { private ArrayList<Weapon> weapons = new ArrayList<Weapon>(); public void addWeapon( Weapon w ) { weapons.add(w); } public void listWeapons() { for(int i=0;i<weapons.size();i++){ System.out.println( weapons.get(i).bezeichnung );} } }
Zuletzt führen wir Spieler und Waffen in der Klasse Spiel zusammen:
public class Spiel{ public Spiel(){ Spieler uschi = new Spieler(); uschi.addWeapon( new Weapon( "Keule" ) ); uschi.addWeapon( new Weapon( "Morgenstern" ) ); uschi.listWeapons(); // Ausgabe: Keule Morgenstern } }
Klassenvariablen und Klassenmethoden
Bis jetzt kennen wir bei Variablen zwei Typen: Lokale Variablen, wie wir sie beispielsweise in Schleifen als Zählvariablen verwenden, und Instanzvariablen. Innerhalb der Klasse Kreis, die sozusagen den Datentyp Kreis definiert, haben wir beispielsweise Radius, Fuellfarbe oder Linienstärke als Instanzvariablen. Jede Variable wird für sich separat für eine Instanz (also ein aus der Klasse erzeugtes Objekt) angelegt, d.h. jede Instanz hat eine eigene Kopie der Variablen. Die Methoden beziehen sich auf die jeweils angelegte Instanz.
Damit alle Objekte einer Klasse eine bestimmte Variable gemeinsam verwenden können, gibt es die sog. Klassenvariablen. Analog dazu benutzt man Klassenmethoden für Operationen, die sich nicht auf ein bestimmtes Objekt, sondern auf die Klasse an sich beziehen. Variablen und Methoden werden mit dem Schlüsselwort static an die jeweilige Klasse gebunden. Der Zugriff auf statische Variablen erfolgt über Klassenname.Variablenname, der Zugriff auf statische Methoden über Klassenname.Methodennname
Folgendes Beispiel soll das Konzept verdeutlichen:
In einer Klasse Testobjekt wird eine statische Variable zaehler angelegt, in der die Anzahl der erzeugten Instanzen der Klasse abgespeichert wird. Über eine statische Getter-Methode kann der aktuelle Wert des Zählers abgefragt werden. In einer zweiten Klasse ZaehlerTest werden drei Instanzen der Klasse Testobjekt erzeugt und als Kontrolle ihre jeweilige Rangzahl ausgegeben. Am Ende wird über die Methode getZaehler() die Anzahl der erzeugten Instanzen abgefragt.
public class Testobjekt { // Klassenvariable static int zaehler = 0; // Instanzvariable int nummer; // Konstruktor public Testobjekt() { zaehler= zaehler+1; nummer = zaehler; } // Klassenmethode static int getZaehler() { return zaehler; } } public class Zaehlertest{ public void zaehlerTest(){ System.out.println("Klassenvariable zaehler:"+ Testobjekt.getZaehler()); Testobjekt obj_1 = new Testobjekt(); Testobjekt obj_2 = new Testobjekt(); Testobjekt obj_3 = new Testobjekt(); System.out.println("Instanzvariable nummer im 1. Objekt: "+ obj_1.nummer); System.out.println("Instanzvariable nummer im 2. Objekt: "+ obj_2.nummer); System.out.println("Instanzvariable nummer im 3. Objekt: "+ obj_3.nummer); System.out.println("Klassenvariable zaehler: " + Testobjekt.getZaehler()); } }