…die sich als aufwändiger und lehrreicher herausgestellt hat, als ich dachte.
Es begann damit, dass ein Kollege mir von dem Computerspiel erzählte, das ihn gerade beschäftigte: Als Schiffbrüchiger auf einer Insel sammelt man unter anderem Gegenstände ein und erntet Pflanzen, jagt Tiere und arbeitet mit Werkzeugen. Zum Beispiel kann man mit einem Messer und einer Kokosnuss man Trinkwasser erzeugen (wobei die Kokosnuss verbraucht wird), und mit einem Schwein und einem Messer erzeugt man Haut und Fleisch, und aus der Haut kann man Leder machen, und aus dem Leder Riemen, und so weiter.
Ha, dachte ich mir, das ist nett, das sieht objektorientiert aus, das könnte ich doch auch mal mit Schülern machen. Erster Gedanke:

Das Messer kriegt folgende Methoden:
- eine Methode
anwendenAuf(Kokosnuss)
- eine Methode
anwendenAuf(Schwein)
- eine Methode
anwendenAuf(Spielobjekt)
(eventuell verzichtbar, da ererbt, aber siehe weiter unten)
Diese drei Methoden heißen zwar alle gleich, sind aber trotzdem verschiedene Methoden und machen verschiedene Sachen – dieses Konzept heißt overloading. – Ein anderes Konzept ist das des overriding, wenn eine Unterklasse die gleichnamige Methode der Oberklasse überschreibt. Das ist der Fall, wenn Methode (3) die ererbte gleichnamige Methode überschreibt. Siehe weiter unten. – Wenn man die Methode anwendenAuf
mit einem Kokosnuss-Objekt als Parameter aufruft, wird Methode (1) ausgewählt und ausgeführt, wenn man sie mit einem Schwein-Objekt aufruft, wird (2) ausgeführt, wenn man sie mit irgendeinem anderen Spielobjekt-Objekt als Parameter aufruft, wird die Methode (3) ausgeführt.
Dann würde jede Schülerin, jeder Schüler eine eigene Klasse erzeugen (Messer, Kokosnuss, Hammer, Feuerstein, Zange.…) und würde für jede interessante Interaktion mit einem anderen Objekttyp eine eigene anwendenAuf
-Methode schreiben – und die Catch-all-Methode für die restlichen Objekttypen übernehmen. Das fände ich ordentlich.
Das funktioniert auch wie gedacht, aber leider nur erst einmal, und dann doch nicht: In der Regel wird man eine Liste oder ein Array von Spielobjekten haben, und der Spieler wählt eins davon aus und wendet sein Messer darauf an. Und dann wird automatisch und stets die Methode (3) aufgerufen, auch wenn sich hinter dem Spielobjekt eine Kokosnuss verbirgt. Will heißen, wenn ich folgenden Code ausführe:
Spielobjekt o = new Kokosnuss();
Kokosnuss k = new Kokosnuss();
Spielobjekt m = new Messer();
m.anwendenAuf(o);
m.anwendenAuf(k); |
Spielobjekt o = new Kokosnuss();
Kokosnuss k = new Kokosnuss();
Spielobjekt m = new Messer();
m.anwendenAuf(o);
m.anwendenAuf(k);
Dann wird bei m.anwendenAuf(o)
die Methode (3) aufgerufen, und nur bei m.anwendenAuf(k)
wird die Methode (1). Denn wenn die ausführbaren Java-Klassen erzeugt werden, also beim Compilieren, also Übersetzen des Quellcodes, dann gilt: Die Kokosnuss o ist vom Typ Spielobjekt, basta. Das heißt static binding. Bei diesem Überstzen wird dann festgelegt, dass bei m.anwendenAuf(o)
eben die Methode (3) ausgeführt wird. Und das geschieht dann auch, wenn das Programm tatsächlich ausgeführt wird.
Zwar hat beim Ausführen des Programms das Objekt o dann den Typ Kokosnuss, wie man herausfinden kann durch einen Aufruf von System.out.println( o.getClass() )
. Aber das bringt nichts mehr, da ist die Wahl bereits getroffen. Etwas anders ist es bei ererbten und überschriebenen Methoden. Hier noch einmal die Zeilen von oben:
Spielobjekt o = new Kokosnuss();
Spielobjekt m = new Messer();
m.anwendenAuf(o); |
Spielobjekt o = new Kokosnuss();
Spielobjekt m = new Messer();
m.anwendenAuf(o);
In der letzten Zeile wird immer noch die Methode (3) anwendenAuf(Spielobjekt)
aufgerufen. Aber es ist die Methode, die in der Klasse Messer definiert ist, und nicht die gleichnamige in der übergeordneten Klasse Spielobjekt. Das ist das overriding von vorhin, und dabei wird erst zur Laufzeit entschieden, also wenn das Programm tatsächlich läuft, welche der beiden Methoden aufgerufen wird. Die Maschine schaut zur Laufzeit, von welchem Typ m wirklich ist (nämlich Messer, und nicht Spielobjekt), und wählt dann die entsprechende Methode. Das heißt dann dynamic binding.
Kurz gesagt:
- Wenn ein Objekt eine ererbte Methode mit einer eigenen überschreibt, wird stets letztere ausgeführt. In der Unterklasse entscheidet man also selber, ob die ererbte Methode benutzt werden soll oder ob es stattdessen eine eigene Methode geben wird.
- Wenn ein Objekt allerdings als Parameter einer Methode übergeben wird, hat die Klasse, die diese Methode zur Verfügung stellt, keine Auswahlmöglichkeit und kann den Objekttyp des Parameters nicht als Auswahlkriterium heranziehen. Der wird vorher festgelegt auf das, was da steht, egal welche Klasse wirklich dahinter steckt.
Fazit: Die Klasse Messer braucht nur eine einzige Methode anwendenAuf(Spielobjekt)
. Man muss das ganze so machen:
void anwendenAuf(Spielobjekt o) {
if (o instanceof Kokosnuss) {
// ...
}
else if (o instanceof Schwein) {
// ...
}
else {
// ...
}
} |
void anwendenAuf(Spielobjekt o) {
if (o instanceof Kokosnuss) {
// …
}
else if (o instanceof Schwein) {
// …
}
else {
// …
}
}
Ich finde das ein bisschen weniger übersichtlich, als für jedes Objekt eine eigene Methode zu haben. Außerdem stört mich der instanceof-Operator. Mit dem kann man herausfinden, ob ein Objekt zu einer bestimmten Klasse (oder: Oberklasse davon) gehört. Er hat aber einen schlechten Ruf, weil er oft ein Anzeichen dafür ist, dass man nicht wirklich objektorientiert programmiert, weil sich seine Verwendung oft ersetzen lässt durch ordentliche Vererbung und überschriebene Methoden. Hier gibt es aber keine andere Lösung. Zugegeben: Man kann für Spielobjekt-Klassen ein eigenes Typ-Attribut einführen und es sich mit getTyp()
geben lassen, und das als Entscheidungskriterium hernehmen. Dann hat jedes Kokosnussobjekt ein Attribut String typ = "kokosnuss"
, was mir aber auch nicht gefällt. Aber immerhin käme ich so um das Erklären von instanceof herum.
So oder so habe ich ein Problem. Nehmen wir an, ich habe Patronen, mit einem Attribut int anzahl
, und Pistolen mit einem Attribut boolean geladen
. Es könnte auch ein Zauberstab mit Ladungen sein, oder Goldmünzen:
class Patrone extends Spielobjekt {
void anwendenAuf(Spielobjekt o) {
if (o instanceof Pistole) {
if (((Pistole) o).geladen == true) {
// nichts, Pistole ist bereits geladen
}
else {
((Pistole) o).geladen = true; // Pistole ist jetzt geladen
anzahl = anzahl-1; // der Patronenhaufen wird um 1 reduziert
if (anzahl == 0 ) {
entfernenAusInventar(this); // Patronenhaufen wird evtl. geloescht
}
}
}
}
} |
class Patrone extends Spielobjekt {
void anwendenAuf(Spielobjekt o) {
if (o instanceof Pistole) {
if (((Pistole) o).geladen == true) {
// nichts, Pistole ist bereits geladen
}
else {
((Pistole) o).geladen = true; // Pistole ist jetzt geladen
anzahl = anzahl‑1; // der Patronenhaufen wird um 1 reduziert
if (anzahl == 0 ) {
entfernenAusInventar(this); // Patronenhaufen wird evtl. geloescht
}
}
}
}
}
Wenn ich in anwendenAuf
auf das übergebene Objekt zugreifen möchte, muss ich einen Cast machen, also das Objekt manuell einer Unterklasse zuweisen. Das kann zu einem Fehler während des Ausführens führen, wenn das Objekt gar nicht zu dieser manuell zugewiesenen Klasse gehört. Wenn es gar nicht zu dieser manuell zugewiesenen Klasse gehören kann, wird bereits beim Compilieren/Übersetzen ein Fehler gemeldet. Dieses Casten ist für Schüler auch nicht leicht nachzuvollziehen, und auch eher schlechter Stil, wenn es denn anders geht. Aber: Es geht wohl nicht.
Später sieht das dann so aus:

