Advanced   Java   Services Vererbung 1 Back Next Up Home

Wir wollen nun unsere Klasse um eine Telefonnummer ergänzen. Hierzu gibt es verschiedene Möglichkeiten. Ein Ansatz wurde in einer Übung verfolgt. Hier haben wir einfach den Dartenteil entsprechend erweitert. Dieser Ansatz hat den Nachteil, daß damit die alte Klasse auffhört zu existieren. Es gibt Situationen, wo man das nicht haben will. Und auch Situationen, wo das nicht geht, weil etwa mit der Erweiterung ein anderer Entwickler befaßt ist und man den Quellcode nicht ändern will oder kann. Hier bieten sich die folgenden Ansätze an.


Der "hat ein" Ansatz  ( has a )
public class PersTel
{
   // Membervariablen
   private Person p ;
   private String tel;

   //....
}

Die neue Klasse verwendet im Datenteil ein Objekt der Klasse Person. Das ist ein "hat ein - Ansatz". Die neue Klasse hat im Datenteil ein Objekt der Klasse Person zur Verfügung. Vorteil ist, daß die alte Klasse in ihrer Form bestehen bleibt. Man hat nun zwei unabhängige Klassen Person und PersTel. Zudem ist für diesen Ansatz kein Ausbau der Theorie erforderlich. Ein Nachteil dabei ist allerdings die aufwendige Implementierung von Methoden, die im folgenden nur angedeutet ist:

public class PersTel
{
   // Membervariablen
   private Person p ;
   private String tel;

   // Konstruktoren
   public PersTel()
   {
      p = new Person() ;
      tel = "" ;
   }
   public PersTel(String v, String n, String t)
   {
      p = new Person(v, n) ;
      tel = t;
   }

   // Methoden
   public String getVorName()
   {
      return p.getVorname();
   }
   public String getNach()
   {
      return p.getNachname();
   }
   public String getTelnum()
   {
      return tel;
   }
   public void println()
   {
      System.out.println(p.getVorname() + " " + p.getNachname() + " " + tel);
   }

   // ...
}

Obwohl es die Methoden getVorname() und getNachname() bereits gibt, können sie nicht auf Objekte der Klasse PersTel angewendet werden, da Person und PersTel zwei völlig verschiedene Datentypen sind. Sie müssen daher für die Klasse PersTel neu geschrieben werden.


Der "ist ein" Ansatz  ( is a )

Durch Vererbung können die Nachteile des obigen Ansatzes vermieden werden. Die Klasse PersTel wird durch Vererbung ein Spezialfall der Klasse Person. Dadurch wird die erbende Klasse PersTel typverwandt zur Klasse Person und die in Person vorhandenen Methoden können von der erbenden Klasse (wenn diese es zuläßt) verwendet werden. Im Gegensatz zur landläufigen Vorstellung von Vererbung bleibt hier der Erblasser am Leben. Durch das neue Schlüsselwort extends läßt sich diese Situation verblüffend einfach modellieren.

public class PersTel extends Person
{
   // Membervariablen
   private String tel;

   //....
}

Wir wollen hier gleich die gebräuchlichsten Bezeichnungen vorstellen, die für diesen Ansatz verwendet werden.

  beerbte Klasse  
  (Person)
  Elternklasse    Vaterklasse    Oberklasse    Basisklasse    parentclass    superclass    baseclass  
  erbende Klasse  
  (PersTel)
  Kindklasse    Sohnklasse    Unterklasse    Ableitung    childclass    subclass   

Die graphische Darstellung der Vererbung sieht folgendermaßen aus. Das Rechteck für die Oberklasse wird oberhalb oder links von der Ableitung plaziert. Der Pfeil geht von der Ableitung zur Oberklasse. Das Wort extends wird in der Regel weggelassen, der durchgezogene Pfeil symbolisiert die Vererbung.

vererb2.jpg

Bereits jetzt kann man mit dem Defaultkonstruktor Objekte vom Typ PersTel erstellen und diese Objekte dürfen alle Methoden der Klasse Person verwenden, sofern diese public sind, wie das folgende Beispiel zeigt.

public class PersTelDemo1
{
   public static void main(String args[])
   {
      PersTel pt = new PersTel();
      pt.setVorname("Kurt");
      pt.setNachname("Tucholsky");
      pt.println();
   }
}

