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


Java ist auch eine Insel (2. Aufl.) von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Java ist auch eine Insel (2. Auflage)
gp Kapitel 6 Eigene Klassen schreiben
  gp 6.1 Eigene Klassen definieren
  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.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 Stringobjekte
    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.8 Methoden überschreiben  downtop

Wir haben gesehen, dass durch Vererbung eine Unterklasse die sichtbaren Eigenschaften erbt. Die Unterklasse kann nun wiederum Methoden hinzufügen. Dabei ist eine überladene Methode, also eine Funktion, die den gleichen Namen wie die Methode aus einer Oberklasse trägt, aber verschiedene Parameter hat, eine ganz normale, hinzugefügte Methode.

Eine Unterklasse kann eine Methode aber auch überschreiben. Dazu gibt es in der Unterklasse eine Methode mit der exakten Parameterliste, dem Methodennamen und dem Rückgabewert der Oberklasse. Implementiert die Unterklasse die Methode neu, so sagt sie auf diese Weise: »Ich kann’s besser«. Die überschreibende Methode kann demnach den Funktionscode spezialisieren und Eigenschaften nutzen, die in der Oberklasse nicht bekannt sind.

Beispiel   Silizium wird von IC spezialisiert. Die Unterklasse überschreibt die Methode istTeuer(). In der UML ergibt sich folgendes Bild:

Abbildung

Listing 6.22   Sand.java

class Silizium
{
  boolean isTeuer() { return false; }
}
class IC extends Silizium
{
  boolean isTeuer() { return true; }
}

class Sand
{
  public static void main( String args[] )
  {
    IC op = new IC();  // oder auch  Silizium op = new IC();
    System.out.println( op.isTeuer() );   // true
  }
}

Wird ein neues IC-Objekt angelegt, so landet der Aufruf von op.isTeuer() bei IC und gibt den Wert true zurück.

Somit bieten sich generell drei Möglichkeiten für Methoden in der Unterklasse an: Hinzufügen, Überladen oder Überschreiben. Wird die Signatur eines Funktionsblocks beim Überschreiben nicht aufmerksam genug beachtet, wird unbeabsichtigt eine Methode überladen. Dieser Fehler ist schwer zu finden. Insbesondere müssen wir uns damit abfinden, dass abgeleitete Klassen den Rückgabewert einer überschreibenden Methode bis zur Java-Version 1.5 nicht spezialisieren können.


Galileo Computing

6.8.1 super: Aufrufen einer Methode aus der Oberklasse  downtop

Wenn wir eine Methode überschreiben, dann entscheiden wir uns für eine gänzlich neue Implementierung. Was ist aber, wenn die Funktionalität im Großen und Ganzen gut war und nur eine Kleinigkeit fehlte? In diesem Fall kann mit der Referenz super auf eine Eigenschaft der Oberklasse verwiesen werden. super ist vergleichbar mit this und kann auch genauso eingesetzt werden:

class PrahlerOber
{
  int i = 1;

  void m()
  {
    System.out.println( "Ich bin toll" );
  }
}

class PrahlerUnter extends PrahlerOber
{
  int i = 2;

  void m()
  {
    super.m();
    System.out.println( "Und ich bin noch toller" );

    System.out.println( super.i );
    System.out.println( i );
  }
}

Die Methode m() aus PrahlerUnter bezieht sich mittels super.m() auf die Methode der Oberklasse. Im zweiten Fall können wir auf die verdeckte Objektvariable mit super.i zugreifen.

Wir sehen hier, dass super ein allgemeines Konzept ist, dass eine Referenz auf die Oberklasse verwaltet. Wir rufen zwar in m() die überschriebene Methode auf, aber wir benötigen dafür ihren Namen. Änderungsfreundlicher wäre eine Variante wie super – die es aber nicht gibt –, denn dann müsste bei einer Änderung des Programmtexts nicht an den Methodennamen angepasst werden. Ein super() für die Oberklasse existiert nur für Konstruktoren.

Eine Aneinanderreihung von super-Schlüsselwörtern bei einer tieferen Vererbungshierarchie ist nicht möglich. Hinter einem super muss eine Objekteigenschaft stehen. Anweisungen wie super.super.i sind somit immer ungültig. Für Variablen gibt es jedoch eine Möglichkeit, die sich durch einen Cast in die Oberklasse ergibt. Wir erfinden eine neue Unterklasse Oberangeber, die wiederum von PrahlerUnter erbt:

class Oberangeber extends PrahlerUnter
{
  void m()
  {
    System.out.println( ((PrahlerOber)this).i );
  }
}

Die this-Referenz entspricht einem Objekt vom Typ Oberangeber. Wenn wir dies aber in den Typ PrahlerOber konvertieren, bekommen wir genau das i aus der Basisklasse unserer Hierarchie. Wir erkennen hier eine sehr wichtige Eigenschaft von Java, nämlich, dass Variablen nicht dynamisch gebunden werden. Anders wäre es, wenn wir die Zeile zu

System.out.println( ((PrahlerOber)this).m() );

