6.11 Abstrakte Klassen
 
Nicht immer soll eine Klasse sofort ausprogrammiert werden. Dies ist der Fall, wenn die Oberklasse lediglich Methoden für die Unterklassen vorgeben möchte, aber nicht weiß, wie sie diese implementieren soll. In Java gibt es dazu zwei Konzepte: abstrakte Klassen und Schnittstellen (engl. interfaces).
6.11.1 Abstrakte Klassen
 
Bisher haben wir Vererbung eingesetzt und jede Klasse konnte Objekte bilden. Das ist allerdings nicht immer sinnvoll, nämlich genau dann, wenn eine Klasse nur in einer Vererbungshierarchie existieren soll. Sie kann dann als Modellierungsklasse eine Ist-eine-Art-von-Beziehung ausdrücken und Signaturen für die Unterklassen vorgeben. Eine Oberklasse besitzt dabei Vorgaben für die Unterklasse, das heißt, alle Unterklassen erben die Methoden. Ein Exemplar der Oberklasse selbst muss nicht existieren.
Um das in Java auszudrücken, deklarieren wir die Oberklasse mit dem Modifizierer abstract. Von dieser Klassen können dann keine Exemplare gebildet werden. Ansonsten verhalten sich die abstrakten Klassen wie normale, sie enthalten die gleichen Eigenschaften und können auch selbst von anderen Klassen erben.
Beispiel Eine abstrakte Klasse Kleidung ist die Oberklasse für konkrete Kleidungsstücke.
abstract class Kleidung
{
public void wasche() {
System.out.println( "Werde sauber" );
}
public void trockne() {
System.out.println( "Werde trocken" );
}
}
|
Mit dieser abstrakten Klasse Kleidung drücken wir aus, dass es eine allgemeine Klasse ist, zu der keine konkreten Objekte existieren. Es gibt in der realen Welt schließlich keinen Gegenstand, der nur ein allgemeines, unspezifiziertes Kleidungsstück ist, sondern nur spezielle Unterarten von Kleidungsstücken, zum Beispiel eine Hose oder eine Socke. Es macht also keinen Sinn, ein Exemplar der Klasse Kleidung zu bilden. Die Klasse soll nur in der Hierarchie auftauchen, um alle konkreten Kleidungsklassen als Typ von Kleidung darzustellen. Das zeigt, dass Oberklassen allgemeiner gehalten sind und Unterklassen weiter spezialisieren. Ein Versuch, ein Objekt der abstrakten Klasse zu bilden, führt zu einem Compilerfehler.
Die abstrakten Klassen werden normal in der Vererbung eingesetzt. Eine Klasse kann die abstrakte Klasse erweitern und auch selbst wieder abstrakt sein.
Beispiel Wenn eine Klasse von einer abstrakten Klasse erbt, dann ist sie vom Typ aller Oberklassen, auch vom Typ der abstrakten Klasse.
class Hose extends Kleidung
{
}
Das heißt, jemand könnte problemlos schreiben:
Hose h1 = new Hose();
Kleidung h2 = new Hose();
Kleidung ks[] = new Kleidung[]{ new Hose(), new Hose() };
|
6.11.2 Abstrakte Methoden
 
