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.7 Vererbundowntop

Neben der Assoziation von Objekten gibt es in der Objektorientierung eine weitere wichtige Möglichkeit zur Wiederverwendung: die Vererbung. Sie basiert auf der Idee, dass Eltern ihren Kindern Eigenschaften mitgeben. Vererbung bindet die Klassen noch dichter aneinander. Mittels dieser engen Verbindung können wir später sehen, dass Klassen in gewisser Weise austauschbar sind.


Hinweis   In Java können nur Untertypen von Klassen definiert werden. Einschränkungen von primitven Typen – etwa im Wertebereich oder in der Anzahl der Nachkomma-stellen – sind nicht möglich. Die Programmiersprache Ada erlaubt das zum Beispiel, und Untertypen sind bei XML-Schema üblich, wo etwa xs:short oder xs:unsignedByte ein Untertyp von xs:integer sind.


Galileo Computing

6.7.1 Vererbung in Java  downtop

Die Klassen in Java sind in einer Hierarchie geordnet. Von Object erben automatisch alle Klassen, direkt oder indirekt. Eine neu definierte Klasse kann durch das Schlüsselwort extends eine Klasse erweitern. Sie wird dann zur Unter- oder Subklasse beziehungsweise Kindklasse. Die Klasse, von der die Unterklasse erbt, heißt Oberklasse (auch Superklasse oder Elternklasse). Durch den Vererbungsmechanismus werden alle sichtbaren Eigenschaften der Oberklasse auf die Unterklasse übertragen. Eine Oberklasse vererbt also Eigenschaften, und die Unterklasse erbt sie.

Syntaktisch wird die Vererbung durch das Schlüsselwort extends beschrieben. Allgemein gilt für eine erbende Klasse Unter und eine Oberklasse Ober:

class Unter   extends   Ober
{
}

Da die Mehrfachvererbung in Java nicht gültig ist, steht hinter dem Schlüsselwort extends lediglich eine einzige Klasse.

Alles, was nun Ober an sichtbaren Eigenschaften besitzt, wird auf Unter vererbt. Die Klasse Unter kann die vererbten Eigenschaften nutzen. Wenn sich die Implementierung einer Methode der Oberklasse ändert, wird die Unterklasse diese Änderung mitbekommen.


Galileo Computing

6.7.2 Einfach- und Mehrfachvererbung  downtop

In Java ist auf direktem Weg nur die Einfachvererbung (engl. single inheritance) erlaubt. In der Einfachvererbung kann eine Klasse lediglich eine andere erweitern. In Programmiersprachen wie C++ können auch mehrere Klassen zu einer neuen verbunden werden. Dies bringt aber einige Probleme mit sich, die in Java vermieden werden. Nehmen wir an, die Klassen O1 und O2 definieren beide eine öffentliche Funktion f(), und U ist eine Klasse, die von O1 und O2 erbt. Steht in U ein Funktionsaufruf f(), ist nicht klar, welche der beiden Funktion gemeint ist. In C++ löst der Scope-Operator (::) das Problem, in dem der Entwickler immer angibt, aus welcher Oberklasse die Funktion anzusprechen ist.

Dazu gesellt sich das Diamanten-Problem (auch Rauten-Problem genannt). Zwei Klassen K1 und K2 erben von einer Oberklasse O eine Eigenschaft x, und eine Unterklasse U erbt von den Klassen K1 und K2. Lässt sich in O auf die Eigenschaft x zugreifen? Eigentlich existiert die Eigenschaft ja nur einmal und dürfte kein Grund zur Sorge sein. Dennoch stellt dieses Szenario ein Problem dar, weil der Compiler vergessen kann, dass es sich in den Unterklassen K1 und K2 nicht verändert hat; mit der Einfachvererbung kommt es erst gar nicht zu diesem Dilemma.


Galileo Computing

6.7.3 Gebäude modelliert  downtop

Wir wollen nun eine Klassenhierarchie für Gebäude aufbauen. Die Hierarchie geht von oben nach unten, von der Oberklasse zur Unterklasse. Im Fall einer Diskothek ist diese ein Gebäude, sodass schon die erste Ist-eine-Art-von-Hierarchie existiert. Eine Diskothek ist eine Art Gebäude. Denkbar wären auch spezielle Diskotheken, etwa Kinderdiskotheken. Wir können auch dann sagen: Eine Kinderdisko ist eine spezielle Art von Diskothek.