umgebaut hätten. Meine Leser sollten dies einmal testen, warum hier nicht m() aus PrahlerOber aufgerufen wird, sondern etwas anderes. Eine genauere Erklärung des dynamischen Bindens folgt gleich.

this == super

In unseren Köpfen existiert vielleicht die Idee, dass die Referenz super ein Verweis auf ein Objekt der Oberklasse ist. Dies ist aber so nicht ganz korrekt, obwohl diese Ausdrucksweise sich so eingebürgert hat. Es handelt sich für die Unterklasse um ein Objekt, welches alle Eigenschaften der Oberklasse übernimmt. Dass die Realisierung der virtuellen Maschine anders aussieht, ist kein Problem für uns; wir stellen uns vor, dass es genau ein Objekt gibt, das alles sammelt und verwaltet. this ist dann immer noch eine Referenz auf das aktuelle Objekt und super bezeichnet das gleiche Objekt, nur als Typ der Oberklasse. Das soll das nachfolgende Beispiel noch einmal zeigen:

Listing 6.23   ThisIstWirklichSuper.java

public class ThisIstWirklichSuper
{
  void out()
  {
    System.out.println( super.toString());
    System.out.println( this.toString());
    System.out.println( super.equals( this ) );
  }

  public static void main( String args[] )
  {
    ThisIstWirklichSuper o = new ThisIstWirklichSuper();
    o.out();
  }
}

Die Referenz super kann nur im Zusammenhang mit einer Eigenschaft verwendet werden. Einfach nur super auszugeben, führt zu einem Übersetzungsfehler. Es lässt sich jedoch super.toString() einsetzen, um eine einfache Stringrepräsentation zu bekommen. Diese fällt natürlich anders aus, wenn die aktuelle Klasse toString() überschreibt. Ähnliches gilt für equals() im obigen Beispiel.

System.out.println( super );           // Das geht nicht.

Wenn wir das obere Programm ausführen, erhalten wir zum Beispiel folgende Ausgabe:

ThisIstWirklichSuper@8f14553c
ThisIstWirklichSuper@8f14553c
true

Wir erinnern uns: Die toString()-Methode von Object ist so implementiert, dass wir den Hash-Wert und den Namen der Klasse sehen. Beide zeigen auf das gleiche Objekt.


Galileo Computing

6.8.2 Nicht überschreibbare Funktionen  downtop

In der Vererbungshierarchie möchte ein Designer in manchen Fällen verhindern, dass Unterklassen eine Methode überschreiben und neu definieren. Da Methodenaufrufe immer dynamisch gebunden werden, könnte ein Aufrufer unbeabsichtigt in der Unterklasse landen. Das kann verhindert werden, indem das Schlüsselwort final vor die Methodendefinition gestellt wird.

Beispiel   Die Oberklasse definiert die Methode sicher() final. Bei dem Versuch, in einer Unterklasse die Funktion zu überschreiben, meldet der Compiler einen Fehler.

class Ober
{
final void sicher() { }
} class Unter extends Ober { void sicher() { } // Compilerfehler! }

Galileo Computing

6.8.3 Fehlende kovariante Rückgabewerte  toptop

Bisher sieht die Java-Sprachdefinition nicht vor, dass es mehrere Methoden in einer Klasse geben kann, die sich nur in ihren Rückgabetypen unterscheiden. Das wären kovariante Rückgabewerte. Ebenfalls ist es nicht möglich, in einer Unterklasse eine Methode zu überschreiben, die einen anderen Rückgabetyp besitzt. Die Methode würde nicht überschrieben und es gibt einen Compilerfehler, da ererbte und neue Methode gegen die Regel verstoßen, dass zwei Methoden sich nie nur im Rückgabetyp unterscheiden dürfen.

Im Design von großen Programmen wäre es jedoch günstig, wenn ein nichtprimitver Rückgabetyp einer überschriebenen Funktion ebenfalls vom Typ dieser Unterklasse ist. Auf diese Weise ließen sich korrekt angepasste, überschriebene Methoden implementieren, und Entwickler könnten sich die expliziten Typanpassungen ersparen.

Betrachten wir zum Beispiel   die clone()-Methode aus Object.

Eine Unterklasse muss den Rückgabetyp Object deklarieren, daher ist Folgendes leider falsch:

class X        // automatisch extends Object
{
  public X clone()
   {
    return (X)super.clone();
  }
}

Der Aufrufer der Methode clone() von X muss selbstständig eine Typumwandlung auf X vornehmen.

Das Komische in diesem Zusammenhang ist, dass es schon veränderte Zugriffsrechte gibt. Eine Unterklasse kann die Sichtbarkeit erweitern. Auch bei Ausnahmen kann eine Unterklasse speziellere Ausnahmen beziehungsweise ganz andere als die Methode der Oberklasse erzeugen.

Die aktuelle Sprachdefinition von C++ unterstützt kovariante Rückgabewerte. Im Zuge der kommenden Version 1.5 werden auch kovariante Rückgabewerte in die Sprache Einzug halten. Auch einige Java-Erweiterungen, unter ihnen der freie GJ-Compiler, erlauben kovariante Rückgabewerte ohne Änderung der JVM durch Einfügen von Typanpassungen.





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]

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