Galileo Computing < openbook >
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.
Galileo Computing - Professionelle Buecher. Auch fuer Einsteiger.


Java ist auch eine Insel von Christian Ullenboom
Buch: Java ist auch eine Insel (Galileo Computing)
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 Pakete
gp 6.3.1 Hierarchische Strukturen
gp 6.3.2 Paketnamen
gp 6.3.3 Eine Verzeichnisstruktur für eigene Projekte
gp 6.4 Privatsphäre und Sichtbarkeit
gp 6.4.1 Wieso nicht freie Methoden und Variablen für alle?
gp 6.4.2 Privat ist nicht ganz privat. Es kommt darauf an, wer's sieht
gp 6.4.3 Zugriffsmethoden für Attribute definieren
gp 6.4.4 Zusammenfassung zur Sichtbarkeit
gp 6.4.5 Sichtbarkeit in der UML
gp 6.5 Statische Methoden und Variablen
gp 6.5.1 Warum statische Eigenschaften sinnvoll sind
gp 6.5.2 Statische Eigenschaften mit static
gp 6.5.3 Statische Eigenschaften als Objekteigenschaften nutzen
gp 6.5.4 Statische Eigenschaften und Objekteigenschaften
gp 6.5.5 Statische Variablen zum Datenaustausch
gp 6.5.6 Warum die Groß- und Kleinschreibung wichtig ist
gp 6.5.7 Konstanten mit dem Schlüsselwort final bei Variablen
gp 6.5.8 Problem mit finalen Klassenvariablen
gp 6.5.9 Typsicherere Konstanten
gp 6.5.10 Statische Blöcke
gp 6.6 Objekte anlegen und zerstören
gp 6.6.1 Konstruktoren schreiben
gp 6.6.2 Einen anderen Konstruktor der gleichen Klasse aufrufen
gp 6.6.3 Initialisierung der Objekt- und Klassenvariablen
gp 6.6.4 Finale Werte im Konstruktor setzen
gp 6.6.5 Exemplarinitialisierer (Instanzinitialisierer)
gp 6.6.6 Zerstörung eines Objekts durch den Müllaufsammler
gp 6.6.7 Implizit erzeugte String-Objekte
gp 6.6.8 Zusammenfassung: Konstruktoren und Methoden
gp 6.7 Veraltete (deprecated) Methoden/Konstruktoren
gp 6.8 Vererbung
gp 6.8.1 Vererbung in Java
gp 6.8.2 Einfach- und Mehrfachvererbung
gp 6.8.3 Gebäude modelliert
gp 6.8.4 Konstruktoren in der Vererbung
gp 6.8.5 Sichtbarkeit
gp 6.8.6 Das Substitutionsprinzip
gp 6.8.7 Automatische und explizite Typanpassung
gp 6.8.8 Finale Klassen
gp 6.8.9 Unterklassen prüfen mit dem Operator instanceof
gp 6.8.10 Methoden überschreiben
gp 6.8.11 super: Aufrufen einer Methode aus der Oberklasse
gp 6.8.12 Nicht überschreibbare Funktionen
gp 6.8.13 Fehlende kovariante Rückgabewerte
gp 6.9 Die oberste aller Klassen: Object
gp 6.9.1 Klassenobjekte
gp 6.9.2 Objektidentifikation mit toString()
gp 6.9.3 Objektgleichheit mit equals() und Identität
gp 6.9.4 Klonen eines Objekts mit clone()
gp 6.9.5 Hashcodes
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 Polymorphie bei Konstruktoraufrufen
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 Ein Polymorphie-Beispiel mit Schnittstellen
gp 6.12.2 Die Mehrfachvererbung bei Schnittstellen
gp 6.12.3 Erweitern von Interfaces - Subinterfaces
gp 6.12.4 Vererbte Konstanten bei Schnittstellen
gp 6.12.5 Vordefinierte Methoden einer Schnittstelle
gp 6.12.6 CharSequence als Beispiel einer Schnittstelle
gp 6.13 Innere Klassen
gp 6.13.1 Statische innere 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


Galileo Computing

6.10 Die Oberklasse gibt Funktionalität vordowntop

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 Gebäude- und Diskotheken-Klasse ein wenig. Zusätzlich wollen wir als Unterklasse von Gebaeude eine neue Klasse einführen, die parallel zur Disko steht: eine Kirche. Zunächst führen wir in der Klasse Gebaeude die Methode getId() ein. Die Idee ist, dass sich später die Objekte über eine ID identifizieren lassen sollen.

Listing 6.38 va/Gebaeude.java

package va;
public class Gebaeude
{
  public final static int UNDEFINIERT  = 0;
  public final static int DISKO        = 1;
  public final static int KIRCHE       = 2;
  /**
   * Liefert den Typ des konkreten Gebäudes.
   * 
   * @return Gebäudetyp.
   */ 
  public int getId()
  {
   return UNDEFINIERT; 
  }
}