Schreiben wir die Hierarchie für Kinderdiskotheken auf. Der Quellcode der Oberklasse Disko muss dazu nicht geändert werden. Das ist typisch für die Modellierung mit Klassenhierarchien – die Oberklasse weiß von einer Unterklasse gar nichts!

Beginnen wir mit der Basisklasse Disko.

Listing 6.27   v7/Disko.java

public class Disko
{
  ...
}

Da keine ausdrückliche extends-Anweisung hinter dem Klassennamen steht, erbt die Klasse automatisch von Object, einer impliziten Basisklasse. Das ist jetzt nicht sonderlich spannend, aber KinderDisko wird interessant.

Listing 6.28   v7/KinderDisko.java

public class KinderDisko extends Disko
{
  public String maskottchen;
}

Die Definition der Klasse trägt den Anhang extends Disko und erbt somit alle sichtbaren Eigenschaften der Oberklasse. Sie selbst fügt der Klasse nur ein Attribut maskottchen zu.

Damit ergibt sich das nachfolgende UML-Diagramm. Vererbung ist durch einen Pfeil in Richtung der Oberklasse angegeben (siehe Abbildung 6.6).

Haben wir ein KinderDisko-Objekt erzeugt, können wir auf alle Eigenschaften der Kinderdisko zugreifen, aber auch auf die Eigenschaften, die geerbt wurden.

Listing 6.29   Ausschnitt aus KiDiDemo.java

KinderDisko saloon = new KinderDisko();
saloon.maskottchen = "Yosemite Sam";
saloon.personRein();

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

Abbildung 6.6   Eine Kinderdisko ist eine Spezialisierung einer Disko


Galileo Computing

6.7.4 Konstruktoren in der Vererbung  downtop

Obwohl Konstruktoren Ähnlichkeit 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 – darüber lässt sich streiten; in der Skript-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.

Ein Beispiel mit Konstruktorweiterleitung

Sehen wir uns noch einmal die Konstruktorverkettung an:

class Gebaeude
{
}
class Disko extends Gebaeude
{
}

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 das folgende Bild in den Klassen für die Laufzeitumgebung im Bytecode:

class Gebaeude
{
  Gebaeude() {
      super()  ;         // für Object()
  }
}
class Disko extends Gebaeude
{
  Disko() {
      super()  ;         // für Gebaeude()
  }
}

Wir sehen, dass wir nicht ausdrücklich super() schreiben müssen, weil dies der Compiler übernimmt.

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

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

public Vector( int initialCapacityint 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 Argumenten füllen

Mitunter ist es nötig, aus der Unterklasse nicht nur den Standard-Konstruktor anzusteuern, sondern einen anderen (parametrisierten) Konstruktor der Oberklasse. Dazu kann ein super()-Aufruf mit Argumenten gefüllt werden. Gründe dafür könnten sein:

gp  Ein parametrisierter Konstruktor der Unterklasse leitet die Argumente an die Oberklasse weiter.
gp  Wenn wir keinen Standard-Konstruktor in der Oberklasse anbieten, müssen wir in der Unterklasse mittels super(Argument ...) 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 )  ;
  }
}

Galileo Computing

6.7.5 Sichtbarkeit protected  downtop

Die Vererbung kann durch private eingeschränkt werden. Eine Subklasse erbt dann alles von einer Superklasse, was nicht private ist. Zusätzlich kommt zu private noch eine Sonderform protected hinzu. Hier kann auch eine Unterklasse alle Eigenschaften sehen. Nur von außen sind die Eigenschaften privat. Eine Ausnahme bilden jedoch Klassen, die im gleichen Paket sind; auch sie können die Eigenschaften einer protected-Klasse sehen. Damit ist protected mehr als nur die Sichtbarkeit für Unterklassen, denn wenn auch Klassen im gleichen Paket lesen können, ist die Sichtbarkeit fast public. Nehmen wir eine Klasse K und L im Paket p an. Deklariert K die Attribute protected, so kann L diese lesen und modifizieren.

