Abstrakte Klassen
Motivation für abstrakte Klassen
- Basisklassen vererben ihre Methoden (und Attribute) an ihre Subklassen
- Häufig weiß man, dass alle Subklassen einer Basisklasse eine bestimmte Methode haben werden
- Auf der Ebene der Basisklasse kennt man das Verhalten der Methode jedoch noch nicht
- Erst die Subklassen wissen (können festlegen), wie sich die Methoden genau verhalten sollen
- Man will aber Polymorphie nutzen und die Methoden daher bereits in der Basisklasse definieren
public void fuettern(Tier t) {
if (t instanceof Hund) {
((Hund) t).fressen();
}
else if (t instanceof Ente) {
((Ente) t).fressen();
}
}
Eigentlich können sich alle Tiere fortbewegen und fressen.
Hund h = new Hund();
fuettern(h);
Ente e = new Ente();
fuettern(e);
Dieser Code-Abschnitt zeigt eine Methode zum Füttern von Tieren, die den Anspruch hat für alle Arten von Tieren (in diesem Fall also zumindest Hund und Ente) zu funktionieren. Da die Methode fressen()
aber erst auf der Ebene der konkreten Tiere (Hund und Ente) auftritt, muss in der fuettern()
-Methode mühselig mit instanceof
geprüft werden, um welches Tier es sich handelt und mit einem Cast der Zugriff auf die entsprechende Methode hergestellt werden. Ein solches Vorgehen funktioniert zwar, ist aber ausgesprochen schlechter Programmierstil, da für jedes neue Tier, das eingeführt wird (z. B. Ziege), ein neuer else if-Zweig in der fuettern()
-Methode eingebaut werden muss. Dies hat zur Folge, dass ein grundlegendes Versprechen der objektorientierten Programmierung, nämlich die einfache Erweiterbarkeit durch Einführen neuer Subklassen nicht eingehalten werden kann: für jede neue Subklasse muss die fuettern()
-Methode erweitert (modifiziert) werden. Vergisst man eine entsprechende Erweiterung in der fuettern()
-Methode kommt es sogar zu dem Fall, dass ein neu eingeführtes Tier gar nicht gefüttert würde und einfach verhungert.
Wir brauchen also einen Mechanismus, um bereits bei Tier die Methoden festzulegen, die alle Subklassen haben sollten.
Schritt 1: Methoden hochziehen
Die Methoden können in die Basisklasse verlegt werden
public class Tier {
private int alter;
public void fortbewegen() { /* Tiere bewegen sich, aber wie? */ }
public void fressen() { /* Tiere fressen, aber wie? */ }
}
public class Hund extends Tier {
public void bellen() {
// Maul auf und los
}
}
public class Ente extends Tier {
public void schwimmen() {
// mit den Füßen paddeln
}
}
public void fuettern(Tier t) {
// Futter bereitstellen
t.fressen();
}
Hund h = new Hund();
fuettern(h);
Ente e = new Ente();
fuettern(e);
- Erst Hund und Ente, wissen wie die Methoden konkret aussehen sollen
- Alle Tiere können sich fortbewegen, aber wie genau?
- Alle Tiere können fressen, aber wie genau?
Ein erster Ansatz, um das beschriebene Problem zu lösen, wäre es die fressen()
- und fortbewegen()
-Methoden in die Basisklasse Tier
zu ziehen und so dafür zu sorgen, dass sie an alle Subklassen (alte wie neue) vererbt wird. Die fuettern()
-Methode kann dann ganz auf instanceof
verzichten und sicher davon ausgehen, dass alle Tiere eine entsprechende fressen()
-Methode haben.
Hier ergibt sich aber das Problem, dass wir auf der Ebene der Tier-Klasse nicht festlegen können, wie die einzelnen Tiere genau fressen.
- Obwohl Tier eine Abstraktion von konkreten Tieren darstellt (in unserem Modell gibt es zwar Enten und Hunde aber keine Tiere), kann man weiterhin Instanzen von Tier erstellen.
- Die Klasse Tier kann nicht wissen, wie sich Enten und Hunde fortbewegen und wie sie fressen. Man kann diese Methoden zwar auf allen Tieren aufrufen, das Verhalten passt aber nicht zum jeweiligen Tier, weil es von der Klasse Tier stammt.
Wir brauchen also eine Möglichkeit, für Ente und Hund (und alle weiteren Subklassen) das Verhalten der Methoden anzupassen.
Schritt 2: Überschreiben nutzen
- Subklassen überschreiben die Methoden der Superklasse
- Die Implementierung in der Superklasse (hier Tier) ist damit bedeutungslos, die Schnittstelle aber festgelegt
Das Problem, dass Tier das konkrete Verhalten von Enten und Hunden beim Fressen und Fortbewegen nicht kennen kann, können wir dadurch lösen, dass wir die Methoden in allen Subklassen überschreiben und mit dem gewünschten Verhalten der jeweiligen Tiere ersetzen. Durch den virtuellen Methodenaufruf wird gewährleistet, dass Verwender von Enten und Hunden bei einer Referenz vom Typ Tier
die richtigen Methoden der konkreten Tiere aufrufen.
public class Hund extends Tier {
public void bellen() { /* Maul auf und los */ }
@Override
public void fortbewegen() { /* auf allen Vieren dackeln */ }
@Override
public void fressen() { /* Maul auf und runter mit Futter */ }
}
public class Ente extends Tier {
public void schwimmen() { /* mit den Füßen paddeln */ }
@Override
public void fortbewegen() { /* watscheln */ }
@Override
public void fressen() { /* Tauchen und Schnäbeln */ }
}
Probleme von Schritt 2
- Es können Instanzen der Superklasse erzeugt werden, obwohl sie keine sinnvolle Implementierung der Methoden hat
- Subklassen können vergessen, die Methoden zu überschreiben
Hund h = new Hund();
h.fortbewegen();
// Es wird eine Instanz von Tier erstellt, obwohl es in der realen
// Welt keine Tiere, sondern nur Hunde und Enten gibt.
Tier t = new Tier();
Tier e = new Ente();
// Ente hat leider vergessen die fressen()-Methode zu überschrieben.
e.fressen();
Bei der Variante mit dem Überschreiben bleiben jedoch zwei grundlegende Probleme ungelöst:
- Es ist weiterhin möglich Objekte von Tier anzulegen, obwohl dies (wie weiter oben beschrieben) sinnlos ist und
- es kann vorkommen, dass Subklassen vergessen eine der beiden Methoden zu überschreiben, sodass doch noch die (leere) Implementierung von Tier zum Tragen kommt.
Uns fehlt also ein Sprachkonstrukt, mit dem wir Subklassen dazu zwingen können ausgewählte Methoden der Superklasse zu überschreiben und mit dem wir klarmachen können, dass es keine Objekte vom Typ der Superklasse geben soll.
Schritt 3: Implementierung weglassen
Wenn die Implementierungen der Methoden der Superklasse sowieso überschrieben werden, warum lässt man sie dann nicht ganz weg?
public class Tier {
private int alter;
public void fortbewegen() {
// mhmm... Tiere bewegen sich, aber wie?
}
public void fressen() {
// mhmm... Tiere fressen, aber wie?
}
}
Wenn die Implementierungen der Methoden der Superklasse sowieso überschrieben werden, warum lässt man sie dann nicht ganz weg?
public abstract class Tier {
private int alter;
public abstract void fortbewegen();
public abstract void fressen();
}
Wenn wir ohnehin alle Subklassen zwingen wollen, ausgewählte Methoden der Superklasse zu überschreiben, dann ist die konkrete Implementierung dieser Methoden egal, da sie ohnehin immer überschrieben werden. Es ist also nur konsequent, diese Implementierungen dann ganz wegzulassen.
Mithilfe von abstrakten Methoden (abstract methods) hat man die Möglichkeit, Methoden zu definieren, ohne eine Implementierung anzugeben. Zu erkennen ist das am fehlenden Methodenrumpf ({ ... }
), der durch ein Semikolon ersetzt wird. Weiterhin erfordert die Java-Syntax, dass man die Methoden mit dem Schlüsselwort abstract
kennzeichnet, obwohl eigentlich der fehlende Methodenrumpf ausgereicht hätte. Man hat sich aber für eine explizite Kennzeichnung entschieden, um ein versehentliches Vergessen der Implementierung erkennen zu können.
Schritt 3: Konsequenzen
- Methode ohne Implementierung = abstrakte Methode (abstract method)
- Klasse mit abstrakten Methoden = abstrakte Klasse (abstract class)
- abstrakte Methoden haben keine Implementierung
⇒ man kann sie nicht aufrufen, bilden nur eine Schnittstelle - Subklassen müssen die abstrakten Methoden implementieren
⇒ man kann das „überschreiben“ nicht mehr vergessen - man muss verhindern, dass abstrakte Methoden auf einer Instanz der abstrakten Klasse aufgerufen werden (hier Tier)
⇒ man verbietet, Objekte abstrakter Klassen zu erzeugen
Durch die Einführung abstrakter Klassen löst man sehr elegant das beschriebene Problem: Man kann die Methoden bereits auf Ebene der Superklasse deklarieren, gibt aber keine Implementierung an. Hierdurch wird das Vorhandensein der Methoden garantiert, das konkrete Verhalten festzulegen wird aber den Subklassen überlassen.
Sobald eine Klasse abstrakte Methoden hat, wird sie selbst zur abstrakten Klasse. Die Konsequenz ist, dass man keine Objekte (Instanzen) der abstrakten Klasse mehr erzeugen kann. So wird sicher verhindert, dass die Methoden ohne Implementierung aufgerufen werden, denn ohne Objekt, kann man keine Methoden auf diesem aufrufen.
Dadurch, dass man die Subklassen (über den Compiler) dazu zwingt, die abstrakten Methoden zu implementieren, kann man sicher sein, dass eine Implementierung vorhanden ist.
Die Lösung
- Die Methoden von Tier werden abstrakt gemacht
- Die Subklassen müssen die Methoden implementieren
Tier definiert abstrakte Methoden, die von Ente und Hund implementiert werden müssen.
Beispiel: Lösung mit abstrakter Klasse
public abstract class Tier {
private int alter;
public abstract void fortbewegen();
public abstract void fressen();
}
public class Hund extends Tier {
public void bellen() { /* Maul auf und los */ }
@Override
public void fortbewegen() { /* auf allen Vieren dackeln */ }
@Override
public void fressen() { /* Maul auf und runter mit dem Futter */ }
}
public class Ente extends Tier {
public void schwimmen() { /* mit den Füßen paddeln */ }
@Override
public void fortbewegen() { /* watscheln */ }
@Override
public void fressen() { /* abtauchen und Schnabel auf */ }
}
public class Verwender {
public void fuettern(Tier t) {
// Futter bereitstellen
t.fressen();
}
}
Abstrakte Klassen
- Abstrakte Methoden
- deklarieren nur eine Schnittstelle
- haben keinen Methodenrumpf
- werden durch das Schlüsselwort
abstract
gekennzeichnet - verhalten sich bei der Polymorphie wie normale Methoden
- Abstrakte Klassen
- haben abstrakte Methoden
- sind selbst abstrakt (Schlüsselwort
abstract
) - können als Typ für Variablen dienen (Polymorphie)
- erlauben keine Objekterzeugung
Man kann in Java abstrakte Methoden deklarieren, die nur eine Schnittstelle aber keine Implementierung definieren, d. h. sie enthalten keinen Methodenrumpf, z. B. public abstract int berechneZahlung();
Das Schlüsselwort abstract
muss in der Methodendeklaration angegeben werden; nur dann darf man den Methodenrumpf weglassen. Das Gegenteil einer abstrakten Methode (abstract method) ist übrigens die konkrete Methode
(concrete Method).
Eine Klasse, in der mindestens eine abstrakte Methode vorkommt, ist selbst wieder abstrakt und muss daher mit dem Schlüsselwort abstract
gekennzeichnet werden:public abstract class Mitarbeiter
Im Prinzip ist dieses zusätzliche abstract
in der Klassendefinition unnötig, da es der Compiler an den abstrakten Methoden erkennen könnte, trotzdem muss man es noch einmal hinschreiben.
Zu beachten ist, dass man abstrakte Klassen deklarieren kann, die keine abstrakten Methoden enthalten. Von diesen Klassen kann man dann keine Objekte erzeugen, es werden aber keine abstrakten Methoden an Subklassen weitergegeben.
public abstract class AbstrakteKlasse {
public int m() {
return 1;
}
}
Von abstrakten Klassen kann man keine Objekte erzeugen, was keinen Sinn ergeben würden, da für mindestens eine Methode keine Implementierung existieren würde. Natürlich können abstrakte Klassen neben den abstrakten Methoden normale Methoden enthalten.
Der gesamte Satz von Methoden (abstrakte und konkrete) wird weitervererbt. Hierdurch kann ein Verwender einer Referenz vom Typ einer abstrakten Klasse alle Methoden aufrufen.
Abstrakte Klassen und Vererbung
Subklassen abstrakter Klassen
- implementieren alle abstrakten Methoden oder
- implementieren nur einen Teil der abstrakten Methoden, müssen dann aber selbst wieder abstrakt sein
Abstrakte Methoden
- werden (einschließlich der Abstraktheit) weitervererbt
- können über mehrere Vererbungsstufen hinweg schrittweise implementiert werden
- können nur
public
oderprotected
sein
Die hier vorgestellten Regeln werden im Folgenden anhand eines konkreten Beispiels erläutert.
Würde man für abstrakte Methoden andere Sichtbarkeiten als public
oder protected
zulassen, könnte man abstrakte Klassen konstruieren bei denen die Subklassen gar nicht mehr in der Lage wären, eine Implementierung zur Verfügung zu stellen. Da dies offensichtlich Unsinn ist, hat man die Sichtbarkeit auf public
oder protected
eingeschränkt. Diese ist konsistent mit der Maßgabe, dass abstrakte Methoden den Vertrag (contract) einer Klasse nach außen darstellen. Ein Vertrag, den die Subklasse nicht sehen könnte, ist sinnlos.
Das Beispiel zeigt deutlich, wie die Vererbung von abstrakten Methoden funktioniert. Die abstrakte Klasse A
enthält zwei abstrakte Methoden (abstrakt1()
und abstrakt2()
) und eine konkrete Methode. Im Folgenden wird nur die Vererbung der abstrakten Methoden weiter beschrieben. Dass die konkreten Methoden vererbt werden, ist klar.
- Die Klasse
B
implementiert beide gerbten abstrakten Methoden und ist damit eine konkrete Klasse. - Die Klasse
C
implementiert nur eine abstrakte Methode, nämlichabstrakt1()
. Somit fehlt die Implementierung vonabstrakt2()
wodurchC
selbst wieder abstrakt ist, da nicht alle geerbten abstrakten Methoden implementiert wurden. - Die Klasse
D
erbt vonC
die abstrakte Methodeabstrakt2()
. Da sie diese nicht implementiert, ist sie selbst wieder abstrakt. - Die Klasse
E
erbt vonD
die abstrakte Methodeabstrakt2()
und implementiert sie, wodurch sie zu einer konkreten Klasse wird. - Die Klasse
F
implementiert keine einzige der beiden ererbten abstrakten Methoden und ist dadurch selbst abstrakt. - Die Klasse
G
erbt vonF
zwei abstrakte Methoden, implementiert sie aber beide und ist dadurch eine konkrete Klasse.
public abstract class A {
public abstract void abstrakt1();
public abstract void abstrakt2();
public void konkret() {
System.out.println("Konkret Alter!");
}
}
public class B extends A {
public void abstrakt1() {
System.out.println("Abstrakt 1 aus B");
}
public void abstrakt2() {
System.out.println("Abstrakt 2 aus B");
}
}
A b = new B();
b.konkret(); b.abstrakt1(); b.abstrakt2();
Konkret Alter!
Abstrakt 1 aus B
Abstrakt 2 aus B
public abstract class C extends A {
public void abstrakt1() {
System.out.println("Abstrakt 1 aus C");
}
}
public abstract class D extends C { /* empty */ }
public class E extends D {
public void abstrakt2() {
System.out.println("Abstrakt 2 aus E");
}
}
A e = new E(); e.konkret(); e.abstrakt1(); e.abstrakt2();
Konkret Alter!
Abstrakt 1 aus C
Abstrakt 2 aus E
public abstract class F extends A { /* empty */ }
public class G extends F {
public void abstrakt1() {
System.out.println("Abstrakt 1 aus G");
}
public void abstrakt2() {
System.out.println("Abstrakt 2 aus G");
}
}
A g = new G(); g.konkret(); g.abstrakt1(); g.abstrakt2();
Konkret Alter!
Abstrakt 1 aus G
Abstrakt 2 aus G
Abstrakte Klassen und Konstruktoren
- Auch abstrakte Klassen brauchen Konstruktoren – obwohl man von ihnen keine Objekte erzeugen kann
- Konstruktoren abstrakter Klassen dienen dazu, die Attribute der Klasse zu initialisieren
- Es gelten die allgemeinen Regeln für Konstruktoren
- Ist kein Konstruktor deklariert, wird vom Compiler ein Default-Konstruktor generiert
- In der ersten Zeile des Konstruktors steht entweder ein
this()
- oder einsuper()
-Aufruf - Wird kein
this()
odersuper()
angegeben, generiert der Compiler ein implizitessuper()
- In mindestens einem Konstruktor steht ein
super()
-Aufruf
public abstract class Mitarbeiter {
protected String name;
protected double gehalt;
protected Date geboren;
public Mitarbeiter(String name, double gehalt, Date geboren) {
this.name = name;
this.gehalt = gehalt;
this.geboren = geboren;
}
public Date getGeburtsdatum() {
return geboren;
}
protected abstract int berechneZahlung();
}
Das Template Method Pattern
Das Template Method Pattern ist ein Entwurfsmuster, das dazu verwendet wird, die Struktur einer Algorithmus-Implementierung festzulegen, während die Schritte des Algorithmus selbst von Unterklassen definiert werden. Es ermöglicht, dass eine abstrakte Superklasse einen allgemeinen Algorithmus definiert, der aus einer Reihe von Schritten besteht, von denen einige von Unterklassen implementiert werden müssen.
Im Template Method Pattern hat die abstrakte Superklasse eine Methode, die als Template Method bezeichnet wird, die die Schritte des Algorithmus in der richtigen Reihenfolge aufruft. Einige dieser Schritte sind in der abstrakten Superklasse definiert und können von Unterklassen genutzt werden, während andere Schritte als abstrakte Methoden in der Superklasse definiert sind und von Unterklassen implementiert werden müssen.
public abstract class Mitarbeiter {
public final double bonus() {
return zielbonus() * zielerreichung();
}
public abstract double zielerreichung();
public abstract double zielbonus();
}
public class Angestellter extends Mitarbeiter {
private double zielbonus = 1000.0;
private double zielerreichung = 0.98;
public double zielbonus() {
return zielbonus;
}
public double zielerreichung() {
return zielerreichung;
}
}
Innerhalb der abstrakten Klasse können die abstrakten Methoden von konkreten Methoden genutzt werden, sodass man über das Template Method-Pattern eine bestimmte Implementierung eines Algorithmus vorgeben kann, in den sich die Subklassen durch Implementierung der abstrakten Methoden einhängen können.
public class Vorstand extends Mitarbeiter {
private double firmengewinn = 300000.0;
public double zielbonus() {
return 0.5 * firmengewinn;
}
public double zielerreichung() {
return 1.00;
}
}
Abstrakte Klassen: Zusammenfassung
Abstrakte Klassen
- Fördern die Trennung von Schnittstelle und Implementierung
- Legen als Basisklassen Eigenschaften von Subklassen fest, ohne die Implementierung vorgeben zu müssen
- Erlauben durch Polymorphie und virtuellen Methodenaufruf elegante objektorientierte Konstruktionen
- Sind echte Superklassen: es besteht eine ist-eine (is-a) Beziehung zwischen Super- und Subklasse
Eine abstrakte Klasse ist eine spezielle Art von Klasse in der Programmierung, die nicht direkt instanziiert werden kann. Sie dient als Vorlage für andere Klassen, die von ihr abgeleitet werden. Eine solche Klasse enthält in der Regel abstrakte Methoden, die von den abgeleiteten Klassen implementiert werden müssen. Sie kann auch konkrete Methoden und Felder enthalten, die von den abgeleiteten Klassen genutzt werden können.