Daß dies funktioniert, zeigt uns, daß ein Objekt der Klasse PersTel ein sogar initialisiertes Objekt der Klasse Person enthalten muß, sonst könnten die Methoden setVorname(), setNachname() und println() ja nicht fehlerfrei arbeiten. Im Gegensatz zum "hat ein" - Ansatz ist dieses Person-Objekt jedoch nicht sichtbar. Da der Datenteil von Person private ist, kann auch die erbende Klasse PersTel nur mit Hilfe der public-Methoden aus Person auf diesen geerbten Datenteil zugreifen. Dieser Schutz ist gewollt, der Entwickler der Klasse Person soll bestimmen können, was durch Vererbung weitergegeben wird und was nicht. Wir bauen nun unsere Klasse PersTel analog zum "hat ein" - Ansatz aus.

public class PersTel extends Person
{
   private String tel;

   // Konstruktoren
   public PersTel()
   {
      tel = "" ;
   }

   // Methoden
   public String getTelnum()
   {
      return tel;
   }
   public void setTelnum(String ph)
   {
      tel = ph;
   }

   public void println()
   {
      System.out.println( this.getVorname() + " " + this.getNachname() + " " + tel);
   }

}

Es sei gleich darauf hingewiesen, daß im Gegensatz zu public-Methoden Konstruktoren, auch wenn sie public sind, nicht vererbt werden. Da Konstruktoren speziell für die Initialisierung von Objekten ihres Typs da sind, müssen sie stets neu geschrieben werden. Man kann jedoch auf die Konstruktoren der Elternklasse Bezug nehmen.


Überschreiben von Methoden  ( Polymorphie, method overriding )

Zu den Methoden getTelnum() und setTelnum(String ph) gibt es nichts neues zu sagen. Interessanter ist da schon die Methode println(). Obwohl wir bereits eine Methode dieses Namens erben, haben wir sie hier neugeschrieben, was ohne weiteres zulässig ist. Diesen Vorgang nennt man überschreiben von Methoden (method overriding). Da die geerbte Methode keine Telefonnummer ausgeben kann, haben wir uns zu diesem Schritt entschlossen. Bei einem Aufruf der Form this.getVorname() greift getVorname() auf den von Person geerbten Datenteil im Objekt this zu. Wie immer kann man ein this vor einem Punkt auch weglassen.


overriding versus overloading

Am Anfang verwechselt man das überladen von Methoden leicht mit dem überschreiben von Methoden.

Diese Möglichkeit, in einer Unterklasse die gleiche Methodensignatur mit einem neuen Inhalt zu versehen, nennt man auch Polymorphie (griech. Vielgestaltigkeit).


Der super-Mechanismus für Methoden

Unsere überschreibung der println()-Methode erscheint umständlich und redundant. In einer Übungsaufgabe haben wir die Klasse Person ergänzt um eine Methode print(), die keinen Zeilenvorschub macht. Diese können wir jetzt wie folgt einsetzen.

public class PersTel extends Person
{
   private String tel;

   // Konstruktoren
   // ...

   // Methoden
   // ...

   public void println()
   {
      print() ;
      System.out.println(" " + tel);
   }
}

Leider können wir die gleiche Idee nicht verwenden, um die Methode print() selbst zu überschreiben:

public class PersTel extends Person
{
   private String tel;

   // Konstruktoren
   // ...

   // Methoden
   // ...

   public void println()
   {
      print() ;                           // 1
      System.out.println(" " + tel);
   }

   public void print()
   {
      print() ;    // das geht nicht      // 2
      System.out.print(" " + tel);
   }

}

In dieser Situation bezieht sich nämlich der Aufruf von print() nicht mehr auf das geerbte print(), sondern auf das eigene print(). Der Compiler sucht eine Methode immer zuerst in der eigenen Klasse und dann erst in der Elternklasse. Der Aufruf //2 ist damit eine Endlosrekursion. Die Ergänzung einer eigenen print()-Methode verändert auch das Verhalten der darüberstehenden println()-Methode. Der Aufruf von print() bezieht sich jetzt ebenfalls auf das eigene print() und wir landen wieder in einer Endlosrekursion. Es gibt jedoch eine Möglichkeit, den Namenskonflikt aufzulösen. Mit dem neuen Schlüsselwort super bringen wir dem Compiler bei, die print()-Methode der Elternklasse aufzurufen:

public class PersTel extends Person
{
   private String tel;

   // Konstruktoren
   public PersTel()
   {
      tel = "" ;
   }

   // Methoden
   // ...

   public void println()
   {
      super.print() ;   // das geht      // 1
      System.out.println(" " + tel);
   }

   public void print()
   {
      super.print() ;  // das geht       // 2
      System.out.print(" " + tel);
   }

}

