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

Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5 (Tiger-Release)
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 Attribute
    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 Statische Blöcke
  gp 6.5 Konstanten und Aufzählungen
    gp 6.5.1 Konstanten mit dem Schlüsselwort final bei Variablen
    gp 6.5.2 Problem mit finalen Klassenvariablen
    gp 6.5.3 Typsicherere Konstanten
    gp 6.5.4 Aufzählungen und 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 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.6.9 Zusammenfassung: Konstruktoren und Methoden
  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
    gp 6.7.6 Das Substitutionsprinzip
    gp 6.7.7 Automatische und explizite Typanpassung
    gp 6.7.8 Array-Typen
    gp 6.7.9 Finale Klassen
    gp 6.7.10 Unterklassen prüfen mit dem Operator instanceof
    gp 6.7.11 Methoden überschreiben
    gp 6.7.12 super: Aufrufen einer Methode aus der Oberklasse
    gp 6.7.13 Nicht überschreibbare Funktionen
  gp 6.8 Die oberste aller Klassen: Object
    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
    gp 6.10.1 Abstrakte Klassen
    gp 6.10.2 Abstrakte Methoden
    gp 6.10.3 Über abstract final
  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 CharSequence als Beispiel einer Schnittstelle
    gp 6.11.7 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 Eine Sich-Selbst-Implementierung
    gp 6.12.6 this und Vererbung
    gp 6.12.7 Implementierung einer verketteten Liste
    gp 6.12.8 Funktionszeiger
  gp 6.13 Generische Datentypen
    gp 6.13.1 Einfache Klassenschablonen
    gp 6.13.2 Einfache Methodenschablonen
    gp 6.13.3 Generics und Vererbung
    gp 6.13.4 Einschränken der Typen
    gp 6.13.5 Umsetzen der Generics, Typlöschung und Raw-Types
    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 Gegenseitige Abhängigkeiten von Klassen
  gp 6.16 Veraltete (deprecated) Methoden/Konstruktoren


Galileo Computing

6.10 Abstrakte Klassedowntop

Nicht immer soll eine Klasse sofort ausprogrammiert werden. Dies ist der Fall, wenn die Oberklasse lediglich Methoden für die Unterklassen vorgeben möchte, aber nicht weiß, wie sie diese implementieren soll. In Java gibt es dazu zwei Konzepte: abstrakte Klassen und Schnittstellen (engl. interfaces).


Galileo Computing

6.10.1 Abstrakte Klassen  downtop

Bisher haben wir Vererbung eingesetzt, und jede Klasse konnte Objekte bilden. Das ist allerdings nicht immer sinnvoll, nämlich genau dann, wenn eine Klasse nur in einer Vererbungshierarchie existieren soll. Sie kann dann als Modellierungsklasse eine Ist-eine-Art-von-Beziehung ausdrücken und Signaturen für die Unterklassen vorgeben. Eine Oberklasse besitzt dabei Vorgaben für die Unterklasse, das heißt, alle Unterklassen erben die Methoden. Ein Exemplar der Oberklasse selbst muss nicht existieren.

Um das in Java auszudrücken, deklarieren wir die Oberklasse mit dem Modifizierer abstract. Von dieser Klasse können dann keine Exemplare gebildet werden. Ansonsten verhalten sich die abstrakten Klassen wie normale, sie enthalten die gleichen Eigenschaften und können auch selbst von anderen Klassen erben. Abstrakte Klassen sind das Gegenteil von konkreten Klassen.


Beispiel   Eine abstrakte Klasse Gebaeude ist die Oberklasse für konkrete Gebäude.

  abstract   class Gebaeude
{
  private int quadratmeter;

  public int getQuadratmeter()
  {
    return quadratmeter;
  }

  protected void setQuadratmeter( int qm )
  {
    quadratmeter = qm;
  }
}

Mit dieser abstrakten Klasse Gebaeude drücken wir aus, dass es eine allgemeine Klasse ist, zu der keine konkreten Objekte existieren. Es gibt in der realen Welt schließlich kein Gebäude, das nur ein allgemeines, unspezifiziertes Gebäude ist, sondern nur spezielle Unterarten von Gebäuden, zum Beispiel Diskotheken, Kirchen und so weiter. Es macht also keinen Sinn, ein Exemplar der Klasse Gebaeude zu bilden. Die Klasse soll nur in der Hierarchie auftauchen, um alle konkreten Kleidungsklassen als Typ von Gebaeude darzustellen. Das zeigt, dass Oberklassen allgemeiner gehalten sind und Unterklassen weiter spezialisieren. Ein Versuch, ein Objekt der abstrakten Klasse zu bilden, führt zu einem Compilerfehler.

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

Abbildung 6.7   In der UML werden abstrakte Klassenamen kursiv gesetzt