Stellen wir uns ein anderes Beispiel mit zwei Klassen aus zwei unterschiedlichen Paketen vor. Ein gemeinsames Oberpaket pakettest besitzt die beiden Unterpakete berlusconi und stefani mit den Klassen Silvio und Stefano.

Listing 6.30   pakettest.berlusconi.Silvio

package pakettest.berlusconi;
public class Silvio
{
  protected String schulz = "einförmiger, supernationalistischer Blonde";
}

Die Klasse Silvio definiert lediglich eine geschützte Variable schulz. Sie wird also von allen Unterklassen und auch allen Klassen im gleichen Paket nutzbar sein. Wir definieren eine zweite Klasse Stefano jedoch in einem anderen Paket:

Listing 6.31   pakettest.stefani.Silvio

package pakettest.stefani;
import pakettest.berlusconi.Silvio;
public class Stefano extends Silvio
{
    String übernommen = schulz;
    void demokratieverständnis(   Silvio s   )
  {
//    _saito_fett_  s.schulz _saito_fettout_  ist hier nicht definiert
  }
}

Obwohl eine Unterklasse die protected-Eigenschaft schulz nutzen kann (in übernommen = schulz ist ein Zugriff auf die Oberklassenvariable), kann sie nicht über den Typ Silvio auf schulz zugreifen.


Galileo Computing

6.7.6 Das Substitutionsprinzip  downtop

Stellen wir uns vor, Bekannte kommen ausgehungert von einer Wandertour und fragen: »Haste was zu essen?« Die Frage zieht wohl darauf ab, dass es bei Hunger ziemlich egal ist, was wir anbieten, wichtig ist nur etwas Essbares. Daher können wir Eis, aber auch Frittierfett anbieten.

Diese Ausgangslage führt uns zu einem wichtigen Konzept in der Objektorientierung: Wenn wenig gefordert wird, kann mehr angeboten werden. Genauer gesagt: Wenn eine Unterklasse U die Oberklasse O erweitert, können wir überall, wo O gefordert wird, etwa als Parameter einer Funktion, auch ein U übergeben, denn wir werden mit der Unterklasse nur spezieller. Derjenige, dem wir mehr übergeben, kann damit zwar nichts anfangen, aber ablehnen wird er das Objekt nicht, da es alle geforderten Eigenschaften aufweist.

Weil an Stelle eines Objekts auch ein Objekt der Unterklasse auftauchen kann, sprechen wir von Substitution. Das Prinzip wurde von der Professorin Barbara Liskov formuliert und heißt daher auch Liskov’sches Substitutionsprinzip.

Bleiben wir bei unserem Beispiel des Parameters. Für unsere Disko-Vererbungsbeziehung bedeutet dies: Überall dort, wo ein Gebäude gefordert ist, können wir eine Disko übergeben oder auch eine Kirche, wenn Kirche eine Unterklasse von Gebäude ist. Auch können wir eine Kinderdisko eingeben, wenn sie eine Unterklasse von Disko ist. Denn alle diese Dinge sind vom Typ Gebäude und daher typkompatibel.

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

In der Java-Bibliothek finden sich zahllose weitere Beispiele. Häufigstes Anwendungsfeld sind Datenstrukturen – etwa eine Liste. Die Datenstrukturen nehmen beliebige Objekte entgegen, denn der Parametertyp ist Object – zu sehen etwa an der Methode add(Object) in java.util.ArrayList, der Klasse für Listen. Die Substitution besagt, dass wir alle Objekte dort einsetzen können, da alle Klassen von Object abgeleitet sind.


Galileo Computing

6.7.7 Automatische und explizite Typanpassundowntop

Das folgende Beispiel zeigt, dass auch ein Exemplar einer Unterklasse einer Variablen vom Typ der Oberklasse zugewiesen werden kann. Wir erzeugen zunächst ein KinderDisko-Objekt:

KinderDisko saloon = new KinderDisko();
Disko d = saloon;

