Galileo Computing < openbook >
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel (3. Aufl.) von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Buch: Java ist auch eine Insel
gp Kapitel 6 Eigene Klassen schreiben
  gp 6.1 Eigene Klassen definieren
    gp 6.1.1 Methodenaufrufe und Nebeneffekte
    gp 6.1.2 Argumentübergabe mit Referenzen
    gp 6.1.3 Die this-Referenz
    gp 6.1.4 Überdeckte Objektvariablen nutzen
  gp 6.2 Assoziationen zwischen Objekten
  gp 6.3 Privatsphäre und Sichtbarkeit
    gp 6.3.1 Wieso nicht freie Methoden und Variablen für alle?
    gp 6.3.2 Privat ist nicht ganz privat. Es kommt darauf an, wer's sieht
    gp 6.3.3 Zugriffsmethoden für Attribute definieren
    gp 6.3.4 Zusammenfassung zur Sichtbarkeit
    gp 6.3.5 Sichtbarkeit in der UML
  gp 6.4 Statische Methoden und Variablen
    gp 6.4.1 Warum statische Eigenschaften sinnvoll sind
    gp 6.4.2 Statische Eigenschaften mit static
    gp 6.4.3 Statische Eigenschaften als Objekteigenschaften nutzen
    gp 6.4.4 Statische Eigenschaften und Objekteigenschaften
    gp 6.4.5 Statische Variablen zum Datenaustausch
    gp 6.4.6 Warum die Groß- und Kleinschreibung wichtig ist
    gp 6.4.7 Konstanten mit dem Schlüsselwort final bei Variablen
    gp 6.4.8 Typsicherere Konstanten
    gp 6.4.9 Statische Blöcke
  gp 6.5 Objekte anlegen und zerstören
    gp 6.5.1 Konstruktoren schreiben
    gp 6.5.2 Einen anderen Konstruktor der gleichen Klasse aufrufen
    gp 6.5.3 Initialisierung der Objekt- und Klassenvariablen
    gp 6.5.4 Finale Werte im Konstruktor setzen
    gp 6.5.5 Exemplarinitialisierer (Instanzinitialisierer)
    gp 6.5.6 Zerstörung eines Objekts durch den Müllaufsammler
    gp 6.5.7 Implizit erzeugte String-Objekte
    gp 6.5.8 Zusammenfassung: Konstruktoren und Methoden
  gp 6.6 Veraltete (deprecated) Methoden/Konstruktoren
  gp 6.7 Vererbung
    gp 6.7.1 Vererbung in Java
    gp 6.7.2 Einfach- und Mehrfachvererbung
    gp 6.7.3 Kleidungsstücke modelliert
    gp 6.7.4 Sichtbarkeit
    gp 6.7.5 Das Substitutionsprinzip
    gp 6.7.6 Automatische und explizite Typanpassung
    gp 6.7.7 Finale Klassen
    gp 6.7.8 Unterklassen prüfen mit dem Operator instanceof
  gp 6.8 Methoden überschreiben
    gp 6.8.1 super: Aufrufen einer Methode aus der Oberklasse
    gp 6.8.2 Nicht überschreibbare Funktionen
    gp 6.8.3 Fehlende kovariante Rückgabewerte
  gp 6.9 Die oberste aller Klassen: Object
    gp 6.9.1 Klassenobjekte
    gp 6.9.2 Hashcodes
    gp 6.9.3 Objektidentifikation mit toString()
    gp 6.9.4 Objektgleichheit mit equals() und Identität
    gp 6.9.5 Klonen eines Objekts mit clone()
    gp 6.9.6 Aufräumen mit finalize()
    gp 6.9.7 Synchronisation
  gp 6.10 Die Oberklasse gibt Funktionalität vor
    gp 6.10.1 Dynamisches Binden als Beispiel für Polymorphie
    gp 6.10.2 Keine Polymorphie bei privaten, statischen und finalen Methoden
    gp 6.10.3 Konstruktoren in der Vererbung
  gp 6.11 Abstrakte Klassen
    gp 6.11.1 Abstrakte Klassen
    gp 6.11.2 Abstrakte Methoden
    gp 6.11.3 Über abstract final
  gp 6.12 Schnittstellen
    gp 6.12.1 Die Mehrfachvererbung bei Schnittstellen
    gp 6.12.2 Erweitern von Interfaces - Subinterfaces
    gp 6.12.3 Vererbte Konstanten bei Schnittstellen
    gp 6.12.4 Vordefinierte Methoden einer Schnittstelle
    gp 6.12.5 CharSequence als Beispiel einer Schnittstelle
  gp 6.13 Innere Klassen
    gp 6.13.1 Geschachtelte Top-Level-Klassen und Schnittstellen
    gp 6.13.2 Mitglieds- oder Elementklassen
    gp 6.13.3 Lokale Klassen
    gp 6.13.4 Anonyme innere Klassen
    gp 6.13.5 Eine Sich-Selbst-Implementierung
    gp 6.13.6 this und Vererbung
    gp 6.13.7 Implementierung einer verketteten Liste
    gp 6.13.8 Funktionszeiger
  gp 6.14 Gegenseitige Abhängigkeiten von Klassen
  gp 6.15 Pakete