Der super-Mechanismus für Konstruktoren

Wir haben bereits eingangs festgestellt, daß unser mit dem Defaultkonstruktor initialisiertes PersTel-Objekt einen initialisierten Person-Teil enthält. Das kann man sich nur so erklären, daß bei der Initialisierung des PersTel-Objektes auch der Person-Teil initialisiert ist. Der Compiler ergänzt nämlich automatisch als erstes Statement eines Konstruktors einen heimlichen Aufruf des Defaultkonstruktors der Elternklasse.

public class PersTel extends Person
{
   private String tel;

   // Konstruktoren
   public PersTel()
   {
      // versteckter Aufruf des Defaultkonstruktors von Person
      tel = "" ;
   }

}

Man kann diesen Aufruf aber auch selbst schreiben. Damit hat man die Möglichkeit, auch andere Konstruktoren der Elternklasse zu aktivieren. Die Konstruktoren der Elternklasse werden dabei aus der Kindklasse heraus immer mit dem Namen super aufgerufen. Zudem muß ein super-Aufruf immer das erste Statement in einem Konstruktor sein. Wir zeigen dies im folgenden Beispiel.

public class PersTel extends Person
{
   private String tel;

   // Konstruktoren
   public PersTel()
   {
      super() ;  // expliziter Aufruf des Defaultkonstruktors von Person
      tel = "" ;
   }
   public PersTel(String n)
   {
      super(n) ;  // expliziter Aufruf des Konstruktors Person(String n)
      tel = "" ;
   }
   public PersTel(String v, String n)
   {
      super(v, n) ;  // expliziter Aufruf des Konstruktors Person(String v, String n)
      tel = "" ;
   }

   // die letzteren als übung

   public PersTel(String v, String n, String t)
   {
      super(v, n) ;  // expliziter Aufruf des Konstruktors Person(String v, String n)
      tel = t ;
   }
   public PersTel(Person p)
   {
      super(p) ;  // expliziter Aufruf des Konstruktors Person(Person p)
      tel = "" ;
   }
   public PersTel(Person p, String t)
   {
      super(p) ;  // expliziter Aufruf des Konstruktors Person(Person p)
      tel = t ;
   }
   public PersTel(PersTel pt)
   {
      super(pt) ;  // expliziter Aufruf des Konstruktors Person(Person p)
      tel = pt.tel ;
   }

}

Typverwandtschaft durch Vererbung

Ein weiterer Vorteil der Vererbung ist die dadurch entstehende Typverwandtschaft. Sie kann verglichen werden mit der Typverwandtschaft bei primitiven Datentypen. So kann man ja stets zum breiteren primitiven Datentyp hin zuweisen, also ist //1 ok, aber //2 falsch

int a = 17;
double x = 13;

x = a ;     // zulässig         //1
// a = x ;  // unzulässig       //2

Person p = new Person() ;
PersTel pt = new PersTel() ;

pt = p ;     // unzulässig      //3
p  = pt ;    // zulässig        //4

Ein wenig raffinierter ist die Sache jedoch bei Objekten. Es überrascht zunächst, daß die Zuweisung //3 falsch ist, wo doch PersTel in gewissem Sinne als der umfassendere Datentyp angesehen wird. Wäre jedoch diese Zuweisung erlaubt, dann würde die Referenz pt auf ein Objekt der Klasse Person zeigen, was beim Aufruf etwa der Methode getTelnum() fatale Folgen hat, existiert doch im Person-Objekt überhaupt keine Telefonnummer. Zeigt dagegen wie in //4 ein Zeiger vpm Typ Person auf ein PersTel- Objekt ist die Datenkonsistenz gewährleistet. Alle Methoden, die Person zur Verfügung hat können auf das Objekt vom Typ PersTel angewandt werden. Es ist allerdings nicht möglich über den Zeiger p etwa auf die Telefonnummer zuzugreifen, da es in Person keine Methoden dazu gibt. Man kann also über die Referenz p fehlerfrei zugreifen, kann aber nicht alle Fähigkeiten des Objekts ausschöpfen. In diesem Zusammenhang nennt man p gerne einen Basiszeiger. Die Typverwandtschaft über Vererbung ist die einzige Verwandtschaft, die es bei Klassen gibt. Klassen, die nicht in einer direkten Vererbungslinie stehen, sind nicht verwandt. Zum besseren Verständnis betrachten wir dazu einen Teil der überschaubaren Hierarchie zu den Klassen String, StringBuilder und StringBuffer, die wir im nächsten Abschnitt besprechen werden.

