Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

 << zurück
Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5
Java ist auch eine Insel

Java ist auch eine Insel
5., akt. und erw. Auflage
1454 S., mit CD, 49,90 Euro
Galileo Computing
ISBN 3-89842-747-1
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.2.1 Gegenseitige Abhängigkeiten von Klassen
  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.4 Statische Methoden und statische Attribute
    gp 6.4.1 Warum statische Eigenschaften sinnvoll sind
    gp 6.4.2 Statische Eigenschaften mit static
    gp 6.4.3 Statische Eigenschaften über Referenzen nutzen?
    gp 6.4.4 Warum die Groß- und Kleinschreibung wichtig ist
    gp 6.4.5 Statische Eigenschaften und Objekteigenschaften
    gp 6.4.6 Statische Variablen zum Datenaustausch
    gp 6.4.7 Statische Blöcke als Klasseninitialisierer
  gp 6.5 Konstanten und Aufzählungen
    gp 6.5.1 Konstanten über öffentliche statische final-Variablen
    gp 6.5.2 Problem mit finalen Klassenvariablen
    gp 6.5.3 Typsicherere Konstanten
    gp 6.5.4 Aufzählungen mit enum in Java 5
    gp 6.5.5 enum-Konstanten in switch
  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 und in statischen Blöcken 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 Private Konstruktoren, Utility-Klassen, Singleton und Fabriken
  gp 6.7 Vererbung
    gp 6.7.1 Vererbung in Java
    gp 6.7.2 Einfach- und Mehrfachvererbung
    gp 6.7.3 Gebäude modelliert
    gp 6.7.4 Konstruktoren in der Vererbung
    gp 6.7.5 Sichtbarkeit protected
    gp 6.7.6 Das Substitutionsprinzip
    gp 6.7.7 Automatische und explizite Typanpassung
    gp 6.7.8 Typen testen mit dem binären Operator instanceof
    gp 6.7.9 Array-Typen und Kovarianz
    gp 6.7.10 Methoden überschreiben
    gp 6.7.11 Mit super eine Methode der Oberklasse aufrufen
    gp 6.7.12 Kovariante Rückgabetypen
    gp 6.7.13 Finale Klassen
    gp 6.7.14 Nicht überschreibbare Funktionen
    gp 6.7.15 Zusammenfassung zur Sichtbarkeit
    gp 6.7.16 Sichtbarkeit in der UML
    gp 6.7.17 Zusammenfassung: Konstruktoren und Methoden
  gp 6.8 Object ist die Mutter aller Oberklassen
    gp 6.8.1 Klassenobjekte
    gp 6.8.2 Objektidentifikation mit toString()
    gp 6.8.3 Objektgleichheit mit equals() und Identität
    gp 6.8.4 Klonen eines Objekts mit clone()
    gp 6.8.5 Hashcodes
    gp 6.8.6 Aufräumen mit finalize()
    gp 6.8.7 Synchronisation
  gp 6.9 Die Oberklasse gibt Funktionalität vor
    gp 6.9.1 Dynamisches Binden als Beispiel für Polymorphie
    gp 6.9.2 Keine Polymorphie bei privaten, statischen und finalen Methoden
    gp 6.9.3 Polymorphie bei Konstruktoraufrufen
  gp 6.10 Abstrakte Klassen und abstrakte Methoden
    gp 6.10.1 Abstrakte Klassen
    gp 6.10.2 Abstrakte Methoden
  gp 6.11 Schnittstellen
    gp 6.11.1 Ein Polymorphie-Beispiel mit Schnittstellen
    gp 6.11.2 Die Mehrfachvererbung bei Schnittstellen
    gp 6.11.3 Erweitern von Interfaces – Subinterfaces
    gp 6.11.4 Vererbte Konstanten bei Schnittstellen
    gp 6.11.5 Vordefinierte Methoden einer Schnittstelle
    gp 6.11.6 Abstrakte Klassen und Schnittstellen im Vergleich
    gp 6.11.7 CharSequence als Beispiel einer Schnittstelle
    gp 6.11.8 Die Schnittstelle Iterable
  gp 6.12 Innere Klassen
    gp 6.12.1 Statische innere Klassen und Schnittstellen
    gp 6.12.2 Mitglieds- oder Elementklassen
    gp 6.12.3 Lokale Klassen
    gp 6.12.4 Anonyme innere Klassen
    gp 6.12.5 this und Vererbung
    gp 6.12.6 Implementierung einer verketteten Liste
    gp 6.12.7 Funktionszeiger
  gp 6.13 Generische Datentypen
    gp 6.13.1 Einfache Klassenschablonen
    gp 6.13.2 Einfache Methodenschablonen
    gp 6.13.3 Umsetzen der Generics, Typlöschung und Raw-Types
    gp 6.13.4 Einschränken der Typen
    gp 6.13.5 Generics und Vererbung, Invarianz
    gp 6.13.6 Wildcards
  gp 6.14 Die Spezial-Oberklasse Enum
    gp 6.14.1 Methoden auf Enum-Objekten
    gp 6.14.2 enum mit eigenen Konstruktoren und Methoden
  gp 6.15 Dokumentationskommentare mit javaDoc
    gp 6.15.1 Einen Dokumentationskommentar setzen
    gp 6.15.2 Mit javadoc eine Dokumentation erstellen
    gp 6.15.3 HTML-Tags in Dokumentationskommentaren
    gp 6.15.4 Generierte Dateien
    gp 6.15.5 Weitere Dokumentationskommentare
    gp 6.15.6 javaDoc und Doclets
    gp 6.15.7 Veraltete (deprecated) Klassen, Konstruktoren und Methoden