Wenn man zuerst auf das Schwert und dann auf eine Kuh klickt, verschwindet die Kuh aus dem Inventar und drei Rindfleisch tauchen auf.
Hmmm… wenn man eine Methode anwendenAuf (Spielobjekt [])
einführt, kann man auch das Durchlaufen von Arrays üben. Mal sehen, wie ich das einbauen kann.
Fußnote:
Eine verlockende Sackgasse ist folgender Gedanke, mit dem der ursprüngliche Plan, verschiedene anwendenAuf
-Methoden zu haben, doch noch verwirklicht werden könnte, wenn es den ginge.
Class c = o.getClass(); // wird zur Laufzeit bestimmt
c oNeu = (c) o; // müsste beim Compilieren überprüft werden können
anwendenAuf(c) |
Class c = o.getClass(); // wird zur Laufzeit bestimmt
c oNeu = (c) o; // müsste beim Compilieren überprüft werden können
anwendenAuf(c)
Man lässt sich von dem Spielobjekt o die Klasse geben, und castet dann o auf seine tatsächliche Klasse. Aber das geht nun einmal nicht, compiliert nicht einmal. Kurz: Ich kann nicht auf eine Klassentyp casten, der in einer Variablen gespeichert ist, weil dann bei der Compilierung nicht überprüft werden kann, ob das überhaupt ein grundsätzlich möglicher Cast wäre. Denn in einer Variablen kann ja alles stecken. Das weiß man erst zur Laufzeit.
Weitere Designfragen:
(1) Diese Regeln, was geschehen soll, wenn man das Messer auf die Kokosnuss anwendet – wo sollen die hin? Man kann es machen wie oben beschrieben, dann ist jede Klasse zuständig für sich selber. Wenn eine neue Klasse (etwa: Hühnchen) eingeführt werden soll, muss man in allen Klassen, deren Objekte etwas mit den Objekten der neuen Klasse anfangen können sollen, den Code ändern. Das ist umständlich. Stattdessen könnte man eine einzige Klasse haben, die alle möglichen Kombinationen von anwendenAuf(Spielobjekt o1, Spielobjekt o2)
behandelt. Großer Vorteil: Führe ich neue Klassen ein, muss ich nur in einer Klasse Code ändern. Nachteil: Diese Methode wird ziemlich groß, da die Kombinationsmöglichkeiten mit der Anzahl der Klassen schnell steigen, auch wenn, zugegeben, nur die potentiell möglichen Kombinationen implmenetiert werden müssen. Bei 5 Typen gibt es maximal 25 Kombinationen, bei 10 Typen schon 100, das wächst geradezu… nein, nicht exponentiell, noch lange nicht, aber polynomiell/quadratisch. Und vor allem kann ich es dann nicht mehr so machen, dass jede Schülerin und jeder Schüler ihre eigenen Anwendungsregeln in eigenen Klassen erstellen.
(2) Richtig viel Arbeit kann ich mir machen, wenn ich Verb-Objekte einführe, also etwa eine Klasse Schneiden
, die enthält, was geschieht, wenn ich ein Objekt des Interface-Typs “KannSchneiden” auf ein Objekt des Interface-Typs “KannGeschnittenWerden” anwende. So ähnlich ist das zwar bei Sprachen für Textadventures, aber so gründlich will ich gar nicht sein.
(3) Wenn ich vorher weiß, welche Unterklassen von Spielobjekt es geben soll, dann kann ich in die Spielobjekt-Oberklasse 30 leere, gegebenenfalls zu überschreibende Methoden platziere: anwendenAuf(Kokosnuss k) {}, anwendenAuf(Palme p) {}
und so weiter. Dazu kommt dann eine zentrale Methode:
void anwendenAuf(Spielobjekt o) {
if (o instanceof Messer) anwendenAuf( (Kokosnuss) o);
else if (o instanceof Messer) anwendenAuf( (Palme) o);
} |
void anwendenAuf(Spielobjekt o) {
if (o instanceof Messer) anwendenAuf( (Kokosnuss) o);
else if (o instanceof Messer) anwendenAuf( (Palme) o);
}
Diese Methode verteilt die Aufrufe dann auf die glechnamigen Methoden mit den korrekten Argumenten. Dann greift wieder das overriding und damit dynamic binding. Dann kann ich wieder meine ursprüngliche Idee umsetzen – separate Methoden für jeden Spielobjekttyp. Allerdings muss ich halt jedesmal den Code in dieser einen Klasse anpassen, wenn eine neue Unterklasse von Spielobjekt eingeführt wird.
(4) Keine Lösung habe ich bisher für das Folgende: Wenn ich ein Messer auf eine Kokosnuss anwende, geschieht dann das gleiche, wie wenn ich eine Kokosnuss auf ein Messer anwende? Wenn ich die Patronen anwende auf die Pistole, soll das gleiche passieren wie bei der Anwendung der Pistole auf die Patronen? Wenn nein, kein Problem, dann passt alles. Wenn ja… die Messerklasse entscheidet nur, wenn das Messer angewendet wird. In welcher Klasse soll stehen, was geschieht, wenn ich die Kokosnuss anwende? Wenn das in Kokosnuss ist, führt das zu Code-Dopplung, und wo sonst? Hier fällt mir nur ein, doch Lösung (1) zu wählen.
Links, die zeigen, dass viele Leute schon vor dem gleichen Problem standen:
(Und die Linksammlung erklärt, warum es nervig ist, wenn der Webfilter der Schule stackoverflow.com sperrt.)