Galileo Computing

6.10 Die Oberklasse gibt Funktionalität vor downtop

Der Vorteil beim Überschreiben ist also, dass die Oberklasse eine einfache Implementierung vorgibt, die die Unterklasse dann spezialisieren kann. Doch nicht nur die Spezialisierung ist aus der Sicht des Designs interessant, sondern auch die Bedeutung der Vererbung. Bei der Vererbung haben wir eine Form der Ist-eine-Beziehung oder Ist-eine-Art-von-Beziehung. Wenn nun eine Oberklasse eine Methode anbietet, die die Unterklassen überschreibt, so wissen wir, dass alle Unterklassen diese Methode haben müssen. Wir werden gleich sehen, dass dies zu einem der wichtigsten Konstrukte in objektorientierten Programmiersprachen führt.

Modifizieren wir dazu unsere Socken- und Hosen-Klassen ein wenig. Damit wir die neuen Klassen unterscheiden können, lassen wir sie mit einem P beginnen. Aus Kleidung wird PKleidung, aus Hose wird PHose und aus Socke wird PSocke. Zunächst führen wir in der Klasse PKleidung die Methode kennung() ein:

Listing 6.24 Polymorphie.java, Teil 1

class PKleidung
{
  public String  farbe;
  String kennung()
  {
    return "Keine Ahnung was ich mal werde";
  }
}

Vielleicht ist an dieser Stelle noch nicht ganz klar, wieso wir die Methode kennung() einführen, hatten doch die Klassen Hose und Socke schon eine Methode kennung(). Das ist zwar richtig, war aber zu unflexibel, da Hose und Socke keine Verbindung hatten. Beide Klassen enthielten nur »zufällig« jeweils eine Methode mit dem Namen kennung(), die jedoch nichts miteinander zu tun hatten. Mit der vorgenommenen Modellierung schaffen wir aber eine Gemeinsamkeit, da Hose und Socke nun automatisch eine Methode kennung() haben.

In der UML sieht das mit den Klassen PSocke und PHose und der Vererbung von PKleidung wie folgt aus:

Abbildung
Hier klicken, um das Bild zu Vergrößern

Die Implementierung von Socke und Hose bleibt, nur fügen die Klassen dann diese Methode nicht neu hinzu, sondern überschreiben die geerbte Implementierung aus der Oberklasse Kleidung. Im Quellcode sieht das dann so aus:

Listing 6.25 Polymorphie.java, Teil 2

class PSocke extends PKleidung
{
  String kennung()
  {
    return "Ich bin eine Socke";
  }
}
class PHose extends PKleidung
{
  String kennung()
  {
    return "Ich bin eine Hose";
  }
}

Es fehlen noch ein paar kleine Testzeilen:

PSocke s = new PSocke();
PHose h = new PHose();
System.out.println( s.kennung() );
System.out.println( h.kennung() );

Die angegebenen Zeilen sind leicht zu verstehen. Die Laufzeitumgebung sucht nun von unten nach oben in der Vererbungshierarchie nach der Methode kennung() und findet sie in PHose beziehungsweise PSocke. Würden wir eine neue Klasse schaffen und kennung() nicht überschreiben, so würde die Laufzeitumgebung kennung() in PKleidung finden.


Galileo Computing

6.10.1 Dynamisches Binden als Beispiel für Polymorphie downtop

Verbinden wir unser Wissen über vererbte Methoden und die Verträglichkeit von Referenztypen zu folgendem Beispiel:

PKleidung k1 = new PSocke();
PKleidung k2 = new PHose();

