Fisch f = new Fisch("Nemo",123);
f.laufe();
Unsere Zoohandlung erweitert ihr Sortiment. In den neuen Aquarien tummeln sich ab sofort viele Fische, die in unser Verwaltungsprogramm aufgenommen werden müssen.
Ein Fisch ist natürlich ebenso ein Tier wie unser Löwe und der Papagei. Allerdings kann er nicht laufen, dafür aber schwimmen. Somit wäre ein denkbares UML-Diagramm für einen Fisch das folgende:
Im ersten Moment erscheint es sinnvoll, Fisch natürlich von Tier abzuleiten, also gemäß dem folgenden UML-Diagramm (wieder nur auf einige wesentliche Methoden und auf nur einen Konstruktor beschränkt):
Das ging ja relativ einfach und schnell.
Aber Moment mal! Jetzt erbt die Klasse Fisch von der Elternklasse Tier das Feld anzahlBeine
und die Methoden setAnzahlBeine
und getAnzahlBeine
! Das ergibt bei einem Fisch nicht wirklich Sinn. Natürlich könnten wir die Anzahl für einen Fisch auf 0 setzen, aber elegant ist das nicht.
Und es kommt noch schlimmer: Die Methode laufe
wird gleichfalls von Tier geerbt. Somit könnte folgender Code eingegeben werden:
Fisch f = new Fisch("Nemo",123); f.laufe();
Java
Das sollte eigentlich gar nicht erst möglich sein. Ein Objekt vom Typ Fisch sollte nur den Zugriff auf sinnvolle Felder und Methoden erlauben.
Betrachten wir das an unserem Beispiel:
Wir wollen, dass nur Loewe und Papagei das Feld anzahlBeine
haben und auch nur diese beiden Tiere sollen die zu Beinen passenden Methoden setAnzahlBeine(int anzahl)
, getAnzahlBeine()
und laufe()
zur Verfügung haben.
Die einfachste Lösung wäre die, diese Eigenschaften und Methoden wieder von Tier in Loewe und Papagei aufzunehmen, also für Papagei beispielsweise so:
Für Loewe würden wir analog vorgehen.
Ein Problem, das jetzt aber auftaucht: Beim Erstellen eines neuen Tiers muss der Programmierer darauf achten, dass in allen Tieren, die Beine haben, die Felder und Methoden gleich heißen.
Das wäre zum Beispiel dann sinnvoll, wenn man alle Tiere, die Beine haben, in einer Liste speichern möchte und mit Hilfe dieser Liste wieder die Anzahl der benötigten Pedikür-Kräfte berechnet. Das könnte so aussehen:
Tier[] tiere = new Tier[2]; // neues Array für zwei Objekte vom Typ Tier erstellen Papagei p = new Papagei("Ara",1.3,"Hallo",40); p.setAnzahlBeine(2); Loewe leo = new Loewe("Leo",115); leo.setAnzahlBeine(4); tiere[0] = p; // p und leo im Array speichern tiere[1] = leo; int summe = 0; // in Summe steht am Ende die Gesamtzahl der Beine for (int i = 0; i<tiere.length; i++) { summe += tiere[i].getAnzahlBeine(); // Problem!! }
Java
Aber welche Überraschung: Wenn man diesen Code eingibt, muss man feststellen, dass das Programm so nicht compiliert! Wie ist das möglich?
Zunächst muss erwähnt werden, dass ein Array vom Typ Tier[]
Objekte vom Typ Tier speichern kann. Ein Papagei und ein Löwe haben diese Eigenschaft: Ein Teil eines Objektes vom Typ Loewe ist vom Typ Tier, wie zum Beispiel das Feld name. Somit kann ein Array vom Typ Tier[]
alle Objekte speichern, deren Typ von Tier
abgeleitet wurde, also beispielsweise Objekte vom Typ Loewe oder Papagei.
In der vorletzten Zeile taucht das Problem auf: getAnzahlBeine()
ist zwar in Loewe und Papagei vorhanden, jedoch nicht mehr in Tier. Somit könnte die vorletzte Zeile nur ausgeführt werden, wenn getAnzahlBeine()
wieder in der Klasse Tier verortet wäre.
Aber dort haben wir diese Methode gerade erst herausgenommen, damit unser Fisch in die Hierarchie passt.
Eine unschöne Lösung, dieses Problem zu beheben, bestünde darin, in der for-Schleife eine Fallunterscheidung zu machen: der Befehl instanceof
überprüft, ob ein Objekt von einem bestimmten Typ ist:
for (int i = 0; i<tiere.length; i++) { if (tiere[i] instanceof Papagei){ summe += ((Papagei) tiere[i]).getAnzahlBeine(); } else if (tiere[i] instanceof Loewe){ summe += ((Loewe) tiere[i]).getAnzahlBeine(); } }
Java
Hier wird also bei jedem Objekt, das im Array gespeichert ist, überprüft, ob es sich um ein Objekt vom Typ Papagei oder Loewe handelt, um dann das zugehörige Objekt vom Typ Tier zu einem Papagei oder Loewe zu casten, wodurch man auf die zugehörige Methode getAnzahlBeine()
zugreifen kann.
Eine schreckliche Lösung! Wie wird das erst, wenn wir andere Tiere mit Beinen hinzunehmen?
Das ist so kompliziert, dass es wohl doch besser ist, getAnzahlBeine()
und setAnzahlBeine
in Tier zu lassen. Aber das war ja auch nicht gut …
Oder gibt es eine bessere Lösung?
Eine Lösung besteht nun darin, Interfaces einzuführen: Interfaces legen normalerweise konstante Felder und Methoden fest, ohne diese Methoden jedoch zu implementieren.
Eine Klasse kann nun ein oder mehrere Interfaces implementieren (also umsetzen), und muss dadurch alle Methoden, die ein Interface festlegt, enthalten und auch umsetzen.
Wie können wir das in unserem Beispiel verwenden?
Wir definieren ein Interface Laufen:
zoohandlung/Laufen.java:
package zoohandlung; public interface Laufen { public void laufe(); public int getAnzahlBeine(); public void setAnzahlBeine(int anzahl); }
Java
Aus Tier entfernen wir laufe()
, getAnzahlBeine()
und setAnzahlBeine(int anzahl)
ebenso wie das Feld anzahlBeine
, da diese ja nur noch in Papagei und Loewe vorkommen sollen:
zoohandlung/Tier.java:
package zoohandlung; public abstract class Tier { protected String name; double gewicht; String futterzeit; public Tier(){} public Tier(String name, double gewicht) { this.name = name; this.gewicht = gewicht; } @Override public String toString(){ return "Ich heiße "+name+" und wiege "+gewicht+" kg"; } }
Java
Dafür setzen wir in Papagei und Loewe jeweils das Interface Laufen ein, was durch das Schlüsselwort implements
verdeutlicht wird. Sobald implements
in der Klassendefinition vorkommt, überprüft der Compiler, dass alle Methoden des Interfaces Laufen auch tatsächlich vorhanden sind, hier also laufe()
, getAnzahlBeine()
und setAnzahlBeine(int anzahl)
:
zoohandlung/Papagei.java:
package zoohandlung; public class Papagei extends Tier implements Laufen { String lieblingswort; double spannweite; int anzahlBeine; // ist jetzt hier und nicht mehr in Tier public Papagei(String name, double gewicht, String lieblingswort, double spannweite) { super(name, gewicht); this.lieblingswort = lieblingswort; this.spannweite = spannweite; } public void fliege() { System.out.println("Ich fliege"); } public String sprich(String wort) { System.out.println(wort); return wort; } @Override public void laufe() { System.out.println("Ich laufe"); } @Override public void setAnzahlBeine(int anzahlBeine) { this.anzahlBeine = anzahlBeine; } @Override public int getAnzahlBeine() { return anzahlBeine; } }
Java
Analog dazu wird auch die Klasse Loewe umgeschrieben:
zoohandlung/Loewe.java:
package zoohandlung; public class Loewe extends Tier implements Laufen { String maehnenShampooMarke; int anzahlBeine; public Loewe(String name, double gewicht) { this.name = name; this.gewicht = gewicht; } public Loewe(String name, double gewicht, String msm) { this.name = name; this.gewicht = gewicht; this.maehnenShampooMarke = msm; } public int bruelle(int dezibel) { System.out.println("Ich brülle mit " + " dB"); return dezibel; } public void laufe() { System.out.println("Ich laufe"); } public void setAnzahlBeine(int anzahlBeine) { this.anzahlBeine = anzahlBeine; } public int getAnzahlBeine() { return anzahlBeine; } }
Java
Auf den ersten Blick sehen Loewe und Papagei so aus, als hätte man einfach nur alle Felder und Methoden, die mit Beinen zu tun haben, wieder in die einzelnen Klassen gezogen. Negativ fällt auf, dass laufe()
, setAnzahlBeine(int anzahlBeine)
und getAnzahlBeine
in Loewe und Papagei vorkommen, und zwar identisch! Da haben wir wieder die doppelten Fehlerquellen!
Was aber haben wir gewonnen?
Kommen wir zurück zu unserem Pediküre-Beispiel:
Soeben hatten wir das Problem, dass beim Zusammenzählen der Fußanzahl in unserer Zoohandlung der Code-Abschnitt
Tier[] tiere = new Tier[2]; // neues Array für zwei Objekte vom Typ Tier erstellen Papagei p = new Papagei("Ara",1.3,"Hallo",40); p.setAnzahlBeine(2); Loewe leo = new Loewe("Leo",115); leo.setAnzahlBeine(4); tiere[0] = p; // p und leo im Array speichern tiere[1] = leo; int summe = 0; // in Summe steht am Ende die Gesamtzahl der Beine for (int i = 0; i<tiere.length; i++) { summe += tiere[i].getAnzahlBeine(); // Problem!! }
Java
zu dem Problem führte, dass getAnzahlBeine()
zwar in Papagei und Loewe enthalten sind, nicht aber in Tier. tiere
muss aber vom Typ Tier[]
sein, damit darin Papageien und Löwen gespeichert werden können.
Wie hilft uns unter Interface Laufen bei der Lösung dieses Problems? Hier eine Möglichkeit:
externesPackage/ExterneMainKlasse.java:
package externesPackage; import zoohandlung.*; public class ExterneMainKlasse { public static void main(String[] args) { Laufen[] tiere = new Laufen[2]; // neues Array für zwei Objekte vom Typ Laufen erstellen Papagei p = new Papagei("Ara", 1.3, "Hallo", 40); p.setAnzahlBeine(2); Loewe leo = new Loewe("Leo", 115); leo.setAnzahlBeine(4); tiere[0] = p; // p und leo im Array speichern tiere[1] = leo; int summe = 0; // in Summe steht am Ende die Gesamtzahl der Beine for (int i = 0; i < tiere.length; i++) { summe += tiere[i].getAnzahlBeine(); // Jetzt geht alles! } System.out.println(summe); } }
Java
Der Unterschied besteht darin, dass das Array tiere
jetzt nicht mehr vom Typ Tier[]
ist, sondern vom Typ Laufen[]
. Somit kann dieses Array alle Objekte vom Typ Laufen
speichern und auch alle Objekte, die das Interface Laufen implementieren!
Die Zeile in der for-Schleife macht nun keine Probleme mehr, da tiere[i]
vom Typ Laufen
ist: somit ist getAnzahlBeine()
bekannt, da diese Methode im Interface Laufen zu finden ist.
Die Tatsache, dass ein Objekt vom Typ Papagei gleichzeitig auch ein Objekt vom Typ Tier und eines vom Typ Laufen ist wird als Polymorphismus ("Vielgestaltigkeit") bezeichnet. |
Das Interface hat uns also dazu verholfen, dass unser Programm für alle Objekte vom Typ Laufen
funktioniert. Kommt eine neue Tiersorte dazu, die Beine hat und das Interface Laufen implementiert, kann dieses auch im Array gespeichert und verarbeitet werden.
Der Vollständigkeit halber hier noch der Code von Fisch:
zoohandlung/Fisch.java:
package zoohandlung; public class Fisch extends Tier { int maxTauchtiefe; int anzahlKiemen; public Fisch(String name, double gewicht) { super(name, gewicht); } public void schwimme() { System.out.println("Ich schwimme!"); } public void blubber(int anzahl) { for (int i = 0; i < anzahl; i++) { System.out.println("blubb"); } } }
Java
Um Interfaces in UML darstellen zu können, werden gestrichelte Linien mit Pfeilspitzen für die Beziehungen verwendet, Interfaces selbst werden mit
<<interface>>
gekennzeichnet.
Das gesamte Klassendiagramm sieht nun so aus:
Den gesamten Code mit Klassendiagramm findest du hier.
Das komplette Netbeans-Projekt als Zip-Datei kannst du hier herunterladen:
Dateiname |
Zoohandlung_Interfaces.zip |
|
Größe in Bytes |
18287 |
|
Einfügezeitpunkt |
10.2.2018, 12:33:14 |