Da eine Kinderdisko eine spezielle Disko ist (KinderDisko als Unterklasse von Disko), funktioniert diese Zuweisung. Auf den ersten Blick erscheint das nicht sonderlich sinnvoll, erfüllt aber einen Zweck: d übernimmt alle Eigenschaften einer Disko von der mächtigeren Klasse KinderDisko, verzichtet aber auf alle anderen Informationen, die eine Kinderdisko oder sonstige Unterklasse noch bietet, beispielsweise das Attribut maskottchen.

Die Klasse Disko bietet dabei das Attribut anzahlLeute an, sodass auch Folgendes problemlos ist:

System.out.println( d.anzahlLeute );

Versuchen wir jedoch eine spezielle Eigenschaft von KinderDisko zu benutzen, etwa das Attribut maskottchen, so ist dies nicht möglich:

System.out.println( d.maskottchen ); // geht nicht

Hier ist der Typ der Variablen d entscheidend. Der Compiler hat d vom Typ Disko kennen gelernt, daher weiß er nicht, dass d eigentlich ein verkapptes KinderDisko-Objekt ist. Genauso gut lässt sich keine neue Referenz vom Typ KinderDisko auf die Disko legen. Hier gilt wiederum, dass die Typen unvereinbar sind, sodass wir einen Compilerfehler erhalten:

KinderDisko kd = d;                  // geht nicht

Es ist aber möglich, das Objekt d durch eine Typumwandlung in eine KinderDisko umzuwandeln. Dies funktioniert aber lediglich dann, wenn d auch wirklich eine Kinderdisko ist. Dem Compiler ist das in dem Moment egal. Diese Bedingung wird erst zur Laufzeit geprüft:

KinderDisko kd = (KinderDisko) d;    // geht wohl

In den beiden folgenden Abschnitten erfahren wir, wieso das sinnvoll und ein mächtiges Konzept ist. Wir werden sehen, dass es möglich ist, eine Basisklasse zu schaffen, und diese verschiedenen Unterklassen Grundfunktionalität beibringen kann. So liefert die Basisklasse einen gemeinsamen Nenner.

Fassen wir die oben stehenden Zeilen in einem kompletten Programm zusammen:

Listing 6.32   v8/DiskoSubstitution.java

package v8;
public class DiskoSubstitution
{
  public static void main( String[] args )
  {
    KinderDisko saloon = new KinderDisko();
    saloon.maskottchen = "Yosemite Sam";
    saloon.personRein();
  Disko zumDickenHomer = new KinderDisko();
//  zumDickenHomer.maskottchen = "Homer Simpson";
  zumDickenHomer.personRein();
  Object hase = new KinderDisko();
//  hase.maskottchen = "Bugs Bunny";
//  hase.personRein();
  }
}

Galileo Computing

6.7.8 Typen testen mit dem binären Operator instanceof  downtop

In Java haben die Entwickler den Operator instanceof in den Wortschatz aufgenommen, mit dem Exemplare auf ihre Verwandtschaft mit einer Klasse geprüft werden können. Mit instanceof kann zur Laufzeit festgestellt werden, ob ein definiertes Objekt vom Typ einer Klasse ist. Dies ist sinnvoll, weil durch objektorientiertes Programmieren laufend Basisobjekte definiert und erweitert werden, und zum Teil verschwindet der Typ für den Compiler, wenn etwa Objekte in Datenstrukturen gelegt werden.

Listing 6.33   v8/ InstanceofDemo.java, Ausschnitt

boolean b;
String str = "Toll";
b = ( str   instanceof   String );                   // wahr
b = ( str   instanceof   Object );                   // wahr

Deklariert ist eine Variable str als Objekt vom Typ String. Für den zweiten Fall gilt: Alle Objekte gehen irgendwie aus Object hervor und sind somit logischerweise Erweiterungen.

b = ( str   instanceof   Date );                     // Compilerfehler

In diesem Fall ist Date keine Basisklasse für String, der Ausdruck ist falsch. Allerdings verweigert der Compiler schon die Übersetzung, da dieser Ausdruck auf keinen Fall passen kann, weil die Vererbungsbeziehungen schon inkompatibel sind.

Die bisherigen Beziehungen hätte der Compiler bereits herausfinden können. Vervollständigen wir das, um zu sehen, dass instanceof wirklich zur Laufzeit den Test machen muss:

KinderDisko saloon = new KinderDisko();
b = ( saloon instanceof KinderDisko );            // ja
Disko d = saloon;
b = ( d instanceof KinderDisko );                 // ja
Object o = saloon;
b = ( o instanceof Disko );                       // ja

Im letzten Fall wird saloon nur noch als einfaches java.lang.Object angesehen. Trotzdem erkennt instanceof hinter der Variablen o zur Laufezit die Disko. Zum Schluss:

Object  o  = new int[ 100 ];
boolean bo = ( o instanceof String );             // für den Compiler O.K.
boolean bf = ( new int[100] instanceof String );  // mag der Compiler nicht

Ein Test auf instanceof null ist immer falsch.


Galileo Computing

6.7.9 Array-Typen und Kovarianz  downtop

Die Aussage »Wer wenig will, kann viel bekommen« gilt auch für Arrays, denn wenn eine Klasse U Unterklasse einer Klasse O ist – und dann gilt Exemplar von U instanceof O –, ist auch U[] ein Untertyp von O[]. Diese Eigenschaft nennt sich Kovarianz. Da Object die Basisklasse aller Objekte ist, kann ein Object-Array auch alle anderen Objekte aufnehmen.

Object[] os = new Object[1];
Disko[]  ds = new Disko[1];
Disko d = new Disko();
System.out.println( d  instanceof Object );    // true
System.out.println( ds instanceof Object[] );  // true
os[0] = d;

Bauen wir uns eine Funktion dummGelaufen() und schauen, was passiert:

public static void dummGelaufen( Object[] feldObject d )
{
  feld[0] = d;
}

Das Element d soll einfach an die erste Stelle ins Feld gesetzt werden mit den soeben definierten Variablen os, ds, und d rufen wir auf:

dummGelaufen( dsd );
dummGelaufen( osd );

Kein Problem! Die Variable d referenziert ein Disko-Objekt, das sich in einem Disko-Array abspeichern lässt. Der zweite Aufruf funktioniert ebenfalls, denn eine Disko lässt sich in einem Object-Feld speichern, da ja ein Object ein Basistyp ist. Ein Dilemma wäre es jedoch, wenn ein Feld nicht den richtigen Typ bekommt.

dummGelaufen( dsnew Date() );

Das Ergebnis ist eine ArrayStoreException. Das haben wir aber auch verdient, denn ein Date-Objekt lässt sich nicht in einem Disko-Feld speichern. Selbst ein new Object() hätte zu einem Problem geführt. Das Typsystem von Java kann diese Spitzfindigkeit nicht prüfen. Erst zur Laufzeit ist ein Test möglich, mit dem denkbar bitteren Ergebnis einer ArrayStoreException.


Galileo Computing

6.7.10 Methoden überschreibedowntop

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 eine andere Parameteranzahl oder andere -typen 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 und dem gleichen Methodennamen. Mit anderen Worten: Es existiert in der Unterklasse eine Methode mit der gleichen Signatur wie in der Oberklasse. Zwar gehört der Rückgabetyp nicht zur Signatur, doch muss eine Unterklasse diesen Typ übernehmen. Das gilt immer bei primitiven Typen, bei Referenztypen können es in Unterklassen auch Untertypen sein.

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. Überladene Funktionen und überschriebene Funktionen sind damit etwas anderes, da eine überladene Funktion mit der Ursprungsfunktion nur »zufällig« den Namen teilt, aber sonst keinen Bezug zur Logik hat.


Beispiel   In eine Disko dürfen nur Personen, die über 18 sind. Eine Methode personRein() soll daher testen, ob Personen Eintritt bekommen. Da KinderDisko von Disko erbt, hätten wir ein Problem, wenn wir personRein() so stehen lassen – dann würde kein Kind mehr in die Kinderdisko kommen! Wir überschreiben daher die Funktion personRein() in der Unterklasse KinderDisko, sodass bereits Personen ab einem Alter von 6 Jahren in die Spezialdisko kommen.

Listing 6.34   Ausschnitt aus v9/Disko.java

  public void personRein( int alter )
  {
  if ( alter >= 18 )
    anzahlPersonen++;
  else
    System.out.println( "Noch zu jung!" );
}

Listing 6.35   Ausschnitt aus v9/Disko.java