Dies geht auf jeden Fall in Ordnung. PSocke und PHose sind Unterklassen von PKleidung, und k1 und k2 verzichten auf die möglicherweise hinzugefügten Attribute der Unterklassen.

Aber wirklich interessant ist die Feststellung, dass PKleidung ja die Methode kennung() besitzt, die wir aufrufen können:

System.out.println( k1.kennung() );
System.out.println( k2.kennung() );

Jetzt ist die spannendste Frage in der gesamten Objektorientierung folgende: Was passiert bei dem Methodenaufruf? Es gibt zwei Möglichkeiten. Da der Typ der Variablen k1 und k2 Kleidung ist, wird die Methode auf PKleidung aufgerufen, und in beiden Fällen ist die Ausgabe »Keine Ahnung, was ich mal werde«. Eine andere Lösung ist, dass sich die Laufzeitumgebung erinnert, dass der Typ der Variablen zwar PKleidung ist, aber dass dahinter ein Socken- beziehungsweise Hosen-Objekt steht. Diese zweite Lösung ist richtig. Da hier aus dem statisch im Programmtext vereinbarten Typ der Variablen nicht abzulesen ist, welche Implementierung der Methode kennung() aufgerufen wird, sprechen wir von dynamischer Bindung. Erst zur Laufzeit wird dynamisch die entsprechende Objektmethode, passend zum tatsächlichen Typ des aufrufenden Objekts, ausgewählt. Die dynamische Bindung ist eine Anwendung von Polymorphie. Obwohl Polymorphie mehr ist als dynamisches Binden, wollen wir beide Begriffe synonym verwenden.

Werfen wir einen Blick auf ein Programm, welches dynamisches Binden noch deutlicher macht.

Listing 6.26 Polymorphie.java, Teil 3

public class Polymorphie
{
  public static void main( String args[] )
  {
    PKleidung k[] = new PKleidung[4];
    k[0] = new PSocke();
    k[1] = new PHose();
    k[2] = new PHose();
    k[3] = new PSocke();
    kennungenAusgeben( k );
  }

Wir erzeugen vier konkrete Kleidungsstücke, die wir in ein Feld einsortieren. Anschließend übergeben wir der Methode kennungenAusgeben() das Feld mit den Kleidungs-Objekten:

  static void kennungenAusgeben( PKleidung k[] )
  {
    for ( int i = 0; i < k.length; i++ )
      // Hier die Polymorphie
      System.out.println( k[i].kennung() );
  }
}

Spätestens hier ist der Compiler mit dem Wissen über die Objekte im Array am Ende, da er nun wirklich bei der Methode kennungAusgeben() nicht weiß, welche Objekte ihn als Array-Elemente erwarten.


Galileo Computing

6.10.2 Keine Polymorphie bei privaten, statischen und finalen Methoden downtop

Obwohl Methodenaufrufe in Java in der Regel polymorph gebunden sind, gibt es bei privaten, statischen und finalen Methoden eine Ausnahme; sie können nicht überschrieben werden und sind daher auch nicht polymorph gebunden. Wir wollen uns das an einer privaten Funktion ansehen:

Listing 6.27 NoPolyWithPrivate.java

class NoPolyWithPrivate
{
  public static void main( String args[] )
  {
    Unter unsicht = new Unter();
    System.out.println( unsicht.bar() );   // 2
  }
}
class Ober
{
  private int furcht()
  {
    return 2;
  }
  int bar()
  {
    return furcht();
  }
}
class Unter extends Ober
{
  int furcht()
  {
    return 1;
  }
}

Der Compiler meldet beim Überschreiben der Funktion furcht() keinen Fehler. Für den Compiler ist es in Ordnung, wenn es eine Methode in der Unterklasse gibt, die den gleichen Namen wie eine private Methode in der Oberklasse trägt. Das ist auch gut so, denn private Implementierungen sind ja sowieso geheim und versteckt. Die Unterklasse soll von den privaten Methoden in der Oberklasse gar nichts wissen.

