Einführung
Wiedergabe stellt eine Verbindung zu YouTube her.
Wir haben bereits die Vererbung als eine mögliche Beziehung zwischen Klassen kennengelernt. Daneben gibt es aber noch eine andere Form von Beziehung: Die sogenannte Assoziation.
Betrachten wir ein Beispiel. Wir wollen eine Klasse Motorrad modellieren. Ein erster grober Entwurf könnte so aussehen:
An dieser Stelle machen wir uns dann für gewöhnlich Gedanken, welche Datentypen für die Attribute geeignet sein könnten. Das ist in diesem Fall aber nicht so klar. Stattdessen könnten wir auf die Idee kommen, dass sowohl der Motor als auch die Reifen wiederum als eigene Klassen modelliert werden sollten. Hier zwei mögliche Klassen:
Objekte der Klassen Motor und Reifen sind also Bestandteile von Objekten der Klasse Motorrad. Anders ausgedrückt kann man auch sagen, ein Objekt der Klasse Motorrad hat ein Objekt der Klasse Motor und zwei der Klasse Reifen. Dies ist eine Form von Assoziation von Klassen.
In einem groben Entwurfsdiagramm kann man dies so darstellen:
Dazu zwei kurze Bemerkungen:
- Die Zahlen an den Pfeilen geben an, wie viele Objekte der Klasse Motor bzw. Reifen ein Objekt der Klasse Motorrad jeweils besitzt.
- Beachte, dass die Pfeilspitze bei dieser Beziehung anders aussieht als bei Vererbung.
Umsetzung in Java
Erstellen der Klassen
Sehen wir uns nun mögliche Umsetzungen der Klassen an. Zunächst die Klassen Motor und Reifen:
public class Motor { // Attribute private int leistung; private double hubraum; // Konstruktor public Motor(int pLeistung, double pHubraum){ leistung = pLeistung; hubraum = pHubraum; } // Ausgabe public void schreibeInfo(){ System.out.println("Leistung: "+leistung); System.out.println("Hubraum: "+hubraum); } // Umwandlung in String: public String toString() { return "Motor mit " + leistung + " PS und " + hubraum + " Liter Hubraum"; } // get-Methoden public int getLeistung(){ return leistung; } public double getHubraum(){ return hubraum; } }
public class Reifen { // Attribute private double durchmesser; private double profiltiefe; private String jahreszeit; // Kontruktor public Reifen(double pDurchmesser, double pProfiltiefe, String pJahreszeit){ durchmesser = pDurchmesser; profiltiefe = pProfiltiefe; jahreszeit = pJahreszeit; } // Ausgabe public void schreibeInfo(){ System.out.println("Durchmesser: "+durchmesser); System.out.println("Profiltiefe: "+profiltiefe); System.out.println("Jahreszeit: "+jahreszeit); } // Umwandlung in String public String toString() { return jahreszeit+"reifen mit " + durchmesser + " cm Durchmesser und " + profiltiefe + " Profiltiefe"; } // get-Methoden public double getDurchmesser(){ return durchmesser; } public double getProfiltiefe(){ return profiltiefe; } public String getJahreszeit(){ return jahreszeit; } }
Wir sehen, dass bei diesen beiden Klassen noch nichts ungewöhnliches geschieht. Wir haben einige Attribute, jeweils einen Konstruktor und die get-Methoden. Um später den Umgang trainieren zu können, haben wir auch jeweils eine Methode schreibeInfo() und auch schon eine toString()-Methode. Natürlich kämen wir auch ohne schreibeInfo() aus, aber diese soll uns helfen, zu sehen, wie man gezielt Methoden dieser Klassen aufrufen kann.
Interessant wird es bei der Klasse Motorrad:
public class Motorrad { // Attribute private Motor meinMotor; private Reifen[] meineReifen; // Kontruktor public Motorrad(Motor pMotor, Reifen pVorne, Reifen pHinten){ meinMotor = pMotor; meineReifen = new Reifen[2]; meineReifen[0] = pVorne; meineReifen[1] = pHinten; } // Ausgabe public void schreibeInfo(){ System.out.println("Motor:"); meinMotor.schreibeInfo(); System.out.println("Reifen:"); for (int i=0; i<meineReifen.length; i=i+1){ meineReifen[i].schreibeInfo(); } } // Umwandlung in String: public String toString(){ return "Motorrad mit einem "+meinMotor+" und vorne einem "+getVorderreifen()+" und hinten einem "+getHinterreifen(); } // get-Methoden public Reifen getVorderreifen(){ return meineReifen[0]; } public Reifen getHinterreifen(){ return meineReifen[1]; } public Motor getMotor(){ return meinMotor; } }
Diese Klasse besitzt nun tatsächlich ein Attribut der Klasse Motor. Wir können also als Typen für Attribute nicht nur die Standardtypen wie int, boolean, String etc. verwenden, sondern auch eigene erstellen. Außerdem besitzt sie auch noch ein Attribut, das aus einem Array von Objekten der Klasse Reifen besteht.
Der Konstruktor verlangt drei Parameter; einem vom Typ Motor und zwei vom Typ Reifen. Der Motor wird direkt dem Attribut meinMotor zugeordnet. Für die Reifen wird zunächst das Array erstellt und anschließend werden die beiden dort abgelegt.
Man sagt, die Klasse Motorrad verwaltet ihre Reifen mittels des Arrays meineReifen und den Motor mittels des Attributs meinMotor. Dies macht man im Implementationsdiagramm so deutlich:
Warum man die Verwaltung mittels eines Arrays in dieser Weise darstellt und hier keinen Assoziationspfeil zeichnet, mag spontan unverständlich erscheinen. Wenn wir später noch andere Verwaltungsmöglichkeiten kennengelernt haben, wird dies etwas klarer werden. Hier sei aber betont, dass ich versuche, mich an die Darstellung im Abi in NRW zu halten. In anderer Literatur kann man abweichende Darstellungen finden.
Testen der Klassen und ihrer Methoden
Nun ist es Zeit, diese Klassen zu testen:
public class Motorradtest { public static void main(String[] args) { Motorrad meinMotorrad = new Motorrad(new Motor(62, 0.6), new Reifen(60, 2.5, "Sommer"), new Reifen(65, 2.5, "Sommer")); System.out.println(meinMotorrad.getVorderreifen().getDurchmesser()); } }
In diesem Beispiel wird zunächst ein Objekt der Klasse Motorrad erstellt. Neu für uns ist, dass dem Konstruktor von Motorrad Objekte anderer Klassen übergeben werden. Dadurch müssen wird beim Aufrufen den Konstruktors von Motorrad einmal den Konstruktor von Motor und zweimal den von Reifen aufrufen. Anschaulich bedeutet dies, dass für für ein neues Motorrad natürlich auch einen neuen Motor und neue Reifen brauchen.
Anschließend wird der Durchmesser der Vorderreifens ausgegeben. Wie wir an diesen Durchmesser kommen, ist auch noch etwas ungewohnt. Zunächst gelangen wir mit meinMotorrad.getVorderreifen()
an den Vorderreifen. Wir gehen aber dann noch einen Schritt weiter und fragen den Vorderreifen nach seinem Durchmesser, wodurch sich dann die recht lange Anweisung meinMotorrad.getVorderreifen().getDurchmesser()
ergibt.
Variieren wir einmal das Beispiel:
public class Motorradtest { public static void main(String[] args) { Motorrad meinMotorrad = new Motorrad(new Motor(62, 0.6), new Reifen(60, 2.5, "Sommer"), new Reifen(65, 2.5, "Sommer")); meinMotorrad.schreibeInfo(); } }
Hier erhalten wir diese Ausgabe:
Motor: Leistung: 62 Hubraum: 0.6 Reifen: Durchmesser: 60.0 Profiltiefe: 2.5 Jahreszeit: Sommer Durchmesser: 65.0 Profiltiefe: 2.5 Jahreszeit: Sommer
Wie genau kommt diese zustande? Sehen wir uns hier nochmal genau die Methode schreibeInfo() der Klasse Motorrad an:
// Ausgabe public void schreibeInfo(){ System.out.println("Motor:"); meinMotor.schreibeInfo(); System.out.println("Reifen:"); for (int i=0; i<meineReifen.length; i=i+1){ meineReifen[i].schreibeInfo(); } }
Diese Methode schreibt zuerst Motor: in der Konsole. Danach ruft diese Methode wiederum die Methode scheibeInfo() des Attributs meinMotor auf. Daher sollten wir uns jetzt ansehen, wie diese Methode der Klasse Motor aussieht:
public void schreibeInfo(){ System.out.println("Leistung: "+leistung); System.out.println("Hubraum: "+hubraum); }
Diese Methode gibt also Leistung und Hubraum in der Konsole aus.
Anschließend ruft schreibeInfo() der Klasse Motorrad in einer Schleife die Methode schreibeInfo() des Vorder- und des Hinterreifens auf. Hier gilt dasselbe Prinzip; in der Klasse Reifen wurde festgelegt, was diese Methode ausgeben soll.
Variieren wir erneut das Beispiel, um die toString()-Methoden zu testen:
public class Motorradtest { public static void main(String[] args) { Motorrad meinMotorrad = new Motorrad(new Motor(62, 0.6), new Reifen(60, 2.5, "Sommer"), new Reifen(65, 2.5, "Sommer")); System.out.println(meinMotorrad); } }
Hier erhalten wir diese (recht lange) Ausgabe:
Motorrad mit einem Motor mit 62 PS und 0.6 Liter Hubraum und vorne einem Sommerreifen mit 60.0 cm Durchmesser und 2.5 Profiltiefe und hinten einem Sommerreifen mit 65.0 cm Durchmesser und 2.5 Profiltiefe
Um diese nachzuvollziehen, sehen wir uns die toString()-Methode von Motorrad an:
// Umwandlung in String: public String toString(){ return "Motorrad mit einem "+meinMotor+" und vorne einem "+getVorderreifen()+" und hinten einem "+getHinterreifen(); }
Wie diese Methode es ja muss, gibt sie einen String zurück. Innerhalb dieses Strings finden wir auch meinMotor, getVorderreifen() und getHinterreifen(). Da meinMotor mit dem String „Motorrad mit einem“ durch ein + verknüpft wird, weiß Java, dass meinMotor in einen String verwandelt werden muss. Also wird die toString()-Methode des Motors aufgerufen und der entsprechende String eingefügt. Dasselbe geschieht anschließend mit den beiden Reifen. Da diese in einem Array liegen, wurden der Übersicht halber die get-Methoden verwendet, um an die Reifen zu gelangen.
ist- vs hat-Beziehung
Wir kennen nun zwei Arten von Beziehungen von Klassen: Vererbung und Assoziation. Anfängern fällt es manchmal schwer, zu entscheiden, welche Art von Beziehung in einer gegebenen Situation vorliegt. Hier hilft ein einfacher sprachlicher Test: Liegt eine ist-Beziehung oder eine hat-Beziehung vor? Bei „ist“ haben wir Vererbung, bei „hat“ hingegen Assoziation. Hier zwei Beispiele:
- Ein Motorrad hat einen Motor. Also liegt Assoziation vor. (Keiner würde sagen „Ein Motorrad ist ein Motor“.)
- Ein Lehrer ist eine Person. Also liegt Vererbung vor.
Vielfachheiten der Beziehungen
In unserem Beispiel wussten wir immer genau, wie viele Objekte einer Klasse ein anderes Objekt (einer anderen Klasse) besitzt. Zum Beispiel hat jedes Objekt der Klasse Motorrad genau zwei Objekte der Klasse Reifen. Es kann aber auch vorkommen, dass die Anzahl nicht genau bekannt ist oder auch variabel gehalten werden soll.
Ein Beispiel aus der realen Welt: Ein Mann kann entweder eine oder keine Ehefrau haben. Er könnte auch kein oder ein oder mehrere Kinder haben. Allgemein kann man Vielfachheiten daher wie folgt durch Beschriftungen am Pfeil angeben:
Beschriftung | Bedeutung |
1 | genau ein assoziiertes Objekt |
0..1 | kein oder genau ein assoziiertes Objekt |
0..* | kein oder beliebig viele assoziierte Objekte |
n..m | n bis m assoziierte Objekte |
1..* | mindestens ein assoziierte Objekte |
n..* | mindestens n assoziierte Objekte |