public class KinderDisko extends Disko
{
  public String maskottchen;
  @Override
    public void personRein( int alter )
    {
    if ( alter >= 6 )
      anzahlPersonen++;
    else
      System.out.println( "Du bist noch zu klein! Du musst draußen spielen." );
  }
}

Damit die Unterklasse auf das Attribut anzahlPerson zugreifen kann, machen wir es in der Oberklasse protected. Das UML-Diagramm zeigt es an der Raute.

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

Abschließend schreiben wir uns noch ein Testprogramm. Wird ein KinderDisko-Objekt angelegt, und rufen wir personRein() auf, so wird die überschriebene Funktion verwendet.

Listing 6.36   v9/KiDiDemo.java

public class KiDiDemo
{
  public static void main( String[] args )
  {
    Disko ttwister = new Disko();
    ttwister.personRein( 45 );
    ttwister.personRein( 10 );  // Noch zu jung!
    KinderDisko flohzirkus = new KinderDisko();
    flohzirkus.personRein( 10 );
    flohzirkus.personRein( 5 ); // Du bist noch zu klein! Du musst draußen spielen.
  }
}

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.

Die Annotation @Override

Unser Beispiel nutzt die Annotation @Override bei der Methode personRein() – und schon früher bei toString() – und macht auf diese Weise deutlich, dass die Unterklasse eine Methode der Oberklasse überschreibt.

  @Override
  public void personRein( int alter )

Annotationen sind zusätzliche Modifizierer, die entweder vom Compiler überprüft werden, oder von uns nachträglich abgefragt werden können. Obwohl wir die Annotation @Override nicht nutzen müssen, hat sie den Vorteil, dass der Compiler überprüft, ob wir tatsächlich eine Methode aus der Oberklasse überschreiben – haben wir uns im Methodennamen verschrieben und würde die Unterklasse auf diese Weise eine neue Methode hinzufügen, so würde der Compiler das als Fehler melden. Fehler wie tostring() fallen schnell auf.


Beispiel   Die Annotation @Override bedeutet nicht, dass diese Methode in Unterklassen überschrieben werden muss, sondern nur, dass sie selbst eine Methode überschreibt.


Galileo Computing

6.7.11 Mit super eine Methode der Oberklasse aufrufen  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;
  @Override
  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 in den Namensraum der Oberklasse geht.