Das Schlüsselwort abstract leitet die Definition einer abstrakten Klasse ein. Eine Klasse kann ebenso abstrakt sein wie eine Methode1
. Eine abstrakte Methode definiert lediglich die Signatur, und eine Unterklasse implementiert dann irgendwann diese Methode. Die Klasse ist dann nur für den Kopf der Methode zuständig, während die Implementierung an anderer Stelle erfolgt. Durch abstrakte Methoden wird ausgedrückt, dass die Oberklasse keine Ahnung von der Implementierung hat und dass sich die Unterklassen darum kümmern müssen.
Beispiel Eine vollständig abstrakte Klasse mit nur einer abstrakten Methode.
abstract class Material
{
abstract int gewicht();
}
Die Definition einer abstrakten Methode wird mit einem Semikolon abgeschlossen.
|
Abbildung 6.6
Abstrakte Methoden werden kursiv dargestellt
|
Vererben von abstrakten Methoden
Wenn wir von einer Klasse abstrakte Methoden erben, so haben wir zwei Möglichkeiten:
1. |
Wir überschreiben alle abstakten Methoden und implementieren sie. Dann kann die erbende Klasse gegebenenfalls korrekt angelegt werden. |
2. |
Wir überschreiben die abstrakte Methode nicht, sodass sie normal vererbt wird. Das bedeutet, eine abstrakte Methode bleibt in unserer Klasse und die Klasse muss wiederum abstrakt sein. |
Beispiel Eine Hierarchie von Tieren wird aufgebaut. Die abstrakte Oberklasse Tier schreibt allen Tieren vor, dass sie eine Anfragemethode istSäuger() und eine Methode ausgabe() implementieren müssen. Es ist einleuchtend, dass die Oberklasse nichts über konkrete Tiere weiß und dass das ein Job der Unterklassen ist. Zunächst wieder das UML-Diagramm. Die Umlaute sind ersetzt.
|
Listing 6.29
AbstractDemo.java, Teil 1
abstract class Tier
{
int alter = -1;
void alterSetzen( int a ) { alter = a; }
abstract boolean istSäuger();
abstract void ausgabe();
}
abstract class Säugetier extends Tier
{
boolean istSäuger() { return true; }
}
class Mensch extends Säugetier
{
void ausgabe() {
System.out.println( "Ich bin ein Mensch" );
}
}
class Delfin extends Säugetier
{
void ausgabe() {
System.out.println( "Ich bin ein Delfin" );
}
}
ausgabe() ist eine Methode, die für die jeweiligen Implementierungen eine kurze Meldung auf dem Schirm ausgibt. Da alle erweiternden Klassen jeweils andere Zeichenketten ausgeben, setzen wir die Methode abstract. Damit muss aber auch die Klasse Tier abstrakt sein. In der ersten Ableitung Säugetier können wir nun Beliebiges hinzufügen, implementieren aber aufgrund der nun bekannten Informationen nur die Methode istSäuger(), die Methode ausgeben() jedoch noch nicht. Auch Säugetier muss wieder abstract sein. Die dritte Klasse ist nun Mensch. Sie erweitert Säugetier und liefert eine Implementierung für ausgabe(). Damit muss sie nicht mehr abstrakt sein.
Es ist durch die automatische Typanpassung ohne weiteres möglich, einem Tier-Objekt eine Referenz für ein Mensch- beziehungsweise Säugetier-Objekt zuzuweisen. Also ist Folgendes richtig:
Tier m = new Mensch(),
d = new Delfin();
Wird ein Mensch- oder Delfin-Objekt erzeugt, so wird der Konstruktor dieser Klassen aufgerufen. Dieser bewirkt einen Aufruf des Konstruktors der Superklasse. Und obwohl diese abstract ist, besitzt sie wie alle anderen Klassen einen Standard-Konstruktor (nur, dass wir ihn nicht mit new direkt aufrufen können). Des Weiteren werden beim Aufruf von Mensch() auch noch die Attribute initialisiert, sodass alter auf -1 gesetzt wird.
Beispiel Wir rufen ausgabe() von einem Mensch- und Delfin-Objekt auf.
|
Listing 6.30
AbstractDemo.java, Teil 2
public class AbstractDemo
{
public static void main( String args[] )
{
Tier m = new Mensch(),
d = new Delfin();
m.ausgabe();
d.ausgabe();
}
}
Das Programm liefert:
Ich bin ein Mensch
Ich bin ein Delfin
6.11.3 Über abstract final
 
Wenn wir eine Klasse als final deklarieren, so bedeutet dies, dass es von dieser Klasse keine Unterklassen geben kann. Objekte können immer noch erzeugt werden. Deklarieren wir eine Klasse dagegen als abstract, so ist diese Klasse meist für die Vererbung vorgesehen und kann nicht mit dem new-Operator zum Exemplar gebracht werden. Nehmen wir an, wir wollten eine Klasse konstruieren, von der es weder Exemplare geben darf (abstract) noch aus der abgeleitet werden kann (final). Deshalb kämen wir auf die Idee, einfach abstract final zu schreiben. Doch der Compiler meldet in diesem Fall:
A class may not be declared both "final" and "abstract".
Die Sprache verbietet, dass eine Klasse zugleich abstract und final sein kann. Doch es gibt Beispiele für Klassen – etwa Math –, wo das sinnvoll wäre. Daher müssen wir zu einem Trick greifen. Die Klasse wird zunächst als final deklariert, sodass es keine Unterklassen geben kann. Damit über den Konstruktor keine Exemplare gebildet werden können, implementieren wir einen privaten Standard-Konstruktor, so ist das Problem gelöst. Jetzt kann die Klasse nur Exemplare von sich selbst anlegen. Diese Klasse stattdessen als abstract zu deklarieren, würde das Problem ebenfalls lösen. Zwar könnte jemand versuchen, Unterklassen zu definieren, doch wenn wir wieder einen privaten Standard-Konstruktor einfügen, lassen sich keine Unterklassen mehr definieren, da kein Konstruktor aus der Oberklasse sichtbar ist. Damit ist die Aufrufkette der Konstruktoren unterbrochen. Bei einer finalen Klasse mit privatem Konstruktor erlauben wir allerdings der Klasse selbst, noch ein Objekt anzulegen. Das ist dann die beste Lösung. Ein Design-Pattern mit dem Namen »Singleton« macht genau dies. Möglicherweise wird es in Zukunft in Java auch abstract final-Klassen geben.
1
Während in Java eine Klasse abstract definiert wird, wird in EIFFEL ein Unterprogramm als deferred gekennzeichnet. Das heißt, die Implementierung wird aufgeschoben.
|