Die Laufzeitumgebung macht etwas Erstaunliches für unsicht.bar(). Die Funktion bar() wird aus der Oberklasse geerbt. Normalerweise wissen wir, dass Funktionen, die in bar() aufgerufen werden, dynamisch gebunden werden, das heißt, dass wir eigentlich bei furcht() in Unter laden müssten, da wir ein Objekt vom Typ Unter haben. Bei privaten Methoden ist das aber anders. Wenn eine aufgerufene Methode den Modifizierer private trägt, dann wird nicht dynamisch gebunden. Das ist ein wichtiger Beitrag zur Sicherheit. Falls nämlich Unterklassen interne private Methoden überschreiben könnten, wäre dies eine Verletzung der inneren Arbeitsweise der Oberklasse. In einem Satz: Private Methoden sind nicht in den Unterklassen sichtbar und werden daher nicht verdeckt oder überschrieben. Andernfalls könnten private Implementierungen im Nachhinein geändert werden und Oberklassen wären nicht mehr sicher davor, dass tatsächlich ihre eigenen Funktionen benutzt werden.

Casten wir in der Methode bar() in der Klasse Ober über die this-Referenz auf ein Objekt vom Typ Unter, dann wird ausdrücklich diese Methode aufgerufen, was jedoch kein typisches objektorientiertes Konstrukt darstellt. bar() in der Klasse Ober ist damit nicht mehr für Ober-Objekte benutzbar.

int bar()
{
  return ((Unter)(this)).furcht();
}

Galileo Computing

6.10.3 Konstruktoren in der Vererbungtoptop

Obwohl Konstruktoren Ähnlichkeiten mit Methoden haben, etwa in der Eigenschaft, dass sie überladen werden oder Ausnahmen erzeugen können, werden sie im Gegensatz zu Methoden nicht vererbt. Das heißt, eine Unterklasse muss ganz neue Konstruktoren angeben, denn mit den Konstruktoren der Oberklasse kann ein Objekt der Unterklasse nicht erzeugt werden. Ob das nun reine Objektorientierung ist, kann diskutiert werden, in der Sprache Python etwa werden auch Konstruktoren vererbt. In Java gehören Konstruktoren eigentlich zum statischen Teil einer Klasse. Die Klasse selbst weiß, wie neue Objekte konstruiert werden. Sehen wir Konstruktoren eher als Initialisierungsmethoden an, läge es natürlich näher, sie wie Objektmethoden zu behandeln. Dagegen spricht jedoch, dass eine Unterklasse mehr Eigenschaften hat und der Konstruktor der Oberklasse dann nur einen Teil initialisieren würde.

In Java sammelt eine Unterklasse zwar automatisch alle sichtbaren Eigenschaften der Oberklasse, aber die Objekte in der Hierarchie existieren einzeln. Das heißt, wenn eine Unterklasse erzeugt wird, dann ruft der Konstruktor der Unterklasse automatisch den Standard-Konstruktor der Oberklasse auf, um das obere Objekt zu initialisieren. Es ist dabei egal, ob der Konstruktor in der Unterklasse parametrisiert ist oder nicht; jeder Konstruktor der Unterklasse muss einen der Oberklasse aufrufen.

Der Aufruf wird meistens automatisch vom Compiler eingefügt, und eine Modifikation des Bytecodes würde die Aufrufreihenfolge empfindlich stören, denn im Bytecode gibt es diese Verpflichtung nicht. Die Sprache sieht für den ausdrücklichen Aufruf des Konstruktors der Oberklasse die Anweisung super() vor. Die Referenz super zeigt nur auf ein Objekt der Oberklasse. Mit Klammern ist immer ein Aufruf verbunden. Erinnern wir uns an dieser Stelle noch einmal an this und this().

Ein Beispiel mit Konstruktorweiterleitung

Sehen wir uns noch einmal die Konstruktorverkettung an:

class Kleidung
{
}
class Sakko extends Kleidung
{
}

Da wir keine expliziten Konstruktoren haben, fügt der Compiler zwei Standard-Konstruktoren ein. Sie rufen zudem den Standard-Konstruktor der Oberklasse auf. Daher ergibt sich folgendes Bild in den Klassen für die Laufzeitumgebung im Bytecode:

Kleidung()
{
  super();         // für Object()
}
Sakko()
{
  super();         // für Kleidung()
}

Wir sehen, dass wir nicht ausdrücklich super() schreiben müssen, da es der Compiler macht.

Ein unnötiges super() in der ersten Zeile?

In vielen Java-Programmen (auch in der Java-Klassenbibliothek, besonders bei den Ausnahmen) steht aber trotzdem in der ersten Zeile des Konstruktors super(). So zum Beispiel in der Klasse Vector:

public Vector(int initialCapacity, int capacityIncrement)
{
  super();
  ...
}

oder in der Klasse IOException:

public IOException()
{
  super();
}