Die abstrakten Klassen werden normal in der Vererbung eingesetzt. Eine Klasse kann die abstrakte Klasse erweitern und auch selbst wieder abstrakt sein.


Beispiel   Wenn eine Klasse von einer abstrakten Klasse erbt, dann ist sie vom Typ aller Oberklassen, auch vom Typ der abstrakten Klasse.

class Disko extends Gebaeude
{
  int anzahlLeute;
}

Das heißt, jemand könnte problemlos schreiben:


Disko     d1       = new Disko();
Gebaeude  d2       = new Disko();
Object    d3       = new Disko();
Gebaeude  diskos[] = new Gebaeude[]{ new Disko(), new Disko() };

Galileo Computing

6.10.2 Abstrakte Methoden  downtop

Das Schlüsselwort abstract leitet die Definition einer abstrakten Klasse ein. Eine Klasse kann ebenso abstrakt sein wie eine Methode. Eine abstrakte Methode definiert lediglich die Signatur, und eine Unterklasse implementiert dann irgendwann diese Methode. Die Klasse ist dann nur für den Kopf der Methode zuständig, während die Implementierung an anderer Stelle erfolgt. Durch abstrakte Methoden wird ausgedrückt, dass die Oberklasse keine Ahnung von der Implementierung hat und dass sich die Unterklassen darum kümmern müssen.

Erinnern wir uns an das Gebäude zurück, welches mit getId() eine Kennung liefert kann. Wir hatten damals für die Id eines unbekannten Gebäudes die Kennung UNDEFINIERT zurückgegeben. Doch schön ist das nicht! Es wäre viel angenehmer, wenn das Gebäude die Methode getId() abstrakt definieren würde. Dann könnten wir die Unterklassen – von denen wir Disko und Kirche definiert hatten – dazu auffordern, die Funktion zu überschreiben, damit sich konkrete Exemplare bilden lassen.

Listing 6.46   vc/Gebaeude.java


package vc;

abstract class Gebaeude
{
  public final static int DISKO  = 1;
  public final static int KIRCHE = 2;

  /**
   * Liefert den Typ des konkreten Gebäudes.
   *
   * @return Gebäudetyp.
   */
    public abstract int getId();  
}

Die Definition einer abstrakten Methode wird mit einem Semikolon abgeschlossen.

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

Abbildung 6.8   Auch abstrakte Methoden werden in UML kursiv gesetzt

Ist mindestens eine Methode abstrakt, so ist es automatisch die ganze Klasse. Deshalb müssen wir das Schlüsselwort abstract ausdrücklich vor den Klassennamen schreiben. Vergessen wir das Schlüsselwort abstract bei einer solchen Klasse, so erhalten wir einen Compilerfehler. Eine Klasse mit einer abstrakten Methode muss abstrakt sein, denn sonst könnte irgendjemand ein Exemplar konstruieren und genau diese Methode aufrufen.

Versuchen wir, ein Exemplar einer abstrakten Klasse zu erzeugen, so bekommen wir ebenfalls einen Compilerfehler. Auch ein indirekter Weg über die Class-Methode newInstance() bringt uns nicht zum Ziel, sondern bringt nur eine InstantiationException ein.

Vererben von abstrakten Methoden

Wenn wir von einer Klasse abstrakte Methoden erben, so haben wir zwei Möglichkeiten:

1. Wir überschreiben alle abstakten Methoden und implementieren sie. Dann kann die erbende Klasse gegebenenfalls korrekt angelegt werden.
       
2. Wir überschreiben die abstrakte Methode nicht, so dass sie normal vererbt wird. Das bedeutet, eine abstrakte Methode bleibt in unserer Klasse und die Klasse muss wiederum abstrakt sein.
       

Beispiel   Eine Hierarchie von Tieren wird aufgebaut. Die abstrakte Oberklasse Tier schreibt allen Tieren vor, dass sie eine Anfragemethode istSäuger() und eine Methode ausgabe() implementieren müssen. Es ist einleuchtend, dass die Oberklasse nichts über konkrete Tiere weiß und dass das ein Job der Unterklassen ist. Zunächst wieder das UML-Diagramm. Die Umlaute sind ersetzt.

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

Listing 6.47   AbstractDemo.java, Teil 1


abstract class Tier
{
  int alter = -1;

  void alterSetzen( int a ) { alter = a; }

  abstract boolean istSäuger();

  abstract void ausgabe();
}
abstract class Säugetier extends Tier
{
  boolean istSäuger() { return true; }
}

class Mensch extends Säugetier
{
  void ausgabe() {
    System.out.println( "Ich bin ein Mensch" );
  }
}

class Delfin extends Säugetier
{
  void ausgabe() {
    System.out.println( "Ich bin ein Delfin" );
  }
}