CharSequenceHier.jpg

Die beiden wichtigsten Punkte zur Typverwandtschaft von Klassen lauten:

So sind String und StringBuffer offensichtlich thematisch verwandt aber eben nicht typverwandt ! Dagegen sind alle Klassen der unteren Ebene typverwandt mit CharSequence. Diese Verwandtschaft äußert sich in einer Zuweisungskompabilität von unten nach oben. Referenzen einer unteren Ebene können Referenzen einer höheren Ebene zugewiesen werden aber nicht umgekehrt, d.h.

Wer die API genau studiert wird Einwenden, daß CharSequence doch ein Interface ist. Da ein Interface aber eine spezielle Form einer Klasse ist (siehe Interfaces) gelten in diesem Punkt die gleichen Regeln.


Späte Bindung  (late binding)
Person p ;
PersTel pt = new PersTel("Alois", "Hinterhuber", "123456") ;

//pt = p ;     // unzulässig      //3
p  = pt ;    // zulässig        //4

p.println();  // gibt tatsächlich auch die Telefonnummer aus

Ein sehr schönes feature objektorientierter Sprachen ist der Mechanismus der "späten Bindung", den Java von C++ übernommen hat. Wir haben gerade gesehen, daß eine Basisreferenz auf ein Objekt eines abgeleiteten Typs zeigen kann. Wenn man nun zu der Basisreferenz p die Methode println() aufruft, so möchte man annehmen, daß die println()-Methode aus der Klasse Person gerufen wird und damit keine Telefonnummer ausgegegeben wird. Tatsächlich wird aber über p die println()-Methode der Klasse PersTel gerufen und damit auch die Telefonnummer ausgegeben.

Alois Hinterhuber
123456

Diese erstaunliche Tatsache nennt man späte Bindung. Erst zur Laufzeit wird entschieden, welche println() Methode verwendet wird. Der Basiszeiger erkennt den Typ der rechten Seite und dann wird in der Vererbungshierarchie aufwärts nach der println()-Methode gesucht. In unserem Fall wird sie da auch sofort gefunden. Man kann es sich so merken:

Beim compilieren wird zunächst nur untersucht, ob eine Methode println() in der Basisklasse existiert. Ist dies nicht der Fall, bricht der Compiler mit einer Fehlermeldung ab. Damit wird sichergestellt, daß zumindest in der Basisklasse die entsprechende Methode existiert. Erst zur Laufzeit greift dann der oben beschriebene Suchmechanismus für die Methode println(). Da nun sichergestellt wurde, daß in der Basisklasse die gesuchte Methode existiert, arbeitet der Suchalgorithmus nie erfolglos. Das folgende Beispiel soll das nochmal erläuterm.

public class LateBinding
{
   public static void main(String[] args)
   {
      A base, a = new A();
      //B b = new B();   wird nicht gebraucht !
      C c = new C();

      base = c ;
      base.println();
   }
}

class A
{
   public void println()
   {
      System.out.println("println() von A");
   }
}

class B extends A
{
   public void println()
   {
      System.out.println("println() von B");
   }
}

class C extends B
{

}

Die Klasse C erbt von B und diese wiederum von A. A und B enthalten println()-Methoden, die sich selbst dokumentieren. C enthält keine println()-Methode. In main() legen wir uns ein Objekt vom Typ A und vom Typ C an und vereinbaren noch einen Basiszeiger vom Typ A, den wir auf das Objekt c vom Typ C zeigen lassen. Das Beispiel läßt sich compilieren und aufrufen und liefert die Zeile "println() von B".


Vererbungshierarchien und Mehrfachvererbung

Mit der Klasse PersTel muß die Vererbungslinie keineswegs zu Ende sein. Es ist ohne weiteres möglich, von der Klasse PersTel eine weitere oder sogar mehrere Ableitungen zu bilden. Eine Klasse kann beliebig viele Kindklassen haben. Umgekehrt gibt es aber in Java keine Mehrfachvererbung bei Klassen, eine Klasse kann also nur eine einzige Elternklasse haben. Das ist die gleiche Struktur wie bei Verzeichnissen. Ein Verzeichnis kann mehrere Unterverzeichnisse haben, aber ein Verzeichnis hat genau ein Oberverzeichnis. Die Vererbungshierarchie der AWT-Klassen für die graphische Oberfläche finden Sie unter dem folgenden Link. Hier stehen die Eltern links und die Kinder rechts.

AWTKlassenHierarchie


Vererbung verbieten mit final