Galileo Computing

6.9 Die Oberklasse gibt Funktionalität vodowntop

Bei der Vererbung haben wir eine Form der Ist-eine-Art-von-Beziehung, sodass die Unterklassen immer vom Typ der Oberklassen sind. Die Methoden, die die Oberklassen besitzen, existieren somit auch in den Unterklassen. Der Vorteil beim Überschreiben ist, dass die Oberklasse eine einfache Implementierung vorgeben kann, die die Unterklasse spezialisieren können. Doch nicht nur die Spezialisierung ist aus der Sicht des Designs interessant, sondern auch die Bedeutung der Vererbung. Wenn nun eine Oberklasse eine Methode anbietet, die die Unterklassen überschreibt oder nicht, so wissen wir immer, dass alle Unterklassen diese Methode haben müssen. Wir werden gleich sehen, dass dies zu einem der wichtigsten Konstrukte in objektorientierten Programmiersprachen führt.

Um dazu ein Beispiel zu formulieren, führen wir zunächst eine Aufzählung ein, die unterschiedliche Gebäudetypen repräsentiert.

Listing 6.47   va/GebaeudeTyp.java

package va;
public enum GebaeudeTyp
{
  UNDEFINIERT,
  GASTSTAETTE,
  KIRCHE,
}

Die konkreten Gebäude sollen später nach ihrem Gebäudetyp gefragt werden können. Eine Kirche soll den GebaeudeTyp.KIRCHE liefert und eine Kneipe oder Disko den GebaeudeTyp.GASTSTAETTE. Damit auch wirklich jedes Gebäude seinen Typ geben kann, führen wir in der Klasse Gebaeude die Methode getTyp() ein.

Listing 6.48   va/Gebaeude.java

package va;
public class Gebaeude
{
  /**
   * Liefert den Typ des konkreten Gebäudes.
   *
   * @return GebaeudeTyp.
   */
  public GebaeudeTyp getTyp()
  {
    return GebaeudeTyp.UNDEFINIERT;
  }
}

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

Als Unterklasse Disko können wir nun getTyp() überschreiben oder nicht. Wenn wir die Methode nicht überschreiben, würde immer GebaeudeTyp.UNDEFINIERT das Ergebnis sein, was unerwünscht ist. Wir werden daher in den Unterklassen die Methode passend überschreiben. Damit Disko auch nicht die einzige Klasse in unserem Beispiel ist, setzten wir eine zweite Klasse Kirche dazu.

Listing 6.49   va/Kirche.java

package va;
public class Kirche extends Gebaeude
{
  @Override
    public GebaeudeTyp getTyp()
  {
    return GebaeudeTyp.KIRCHE;
  }
  }

Listing 6.50   va/Disko.java