Wir rufen zwar in m() die überschriebene Methode auf, benötigen aber dafür ihren Namen. Interessant wäre eine Variante von super – die es aber nicht gibt –, mit der der Methodenname egal ist. So muss bei einer Änderung des Namens an zwei Stellen 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
{
  @Override
  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 Funktion ändern in:

void m()
{
  ((PrahlerOber)this).m();
}

Meine Leser sollten einmal testen, warum hier nicht m() aus PrahlerOber aufgerufen wird, sondern etwas anderes. Eine genauere Erklärung des dynamischen Bindens folgt in Kürze.

this == super

In unseren Köpfen geistert vielleicht die Vorstellung herum, wonach die Referenz super ein Verweis auf ein Objekt der Oberklasse ist. Das ist aber so nicht ganz korrekt, obwohl sich diese Ausdrucksweise so eingebürgert hat. Es handelt sich für die Unterklasse um ein Objekt, das 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. Dies soll das nachfolgende Beispiel noch einmal belegen:

Listing 6.37   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 String-Reprä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 die 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.7.12 Kovariante Rückgabetypen  downtop

Seit Java 5 kann eine Unterklasse eine Methode mit nichtprimitivem Rückgabetyp überschreiben, die einen anderen Rückgabetyp besitzt, nämlich den der Unterklasse. Das nennt sich kovarianter Rückgabetyp und ist sehr praktisch, das sich auf diese Weise Entwickler oft explizite Typanpassungen ersparen können.


Beispiel   Die Klasse Lautsprecher definiert eine Methode gibThis(), die lediglich die this-Referenz zurückgibt. Eine Unterklasse überschreibt die Methode und liefert einen spezielleren Typ.

Listing 6.38   FetterBassLautsprecher.java

class Lautsprecher
{
    Lautsprecher   gibThis()
  {
    return this;
  }
}
class FetterBassLautsprecher extends Lautsprecher
{
  @Override
  FetterBassLautsprecher gibThis()
//  Lautsprecher gibThis()
  {
    return this;
  }
}

Merkwürdig in diesem Zusammenhang ist, dass es schon immer in Java veränderte Zugriffsrechte gegeben hat. Eine Unterklasse kann die Sichtbarkeit erweitern. Auch bei Ausnahmen kann eine Unterklasse speziellere Ausnahmen beziehungsweise ganz andere als die Methode der Oberklasse erzeugen.


Galileo Computing

6.7.13 Finale Klassen  downtop

Soll eine Klasse keine Unterklassen bilden, werden Klassen mit dem Modifizierer final versehen. Dadurch kann vermieden werden, dass Unterklassen Eigenschaften nachträglich verändern können. Ein Versuch, von einer finalen Klasse zu erben, führt zu einem Compilerfehler. Dies schränkt zwar die objektorientierte Wiederverwendung ein, wird aber aufgrund von Sicherheitsaspekten in Kauf genommen. Eine Passwortüberprüfung soll zum Beispiel nicht einfach überschrieben werden können.

In der Java-Bibliothek gibt es eine Reihe von finalen Klassen, von denen wir einige bereits kennen gelernt haben:

gp  String, StringBuffer, StringBuilder
gp  Wrapper-Klasssen
gp  Math
gp  System
gp  Font, Color

Galileo Computing

6.7.14 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.7.15 Zusammenfassung zur Sichtbarkeit  downtop

In Java gibt es vier Sichtbarkeiten und drei Sichtbarkeits-Modifizierer.

gp  Öffentliche Typen und Eigenschaften werden mit dem Modifizierer public deklariert. Die Typen sind überall sichtbar, also kann auf öffentliche Eigenschaften jede Klasse und Unterklasse aus einem beliebigen anderen Paket zugreifen. Die mit public deklarierten Methoden und Variablen sind überall dort sichtbar, wo auch die Klasse sichtbar ist. Bei einer unsichtbaren Klasse sind auch die Eigenschaften unsichtbar.
gp  Der Modifizierer private für Typen ist seltener, da er sich nur dann einsetzen lässt, wenn in einer Datei mehrere Typen definiert werden. Derjenige Typ, der den Dateinamen bestimmt, kann nicht privat sein, doch andere Typen dürfen unsichtbar sein – sie lassen sich dann nur vom dem sichtbareren Typ verwenden. Auch innere Klassen können privat sein. Die mit private deklarierten Methoden und Variablen sind nur innerhalb der sie definierenden Klasse sichtbar. Eine Ausnahme bilden innere Klassen, die auch auf private Eigenschaften zugreifen können. Auch wenn diese Klasse erweitert wird, sind die Elemente nicht sichtbar.
gp  Während private und public Extreme darstellen, liegt die Paketsichtbarkeit dazwischen. Sie ist die Standard-Sichtbarkeit und kommt ohne Modifizierer aus. Paketsichtbare Typen und Eigenschaften sind nur für die Klassen aus dem gleichen Paket sichtbar, also weder für Klassen noch Unterklassen aus anderen Paketen.
gp  Der Sichtbarkeits-Modifizierer protected hat eine Doppelfunktion. Zum einen hat er die gleiche Bedeutung wie Paketsichtbarkeit, und zum anderen gibt er die Elemente für Unterklassen frei. Dabei ist es egal, ob die Unterklassen aus dem eigenen Paket stammen (da würde ja die Standard-Sichtbarkeit reichen), oder aus einem anderen Paket. Eine Kombination aus private protected wäre wünschenswert, um die Eigenschaften nur für die Unterklassen sichtbar zu machen und nicht gleich für die Klassen aus dem gleichen Paket; das aber haben die Entwickler für die aktuelle Java-Version nicht vorgesehen.

Tabelle 6.1   Wer sieht welche Eigenschaften bei welcher Sichtbarkeit?

Die R sieht/sehen Q Eigenschaften eigene Klasse Klassen im gleichen Paket Unterklassen andere Klassen
public Ja Ja Ja Ja
protected Ja Ja Ja Nein
»paketsichtbar« Ja Ja Nein Nein
private Ja Nein Nein Nein

Der Einsatz der Sichtbarkeitsstufen über die Schlüsselworte public, private und protected und der Standard »paketsichtbar« ohne explizites Schlüsselwort sollte überlegt erfolgen. Objektorientierte Programmierung zeichnet sich durch überlegten Einsatz von Klassen und deren Beziehungen aus. Am besten ist die restriktivste Beschreibung; also nie mehr Öffentlichkeit als notwendig. Das hilft, die Abhängigkeiten zu minimieren und später Inneres einfacher zu verändern.


Galileo Computing

6.7.16 Sichtbarkeit in der UML  downtop

Für die Sichtbarkeit von Attributen und Operationen sieht die UML diverse Symbole vor, die vor die jeweilige Eigenschaft gesetzt werden:


Symbol Sichtbarkeit
+ Öffentlich
Privat
# Geschützt (protected)
~ Paketsichtbar


Hinweis    Wenn in der UML kein Sichtbarkeitsmodifizierer steht, so bedeutet das nicht paketsichtbar! Es heißt nur, dass dies noch nicht definiert ist.


Galileo Computing

6.7.17 Zusammenfassung: Konstruktoren und Methoden  toptop

Methoden und Konstruktoren haben einige Gemeinsamkeiten in der Signatur, aber auch einige wichtige Unterschiede wie den Rückgabewert oder den Gebrauch von this und super. Die folgende Tabelle fasst die Unterschiede und Gemeinsamkeiten noch einmal kompakt zusammen:


Tabelle 6.2   Gegenüberstellung von Konstruktoren und Methoden

Benutzung Konstruktoren Methoden
Modifizierer Sichtbarkeit public, protected, paketsichtbar und private. Können nicht abstract, final, native, static oder synchronized sein. Sichtbarkeit public, protected, paketsichtbar und private Können abstract, final, native, static oder synchronized sein.
Rückgabewert Kein Rückgabewert, auch nicht void Rückgabetyp oder void
Bezeichnername Gleicher Name wie die Klasse. Beginnt daher in der Regel mit einem Großbuchstaben. Beliebig. In der Regel beginnt er mit einem Kleinbuchstaben.
this this() bezieht sich auf einen anderen Konstruktor der gleichen Klasse. Wird this()benutzt, muss this() in der ersten Zeile stehen. this ist eine Referenz in Objektmethoden, die sich auf das aktuelle Exemplar bezieht.
super Ruft einen Konstruktor der Oberklasse auf. Wird super() benutzt, muss super() in der ersten Zeile stehen. super ist eine Referenz, die auf die Oberklasse zeigt. Damit lassen sich überschriebene Methoden aufrufen.
Vererbung Konstruktoren werden nicht vererbt. Sichtbare Methoden werden vererbt.

Reihenfolge der Eigenschaften in Klassen

Verschiedene Elemente einer Klasse müssen in einer Klasse untergebracht werden. Eine verbreitete Reihenfolge ist in Sektionen, die mit statischen Eigenschaften beginnt und mit Objekteigenschaften weitergeht:

gp  Klassenvariablen (statische Eigenschaften)
gp  Objektvariablen
gp  Konstruktoren
gp  Methoden

Innerhalb eines Blocks werden die Informationen oft auch bezüglich ihrer Zugriffsrechte sortiert. Am Anfang stehen sichtbare Eigenschaften und tiefer private. Der öffentliche Teil befindet sich deswegen am Anfang, da wir uns so schnell einen Überblick verschaffen können. Der zweite Teil ist dann nur noch für die erbenden Klassen interessant, und der letzte Teil beschreibt allein geschützte Informationen für die Entwickler. Die Reihenfolge kann aber problemlos gebrochen werden, in dem private Methoden hinter öffentlichen stehen, um zusammenhängende Teile auch zusammenzuhalten.




1  Die Zeitschrift »Discover« zählt sie zu den 50 wichtigsten Frauen in der Wissenschaft. Genauso übrigens wie Heidi Hammel, eine Astronomin, die sich dem Jupiter verschrieben hat und sich an bayerischem Starkbier erfreut.

2  Schon seltsam, dass synchronized nicht erlaubt ist, aber ein Konstruktor ist implizit synchronized.

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