Der Vererbungsmechanismus ist grundsätzlich ein offener Mechanismus. Jemand findet eine Klasse, die ein anderer geschrieben hat, für seine Zwecke brauchbar. Allerdings möchte er sie noch genau auf seine Bedürfnisse zuschneiden. Mit Vererbung ist dies möglich. Der Entwickler ergänzt dann Daten und Methoden, wie er es für notwendig hält. Nicht immer will man seine Klassen jedoch einfach an Dritte weitergeben. Möchte der Entwickler einer Klasse verhindern, daß seine Klasse Kinder bekommt, so kann er dies sehr einfach verhüten. Mit dem Schlüsselwort final kann er eine Vererbung generell verbieten. Im Falle unserer Klasse Person würde das so aussehen:

public final class Person
{
   // ...
}

Der Versuch, diese Klasse zu beerben, also

public class PersTel extends Person
{
   // ...
}

endet dann mit der Fehlermeldung "cannot inherit from final Person". Beispiele aus der API sind etwa die Klassen String und StringBuffer oder auch die Klasse System .


Überschreiben verbieten mit final

Man muß nicht gleich das Vererben pauschal unterbinden, man kann mit final auch subtiler arbeiten und nur einzelne Methoden verbieten. Dazu setzt man das Schlüsselwort in der Signatur der Methode ebenfalls nach dem Schlüsselwort public.

public class Person   // Klasse ist nicht final
{
   // ...

   public final void notOverridable()   // Methode ist final
   {
      //...
   }

}

Nun kann man zwar eine Unterklasse von Person bilden, aber obige Methode nicht überschreiben.

public class PersTel extends Person     // OK
{
   // ...

   public void notOverridable()   // compile time error
   {
      //...
   }

}

Der Versuch, die Klasse PersTel zu compilieren, endet mit der Fehlermeldung notOverridable() in PersTel cannot override notOverridable() in Person; overridden method is final.

Das explizite Untersagen des Überschreibens hat jedoch zur Folge, daß der Mechanismus der späten Bindung ausgehebelt wird, da man ja in der Unterklasse die Methode nicht mehr überschreiben kann.


Object als Rootklasse

Ähnlich wie es bei Verzeichnissen ein oberstes Verzeichnis gibt, so gibt es in Java auch eine Rootklasse, von der sich alle Klassen ableiten (müssen). Auch unsere Klasse Person ist eine Ableitung der Klasse Object. Der Beweis dafür ist recht einfach zu erbringen:

public class DochNichtRoot
{
   public static void main(String args[])
   {
      Person p = new Person();
      System.out.println( p.toString() );
   }

}

Das obige kleine Programm läßt sich fehlerfrei compilieren, obwohl es in unserer Klasse keine Methode mit dem Namen toString() gibt. Wir erben nämlich diese Methode von der Klasse Object. Schreibt man keine explizite extends Klausel, so ergänzt der Compiler automatisch "extends Object". Also ist hier die Klasse DochNichtRoot keine Wurzelklasse, sondern eine Unterklasse von Object.

Das Konzept, alle Klassen von einer generellen Rootklasse abzuleiten, hat große Vorteile, so sind etwa für jede Javaklasse die folgenden Zuweisungen möglich, da Object ja immer eine Basisklasse ist.

Object ob1, ob2 ;
Person p = new Person();
ob1 = p ;
int arr[] = new int[5] ;
ob2 = arr ;

Wie man sieht, sind auch Arrays nur spezielle Objekte.


Zusammenfassung

Das Thema Vererbung ist hiermit keineswegs erschöpfend behandelt. Eine ganze Reihe von Feinheiten fehlen noch, so gibt es etwa noch neben public und private die access modifier protected und (default). Mit dem letzteren meint man den Schutzzustand, der entsteht, wenn man keinen (der drei anderen) access modifier verwendet. Außerdem ist der genaue Vorgang der Initialisierung eines Objektes, das geerbte Teile enthält, noch nicht geklärt. Des weiteren wurden namenlose Blöcke bzw. anonyme Konstruktoren noch nicht erläutert. Ganz zu schweigen von Interfaces und geschachtelten Klassen (inner class oder in neuerer Terminologie nested class).

In den folgenden Abschnitten werden zunächst einige wichtige Standardklassen aus der API besprochen. Danach beschäftigen wir uns eingehender mit der Vererbung.


Übungen

Erweiterung der Klasse java.util.Random

Ausbau der Klasse PersTel

Anlegen einer Klasse PersAddr

this and super

Valid XHTML 1.0 Strict top Back Next Up Home