package va;
public class Disko extends Gebaeude
{
  @Override
  public GebaeudeTyp getTyp()
  {
    return GebaeudeTyp.GASTSTAETTE;
  }
}

Es fehlen noch einige kleine Testzeilen:

Listing 6.51   va/PolyTester.java, Ausschnitt aus main()

Disko  d = new Disko();
Kirche k = new Kirche();
System.out.println( "Typ der Disko: "  + d.getTyp() ); // Typ der Disko: GASTSTAETTE
System.out.println( "Typ der Kirche: " + k.getTyp() ); // Typ der Kirche: KIRCHE

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

Eclipse

Eclipse zeigt bei der Tastenkombination (Ctrl)+(T) eine Typhierarchie an, standardmäßig die Oberklassen und bekannten Unterklassen.


Galileo Computing

6.9.1 Dynamisches Binden als Beispiel für Polymorphidowntop

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

Gebaeude g1 = new Disko();
Gebaeude g2 = new Kirche();

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

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

System.out.println( g1.getTyp() );
System.out.println( g2.getTyp() );

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

1. Da die Variablen g1 und g2 vom Typ Gebaeude sind, wird die Methode getTyp() von der Klasse Gebaeude aufgerufen, und in beiden Fällen ist die Rückgabe die Aufzählung GebaeudeTyp.UNDEFINIERT.
       
2. Die Laufzeitumgebung weiß, dass hinter der Variablen ein Disko- beziehungsweise Kirchen-Objekt steht.
       

Diese zweite Lösung ist richtig.

Listing 6.52   va/PolyTester.java, Ausschnitt aus main()

System.out.println( "Typ Gebäude 1: " + g1.getTyp() ); // Typ Gebäude 1: GASTSTAETTE
System.out.println( "Typ Gebäude 2: " + g2.getTyp() ); // Typ Gebäude 2: KIRCHE

Da hier aus dem statisch im Programmtext vereinbarten Typ der Variablen nicht abzulesen ist, welche Implementierung der Methode getTyp() 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, das dynamisches Binden noch deutlicher macht. In eine Stadt werden eine Reihe von Gebäuden gesetzt.

Listing 6.53   va/Stadt.java

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

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[] gebaeude )
  {
    for ( Gebaeude g : gebaeude )
      System.out.println( g.getTyp() ); // 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.9.2 Keine Polymorphie bei privaten, statischen und finalen Methodedowntop

Obwohl Methodenaufrufe in Java in der Regel dynamisch 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.54   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
{
  // Überschreibt nicht, daher kein @Override
    public 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 ohnehin geheim und versteckt. Die Unterklasse soll von den privaten Methoden in der Oberklasse gar nichts wissen. Statt von Überschreiben sprechen wir hier von Überdecken.

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 landen müssten, da wir ein Objekt vom Typ Unter haben. Bei privaten Methoden ist das aber anders, da sie nicht vererbt werden. Wenn eine aufgerufene Methode den Modifizierer private trägt, dann wird nicht dynamisch gebunden und unsicht.bar() bezieht sich bei furcht() auf die Methode aus Ober.

System.out.println( unsicht.bar() );   // 2

Anders wäre es, wenn bei furcht() der Sichtbarkeitsmodifizierer public wäre; wir würden dann die Ausgabe 1 bekommen.

Dass private und statische Funktionen nicht überschrieben werden, 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.

Schauen wir, was passiert, wenn wir in der Methode bar() über die this-Referenz auf ein Objekt vom Typ Unter casten.

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

Dann wird ausdrücklich diese furcht() aus Unter aufgerufen, was jedoch kein typisches objektorientiertes Konstrukt darstellt, da Oberklasse ihre Unterklasse im Allgemeinen nicht kennen. bar() in der Klasse Ober ist damit unnütz.


Galileo Computing

6.9.3 Polymorphie bei Konstruktoraufrufen  toptop

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, 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. Wir können aber in der Oberklasse genau jene Methode der Unterklasse aufrufen, die die Unterklasse aus der Oberklasse überschreibt. Da Methodenaufrufe dynamisch gebunden werden, kann eine Methode den Wert auslesen.

Listing 6.55   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";
  @Override
  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, indem 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.

 << zurück




Copyright © Galileo Press GmbH 2005
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, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de