Wie wir gesehen haben, ist dies nicht notwendig, kann aber die Lesbarkeit fördern. Wir sind uns dann sofort bewusst, dass die »Methode« ein Konstruktor ist und dass der Standard-Konstruktor aufgerufen wird.

super() mit Parameter aufrufen

Mitunter ist es nötig, nicht nur den Standard-Konstruktor anzusteuern, sondern einen anderen Konstruktor der Oberklasse, den wir uns aussuchen wollen. Dazu kann super() mit Parametern gefüllt werden. Gründe dafür könnten sein:

1. Ein parametrisierter Konstruktor der Unterklasse leitet oft die Parameter an die Oberklasse weiter.
2. Wenn wir keinen Standard-Konstruktor in der Oberklasse anbieten, dann müssen wir in der Unterklasse mittels super(Parameter,...) einen speziellen, parametrisierten Konstruktor aufrufen.

Dazu noch einmal einen Blick auf die Implementierung von IOException, wo wir direkt die Zeichenkette weiter nach oben geben:

public class IOException extends Exception
{
  public IOException()
  {
    super();
  }
  public IOException( String s )
  {
    super(s);
  }
}

Variableninitialisierung

Dass ein Konstruktor der Unterklasse zuerst den Konstruktor der Oberklasse aufruft, kann die Initialisierung der Variablen in der Unterklasse anfällig stören.

Nehmen wir an, wir hätten in Sakko eine Objektvariable alter=3 eingesetzt. Wo wird diese initialisiert? Vor oder hinter dem super()? Da die Sprachdefinition Anweisungen vor super() verbietet, muss also die Zuweisung hinter dem Aufruf der Oberklasse folgen. Das Problem ist nun, dass ein Konstruktor der Oberklasse ja früher aufgerufen wurde als Variablen in der Unterklasse initialisiert wurden. Wenn es die Oberklasse nun schafft, auf die Variablen der Unterklasse zuzugreifen, so wird der erst später gesetzte Wert fehlen. Der Zugriff gelingt tatsächlich, doch nur durch einen Trick, da eine Oberklasse (etwa Kleidung) nicht auf die Variablen der Unterklasse (hier alter) zugreifen kann. Aber wir können in der Oberklasse eine Methode der Unterklasse aufrufen, nämlich genau die, die die Unterklasse aus der Oberklasse überschreibt. Da Methodenaufrufe dynamisch gebunden werden, kann eine Methode den Wert auslesen.

Beispiel   Noch nicht korrekt initialisierte Objekte

Listing 6.28 Sakko.java

class Kleidung
{
  Kleidung()
  {
    wasBinIch();
  }
 
  void wasBinIch()
  {
    System.out.println("Ich weiß es noch nicht :-(");
  }
}
 
public class Sakko extends Kleidung
{
  String was = "Ich bin ein Sakko";
 
  void wasBinIch()
  {
    System.out.println( was );
  }
  public static void main( String args[] )
  {
    Kleidung k = new Kleidung();
 
    Sakko blau = new Sakko();
    blau.wasBinIch();
  }
}

Die Ausgabe ist nun folgende:

Ich weiß es noch nicht :-(
null
Ich bin ein Sakko

Das Besondere bei diesem Programm ist nun, dass Methoden von überschriebenen Klassen dynamisch in der Oberklasse gebunden werden. Diese Bindung gibt es auch dann schon, wenn das Objekt noch nicht vollständig initialisiert wurde. Daher ruft der Konstruktor der Oberklasse Kleidung nicht wasBinIch() von Kleidung auf, sondern wasBinIch() von Sakko. Wenn in diesem Beispiel ein Sakko-Objekt erzeugt wird, dann ruft Sakko den Konstruktor von Kleidung auf. Dieser ruft wiederum die Methode wasBinIch() in Sakko auf, und er findet dort keinen String, da dieser erst nach super() gesetzt wird. Schreiben wir den Konstruktor von Sakko einmal ausdrücklich hin:

public class Sakko extends Kleidung
{
  String was;
  Sakko()
  {
    super();
    was = "Ich bin ein Sakko";
  }
}

Die Konsequenz, die sich daraus ergibt, ist folgende: Dynamisch gebundene Methodenaufrufe über die this-Referenz sind in Konstruktoren potenziell gefährlich und sollten deshalb vermieden werden.





Copyright © Galileo Press GmbH 2003
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing] [Buchkatalog] [Neue Bücher] [Vorschau]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de