ausgabe() ist eine Methode, die für die jeweiligen Implementierungen eine kurze Meldung auf dem Schirm ausgibt. Da alle erweiternden Klassen jeweils andere Zeichenketten ausgeben, setzen wir die Methode abstract. Damit muss aber auch die Klasse Tier abstrakt sein. In der ersten Ableitung Säugetier können wir nun Beliebiges hinzufügen, implementieren aber aufgrund der nun bekannten Informationen nur die Methode istSäuger(), die Methode ausgabe() jedoch noch nicht. Auch Säugetier muss wieder abstract sein. Die dritte Klasse ist nun Mensch. Sie erweitert Säugetier und liefert eine Implementierung für ausgabe(). Damit muss sie nicht mehr abstrakt sein.

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

Implementiert eine Klasse nicht alle geerbten abstrakten Methoden, so muss die Klasse selbst wieder abstrakt sein. Eclipse bietet uns mit (Strg)+(1) an, entweder die Klasse abstrakt zu machen, oder alle geerbten abstrakten Methoden mit einem Dummy-Rumpf zu implementieren.

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

Die Polmorphie bei Tieren, Menschen und Delfinen

Es ist durch die automatische Typanpassung ohne weiteres möglich, einem Tier-Objekt eine Referenz für ein Mensch- beziehungsweise Säugetier-Objekt zuzuweisen. Also ist Folgendes richtig:


Tier m = new Mensch(),
     d = new Delfin();

Wird ein Mensch- oder Delfin-Objekt erzeugt, so wird der Konstruktor dieser Klassen aufgerufen. Dieser bewirkt einen Aufruf des Konstruktors der Superklasse. Und obwohl diese abstract ist, besitzt sie wie alle anderen Klassen einen Standard-Konstruktor (nur, dass wir ihn nicht mit new direkt aufrufen können). Des Weiteren werden beim Aufruf von Mensch() auch noch die Attribute initialisiert, so dass alter auf -1 gesetzt wird.


Beispiel   Wir rufen ausgabe() von einem Mensch- und Delfin-Objekt auf.

Listing 6.48   AbstractDemo.java, Teil 2


public class AbstractDemo
{
  public static void main( String args[] )
  {
    Tier m = new Mensch(),
         d = new Delfin();

    m.ausgabe();
    d.ausgabe();
  }
}

Das Programm liefert:


Ich bin ein Mensch
Ich bin ein Delfin

Galileo Computing

6.10.3 Über abstract final  toptop

Wenn wir eine Klasse als final deklarieren, so bedeutet dies, dass es von dieser Klasse keine Unterklassen geben kann. Objekte können immer noch erzeugt werden. Deklarieren wir eine Klasse dagegen als abstract, so ist diese Klasse meist für die Vererbung vorgesehen und kann nicht mit dem new-Operator zum Exemplar gebracht werden. Nehmen wir an, wir wollten eine Klasse konstruieren, von der es weder Exemplare geben darf (abstract) noch aus der abgeleitet werden kann (final). Deshalb kämen wir auf die Idee, einfach abstract final zu schreiben. Doch der Compiler meldet in diesem Fall:


A class may not be declared both "final" and "abstract".

Die Sprache verbietet, dass eine Klasse zugleich abstract und final sein kann. Doch es gibt Beispiele für Klassen – etwa Math –, wo das sinnvoll wäre. Daher müssen wir zu einem Trick greifen. Die Klasse wird zunächst als final deklariert, so dass es keine Unterklassen geben kann. Damit über den Konstruktor keine Exemplare gebildet werden können, implementieren wir einen privaten Standard-Konstruktor, so ist das Problem gelöst. Jetzt kann die Klasse nur Exemplare von sich selbst anlegen. Diese Klasse stattdessen als abstract zu deklarieren, würde das Problem ebenfalls lösen. Zwar könnte jemand versuchen, Unterklassen zu definieren, doch wenn wir wieder einen privaten Standard-Konstruktor einfügen, lassen sich keine Unterklassen mehr definieren, da kein Konstruktor aus der Oberklasse sichtbar ist. Damit ist die Aufrufkette der Konstruktoren unterbrochen. Bei einer finalen Klasse mit privatem Konstruktor erlauben wir allerdings der Klasse selbst, noch ein Objekt anzulegen. Das ist dann die beste Lösung. Ein Design-Pattern mit dem Namen »Singleton« macht genau dies. Möglicherweise wird es in Zukunft in Java auch abstract final-Klassen geben.

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






1   Während in Java eine Klasse abstract definiert wird, wird in EIFFEL ein Unterprogramm als deferred gekennzeichnet. Das heißt, die Implementierung wird aufgeschoben.





Copyright © 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