Vielleicht ist an dieser Stelle noch nicht ganz klar, wieso wir die Methode getId() hier einführen, gäbe es doch auch die Möglichkeit, die Funktion getId() in Disko und in Kirche einzusetzen. Das ist zwar richtig, aber die Klassen hätten dann nur rein »zufällig« eine Methode mit dem Namen getId(). Es gäbe keine Gemeinsamkeit, und daher nehmen wie die Modellierung über die Oberklasse vor. Wie schaffen somit eine Gemeinsamkeit, da Disko und Kirche nun automatisch eine Methode getId() haben.

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

Die Implementierung von Disko und Kirche überschreiben die geerbte Implementierung aus der Oberklasse Gebaeude. Im Quellcode sieht das dann so aus:

Listing 6.39 va/Kirche.java

package va;
public class Kirche extends Gebaeude
{
  private int anzahlBeichtstühle;
  public int getId()
  {
    return KIRCHE;
  }
}

Listing 6.40 va/Disko.java

package va;
public class Disko extends Gebaeude
{
  public int getId()
  {
    return DISKO;
  }
}

Es fehlen noch ein paar kleine Testzeilen:

Disko  d = new Disko();
Kirche k = new Kirche();
System.out.println( "Id Kirche:" + d.getId() +   // Id Kirche:1
                    " Id Disko: " + k.getId() ); // Id Disko: 2

Die angegebenen Zeilen sind leicht zu verstehen. Die Laufzeitumgebung sucht nun von unten nach oben in der Vererbungshierarchie nach der Methode getId() und findet sie sofort in Kirche beziehungsweise Disko. Würden wir eine neue Unterklasse von Gebaeude schaffen und getId() nicht überschreiben, so würde die Laufzeitumgebung getId() in Gebaeude finden und 0 liefern.


Galileo Computing

6.10.1 Dynamisches Binden als Beispiel für Polymorphiedowntop

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

Gebaeude e = new Disko();
Gebaeude l = new Kirche();

Dies geht auf jeden Fall in Ordnung, da Disko und Kirche ##Unterklassen von Gebaeude sind. e und l verzichten auf die möglicherweise hinzugefügten Attribute der Unterklassen.

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

System.out.println( e.getId() );
System.out.println( l.getId() );

Jetzt ist die spannendste Frage in der gesamten Objektorientierung Folgende: Was passiert bei dem Methodenaufruf? Es gibt zwei Möglichkeiten.

1. Da die Variablen e und l den Typ Gebaeude besitzen, wird die Methode auf Gebaeude aufgerufen, und in beiden Fällen ist die Rückgabe von getId() 0.
2. Eine andere Lösung ist, dass sich die Laufzeitumgebung erinnert, dass der Typ der Variablen zwar Gebaeude ist, aber dass dahinter ein Disko- beziehungsweise Kirchen-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 getId() 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.41 va/Stadt.java

package va;
public class Stadt
{
  public static void main( String args[] )
  {
    Gebaeude gebaeude[] = { new Disko(), new Kirche(), new Kirche() };
    kennungenAusgeben( gebaeude );  // 1 2 2
  }

Wir erzeugen drei konkrete Gebäude, die wir in ein Feld legen. Anschließend übergeben wir der Methode kennungenAusgeben() das Feld mit den Objekten:

  static void kennungenAusgeben( Gebaeude g[] )
  {
    for ( int i = 0; i < g.length; i++ )
      System.out.println( g[i].getId() ); // Hier ist die Polymorphie
  }
}

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 Methodendowntop

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.42 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 Polymorphie bei Konstruktoraufrufentoptop

Dass ein Konstruktor der Unterklasse zuerst den Konstruktor der Oberklasse aufruft, kann die Initialisierung der Variablen in der Unterklasse anfällig stören. Schauen wir uns erst Folgendes an:

class Rausschmeisser extends Mukkityp
{
  String was = "Ich bin ein Rausschmeisser";
}

Wo wird nun die Variable was initialisiert? Wir wissen, dass die Initialisierungen immer im Konstruktor vorgenommen werden, aber da gibt es ja noch gleichzeitig ein super() im Konstruktor. 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 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 Mukkityp) nicht auf die Variablen der Unterklasse 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.

Listing 6.43 Rausschmeisser.java

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

Die Ausgabe ist nun Folgende:

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

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

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

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. Vermeiden lässt sich das, in dem der Konstruktor nur private Methoden aufruft, denn diese werden nicht dynamisch gebunden. Wenn der Konstruktor eine private Methode in seiner Klasse aufruft, dann bleibt es auch dabei.





Copyright (c) Galileo Press GmbH 2004
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