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


Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Buch: Java ist auch eine Insel - Zum Katalog
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.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 Vererbung
    gp 6.6.1 Vererbung in Java
    gp 6.6.2 Einfach- und Mehrfachvererbung
    gp 6.6.3 Kleidungsstücke modelliert
    gp 6.6.4 Sichtbarkeit
    gp 6.6.5 Das Substitutionsprinzip
    gp 6.6.6 Automatische und Explizite Typanpassung
    gp 6.6.7 Finale Klassen
    gp 6.6.8 Unterklassen prüfen mit dem Operator instanceof
  gp 6.7 Methoden überschreiben
    gp 6.7.1 super: Aufrufen einer Methode aus der Oberklasse
    gp 6.7.2 Nicht überschreibbare Funktionen
    gp 6.7.3 Fehlende kovariante Rückgabewerte
  gp 6.8 Die Oberste aller Klassen: Object
    gp 6.8.1 Klassenobjekte
    gp 6.8.2 Hashcodes
    gp 6.8.3 Objektidentifikation mit toString()
    gp 6.8.4 Objektgleichheit mit equals() und Identität
    gp 6.8.5 Klonen eines Objekts mit clone()
    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 Konstruktoren in der Vererbung
  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 Die Mehrfachvererbung bei Schnittstellen
    gp 6.11.2 Erweitern von Interfaces – Subinterfaces
    gp 6.11.3 Vererbte Konstanten bei Schnittstellen
    gp 6.11.4 Vordefinierte Methoden einer Schnittstelle
    gp 6.11.5 CharSequence als Beispiel einer Schnittstelle
  gp 6.12 Innere Klassen
    gp 6.12.1 Geschachtelte Top-Level 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 Gegenseitige Abhängigkeiten von Klassen
  gp 6.14 Pakete

Kapitel 6 Eigene Klassen schreiben

Das Gesetz ist der abstrakte Ausdruck des allgemeinen
an und für sich seienden Willens.
Georg Wilhelm Friedrich Hegel


Galileo Computing

6.1 Eigene Klassen definieren  downtop

Die Deklaration einer Klasse wird durch das Schlüsselwort class eingeleitet. Wir wollen das am Beispiel der Klasse Socke darstellen. Diese einfache Klasse definiert Daten und Methoden. Die Signatur einer Methode bestimmt ihren Namen und ihre Parameterliste. Die Socke-Klasse speichert wesentliche Attribute, die jeder Socke zugeordnet werden.

Abbildung 6.1  
Abbildung

Zu unserer Socken-Klasse wollen wir ein konkretes Java-Programm angeben. Eine Klasse Socke definiert die Attribute gewicht und farbe, und die andere Klasse erzeugt in der main()-Funktion ein Socke-Objekt. Wir erkennen am Schlüsselwort private, dass es Daten geben kann, die nach außen nicht sichtbar sind, da der Compiler die Sichtbarkeit erzwingt. Innerhalb der Klasse lässt sich das Attribut selbstverständlich verwenden. Wer außer der Klasse sollte es sonst können?

Abbildung Listing 6.1   SockeDemo.java
class Socke
{
public String farbe;
public int gewicht;

public void trockne()
{
istTrocken = true;
}

public void wasche()
{
istTrocken = false;
}

public boolean istTrocken()
{
return istTrocken;
}

private boolean istTrocken;
}

public class SockeDemo
{
public static void main( String args[] )
{
Socke stinki;

stinki = new Socke();
stinki.farbe = "rot";
stinki.gewicht = 565;
stinki.wasche();
System.out.println( "Ist die Socke trocken? " +
stinki.istTrocken() );
}
}

Die angegebene Klasse enthält die Methode trockne() und zwei Objekt-Variablen. Um ein neu angelegtes Socke-Objekt zum Waschen aufzufordern, ruft die main()-Methode die Methode wasche() für das erzeugte Objekt auf: die Nachricht (auch Botschaft) wasche() wird an das gewünschte Exemplar der Klasse Socke geschickt. In der Konsolenausgabe erfahren wir dann über istTrocken(), ob die Socke feucht ist oder nicht. istTrocken() gibt ein boolean zurück. Damit kapselt die Methode die private-Variable istTrocken, auf die kein Zugriff von außen möglich ist. Das Beispiel zeigt, dass ein Attribut und eine Methode den gleichen Namen besitzen können.


Galileo Computing

6.1.1 Methodenaufrufe und Nebeneffekte  downtop

Alle Attribute und Operationen einer Klasse sind in der Klasse selbst sichtbar. Das heißt, innerhalb einer Klasse werden die Objektvariablen und Funktionen mit ihrem Namen verwendet. Somit greift die Funktion trocknen() direkt auf die möglichen Attribute zu. Das wird oft für Nebeneffekte (Seiteneffekte) genutzt. Eine Methode wie trocknen() ändert ausdrücklich eine Objektvariable und verändert so den Zustand des Objekts.


Galileo Computing

6.1.2 Argumentübergabe mit Referenzen  downtop

In Java werden alle Datentypen als Wert übergeben (engl. copy by value). Das heißt, die formalen Parameter sind lokale Variablen des Unterprogramms, die mit den aktuellen Parameterwerten initialisiert werden. Objekte werden bei der Parameterübergabe nicht kopiert, sondern es wird ihre Referenz übergeben. Die aufgerufene Methode kann dann das Objekt verändern. Dies muss in der Dokumentation der Methode angegeben werden.

Listing 6.2   ZuOftGewaschen.java
class WaschSocke
{
String farbe;
}

class Waschmaschine
{
static void auswaschen( WaschSocke s )
{
s.farbe = "weiß";
}
}

public class ZuOftGewaschen
{
public static void main( String args[] )
{
WaschSocke omisSocke = new WaschSocke();

omisSocke.farbe = "schwarz";
System.out.println( omisSocke.farbe );

Waschmaschine.auswaschen( omisSocke );
System.out.println( omisSocke.farbe );
}
}

Das Beispiel zeigt eine Socke, die ihre Farbe durch Auswaschen verliert. Die Objektreferenz, die an auswaschen() übergeben wird, lässt eine Attributänderung im Socken-Objekt zu. Zeigt die Referenz schwarz auf ein Socken-Objekt, findet die Änderung in der Methode auswaschen() statt, da die Methode das Objekt über eine Kopie der Objektreferenz unter dem Namen s anspricht. In Java wird, anders als zum Beispiel in C++, bei der Parameterübergabe niemals eine Kopie des übergebenen Objekts angelegt, nur die Objektreferenz wird kopiert und per Wert übergeben.

Wir wollen an dieser Stelle noch einmal den Unterschied zu primitiven Typen betonen. Wird ein eingebauter Typ einer Funktion übergeben, so gibt es nur Veränderungen in dieser Methode am Parameter, der ja als lokale Variable behandelt werden kann. Eine Veränderung dieser lokalen Variablen tritt somit nicht nach außen und bleibt lokal.


Galileo Computing

6.1.3 Die this-Referenz  downtop

In jedem Konstruktor und jeder Objektmethode einer Klasse existiert eine Referenz mit dem Namen this, die auf das aktuelle Exemplar der Klasse zeigt. Mit dieser this-Referenz lassen sich elegante Lösungen realisieren, wie folgende Beispiele zeigen:

gp  Die this-Referenz löst das Problem, wenn lokale Variablen Objektvariablen verdecken.
gp  Wenn Methoden this-Referenzen liefern, hat das gute Gründe, denn Methoden können einfach hintereinander gesetzt werden. Es gibt viele Beispiele für Arbeitsweisen in den Java-Bibliotheken, etwa bei der Klasse StringBuffer.
Beispiel Eine Klasse definiert eine Methode inc(), die den internen Wert einer privaten Variablen hochzählt.

Listing 6.3   ThisGoOn.java
public class ThisGoOn
{
private int value;

public int getValue() { return value; }

public ThisGoOn inc()
{
value++;
return this;
}

public static void main( String args[] )
{
System.out.println( new ThisGoOn().inc().getValue() ); // 1

ThisGoOn ref = new ThisGoOn();
ref.inc().inc().inc();

System.out.println( ref.getValue() ); // 3
}
}

Aus diesem Beispiel mit der main()-Methode können wir erkennen, dass new ThisGoOn() eine Referenz liefert, die wir sofort für den Methodenaufruf nutzen. Da inc() wiederum eine Objektreferenz vom Typ ThisGoOn liefert, ist getValue() möglich. Die Verschachtelung von inc().inc() bewirkt, dass immer das interne Attribut erhöht wird und der nächste Methodenaufruf in der Kette eine Referenz auf dasselbe Objekt, aber mit verändertem internem Zustand (= Zählerstand), über this bekommt.

Überdeckte Objektvariablen nutzen

Hat eine lokale Variable den gleichen Namen wie eine Objektvariable, so verdeckt sie diese. Das heißt aber nicht, dass auf die äußere Variable nicht mehr zugegriffen werden kann. Mit der this-Referenz kann auf das aktuelle Objekt zugegriffen werden und entsprechend mit dem Punkt-Operator auf einzelne Variablen des Objekts. Häufiger Einsatzort sind Funktions- oder Konstruktorparameter, die genauso genannt werden wie die Exemplar-Variablen, um damit eine starke Zugehörigkeit auszudrücken.

Listing 6.4   PunktThisDemo.java
class PunktThisDemo
{
int x, y;

void setzePosition( int x, int y )
{
x = 12; // Zuweisung an lokale Variable x
this.x = 12; // Zuweisung an Objektvariable x
this.x = x; // Initialisierung der Objektvariable
this.y = y;
}
}

Der Methode setzePosition() werden zwei Werte übergeben, die anschließend die Objektvariablen initialisieren.

Genau in dem Moment, wo eine lokale Variable deklariert wird und sie eine Objekt- oder Klassenvariable überlagert, wird beim Zugriff auf die lokale Variable verwiesen. Soll die lokale Variable den Wert der Objektvariablen annehmen, was häufig vorkommt, denn ein Zugriff auf eine lokale Variable ist schneller als der Zugriff auf eine Objekt- oder Klassenvariable, lässt sich nicht einfach Folgendes schreiben:

class A
{
int x;
void foo() {
x = 1;
int x = x; // Fehler
}
}

Das Problem in der Zeile ist, dass die lokale Variable x mit ihrem eigenen Wert initialisiert werden soll. Das x auf der rechten Seite bezeichnet nicht die Objektvariable x. Denn die Deklaration mit gleichzeitiger Initialisierung ist in Wirklichkeit nichts anderes als eine Kurzschreibweise für

int x;
x = x;

In dem Moment, in dem x deklariert ist, ist jeder Zugriff auf die lokale Variable bezogen. Bei der rechten Seite von x=x handelt es sich um einen Lesezugriff auf eine nicht-initialisierte Variable


Galileo Computing

6.2 Assoziationen zwischen Objekten  downtop

Eine wichtige Eigenschaft von objektorientierten Systemen ist der Austausch von Nachrichten untereinander. Dazu »kennt« ein Objekt ein anderes Objekt und bittet dieses, etwas zu machen. Dieses Kennen nennt sich Assoziation und ist vielleicht das wichtigste Werkzeug bei der Bildung von Objektverbänden.

Wir können uns etwa vorstellen, dass eine Socke einen Verweis auf einen Hersteller besitzt. Dann sind Socke und Hersteller zwei Objekte, die sich kennen. Gehen wir einfach davon aus, dass die Socke den Hersteller kennt, aber der Hersteller seine Socken nicht. Dann würde das in Java etwa so aussehen:

class Socke
{
String farbe;
int größe;

Hersteller produzent;
}

class
Hersteller
{
// ...
}

In der UML existiert dafür ebenfalls eine grafische Darstellung. Die beiden Klassen sind durch eine Linie verbunden. Da jede Assoziation eine Richtung hat, lässt sich auch ein Pfeil am Ende der Assoziation anbringen, wenn die Assoziation einseitig ist, so wie in unserem Fall. In der Regel tauchen die Namen der Assoziation, wie in der UML-Grafik zu sehen, nicht als Variablennamen auf.

Diese gerichteten Assoziationen sind in Java sehr einfach umzusetzen, wie wir im Beispiel gesehen haben. Beidseitige Assoziationen erfordern schon etwas mehr Programmieraufwand, da sichergestellt sein muss, dass beide Seiten eine gültige Referenz besitzen. Denn wird die Assoziation auf einer Seite aufgekündigt, etwa durch Setzen der Referenz auf null, dann muss auch die andere Seite die Referenz lösen. Am besten wird dies mit Zugriffsmethoden gelöst, etwa wie setzeHersteller(), löscheHersteller() bei Socke und setzeEigentümer() und löscheEigentümer() oder vielleicht setzeProdukt()/löscheProdukt() beim Hersteller. Hinzu kommt , dass der Hersteller sicherlich nicht nur eine Socke produziert hat, sondern mehrere. Daher findet sich auf der Seite des Herstellers eine Datenstruktur, die alle produzierten Socken speichert.

Abbildung 6.2   Eine gerichtete Beziehung
Abbildung


Galileo Computing

6.3 Privatsphäre und Sichtbarkeit  downtop

Innerhalb einer Klasse sind alle Funktionen und Attribute für die Methoden sichtbar. Damit die Daten einer Klasse vor externem Zugriff geschützt sind, und Methoden nicht von außen aufgerufen werden können, verbietet das Schlüsselwort private allen von außen zugreifenden Klassen den Zugriff.

In UML werden private Eigenschaften mit einem führenden Minus gekennzeichnet.

Abbildung 6.3   Ein privates Attribut pass
Abbildung

Beispiel Eine Klasse Password mit dem privaten Attribut pass. Listing 6.5   PassDemo.java, Teil 1
class Password
{
void setPassword( String oldpass, String newpass )
{
if ( oldpass != null && oldpass.equals(pass) )
{
pass = newpass;
System.out.println( "Passwort gesetzt." );
}
else
System.out.println("Passwort konnte nicht gesetzt werden.");
}

private String pass = "";
}

Wir sehen, dass öffentliche Objektmethoden ganz selbstverständlich auf das private-Element zugreifen können.

Eine Klasse PassDemo will nun auf das Passwort von außen zugreifen.

Listing 6.6   PassDemo.java, Teil 2
public class PassDemo
{
public static void main( String args[] )
{
Password pwd = new Password();

pwd.setPassword( "", "TeutoburgerWald" );
pwd.setPassword( "TeutoburgerWald", "Doppelkeks" );
pwd.setPassword( "Dopplerkeks", "panic" );

// System.out.println( pwd.pass ); // Compilerfehler
}
}

Die Klasse Password enthält den privaten String pass und dieser kann nicht referenziert werden. Der Compiler erkennt zur Compile- bzw. Laufzeit Verstöße und meldet diese. So schreibt zum Beispiel der Compiler Jikes von IBM zur Übersetzungszeit:

19.     System.out.println( pwd.pass 
);   // Compilerfehler
<-->
*** Error: The field "pass" in type "Password" is private\
and not accessible here.

Allerdings wäre es manchmal besser, wenn der Compiler uns nicht verraten würde, dass das Element privat ist, sondern einfach nur melden würde, dass es dieses Element nicht gibt.


Galileo Computing

6.3.1 Wieso nicht freie Methoden und Variablen für alle?  downtop

Private Funktionen und Variablen dienen in erster Linie dazu, den Klassen Modularisierungsmöglichkeiten zu geben, die von außen nicht sichtbar sein müssen. Zwecks Strukturierung werden Teilaufgaben in Funktionen gegliedert, die aber von außen nie alleine aufgerufen werden dürfen. Da die Implementierung versteckt wird und der Programmierer vielleicht nur eine Zugriffsfunktion sieht, wird auch der Terminus »Data Hiding« verwendet. Nehmen wir zum Beispiel ein Radio. Von außen bietet es die Funktionen an(), aus(), lauter(), leiser() an, aber wie es ein Radio zum Musikspielen bringt ist eine ganz andere Frage, die wir als gewöhnliche Benutzer eines Radios lieber nicht beantwortet wissen wollen.

Dem unerlaubten Zugriff steht der freie Zugriff auf Funktionen und Variablen entgegen. Eingeleitet wird dieser durch das Schlüsselwort public. Damit ist von außen jederzeit Zugriff möglich. Wird weder public, protected noch private verwendet, so ist ein Zugriff von außen nur innerhalb des Pakets möglich. Damit können Gruppen von Klassen gebildet werden, die gegenseitig Teile ihres Innenlebens kennen. Von außerhalb des Pakets ist der Zugriff auf diese Teile dann untersagt, analog zu private.

Der Einsatz von private zeigt sich besonders in Unterklassen, denn werden in einer Oberklasse Eigenschaften mit private gekennzeichnet, dann ist der Zugriff auf diese Funktionen in der Unterklasse nicht erlaubt. Interessieren diese Informationen auch manche Unterklassen, lassen sich die Methoden und Attribute protected deklarieren. Damit räumt eine Oberklasse den Unterklassen spezielle Privilegien ein. Mit protected sind die Mitglieder einer Klasse für die Unterklassen sichtbar und ebenso im gesamten Paket.


Galileo Computing

6.3.2 Privat ist nicht ganz privat. Es kommt darauf an wer’s sieht  downtop

Wir wollen sehen, dass es in einem Spezialfall für eine Referenz ref doch möglich ist, auf ein privates Attribut oder eine private Methode des referenzierten Objekts zuzugreifen. Dazu muss dieser Zugriff in der Methode einer Klasse stattfinden, in der die private-Eigenschaft oder -Methode selbst definiert ist. Das sehen wir am besten an einem Beispiel.

Listing 6.7   WhoMayUsePrivate.java
public class WhoMayUsePrivate
{
public static void main( String args[] )
{
Compare m = new Compare();
boolean b = m.compare( new Compare() );

System.out.println( b );
}
}

class Compare
{
private int priv = (int)Math.random();

public boolean compare( Compare comp )
{
// Zugriff auf ein privates Element über comp.priv
return priv == comp.priv;
}
}

Interessant ist die Zeile unter dem Kommentar. Die Methode compare() der Klasse Compare vergleicht das eigene Attribut priv des aufrufenden Objekts mit dem Attribut des als Parameter übergebenen Objekts comp. An dieser Stelle sehen wir, dass der Zugriff auf comp.priv zulässig ist, obwohl priv privat ist. Dieser Zugriff ist aber erlaubt, da die compare()-Methode in der Compare-Klasse definiert ist und der Parameter ebenfalls vom Typ Compare ist. Mit Unterklassen funktioniert das schon nicht mehr. Private Attribute und Methoden sind also gegen Angriffe von außerhalb der definierenden Klasse geschützt.

Beispiele aus String und Integer

Auch in den Klassen der Standardbibliothek erkennen wir Beispiele dieser Sichtbarkeitsregel, häufig an der Methode equals(). Oft kapselt eine Klasse ein Attribut privat und bietet dann Zugriffsmethoden an. Wir wollen uns die equals()-Methode von String und die compareTo()-Methode von Integer daher etwas genauer in der Sun-Implementierung anschauen.

class String
{
private char value[];
private int count;
private int offset;

public boolean equals(Object anObject) {
if (this == anObject)
return true;
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
// Hier den zeichenweisen Vergleich
}
}
return false;
}
}

Die String-Klasse speichert eine Referenz auf ein char-Feld, ebenso die Länge und eine Verschiebung (Offset) in privaten Variablen. Ein String-Objekt kann seinen Inhalt mit dem eines anderen Strings vergleichen. Dazu verwenden wir die überschriebene Methode equals(), die mit anObject als Parameter einen String annimmt, mit dem der aktuelle String seinen Vergleich durchführt. Der Vergleich von Zeichenketten ist dabei denkbar einfach: Falls das vergleichende Objekt mit dem zu vergleichenden identisch ist, müssen sie auch inhaltlich gleich sein und der Rückgabewert ist true. Er kann auf keinen Fall true sein, wenn der Typ von anObject nicht auch String ist. Wenn die Typen passen, dann wird Zeichen für Zeichen des Zeichenfelds verglichen. Bei anotherString.count, der typkonformen Variante von anObject, sehen wir, dass zuerst die Längen verglichen werden. Wenn diese schon nicht gleich sind, können auch die Zeichenketten nicht gleich sein. In anotherString.count steckt schon der Zugriff auf das private Element des anderen Strings.

Das zweite Beispiel bezieht sich auf die Klasse Integer mit dem internen Attribut value und der Zugriffsmethode intValue(), die den Wert direkt durchleitet, sowie auf die Vergleichsmethoden compareTo() und equals(). Der Unterschied zwischen den beiden Vergleichsmethoden ist, dass equals() per Definition aus Object die Rückgabewerte true oder false geben muss, während compareTo() entweder -1, 0 oder 1 liefert.

class Integer
{
private int value;

public int intValue() {
return value;
}

public int compareTo(Integer anotherInteger) {
int thisVal = this.value;
int anotherVal = anotherInteger.value;
return (thisVal<anotherVal ? -1 :
(thisVal==anotherVal ? 0 : 1) );
}

public boolean equals(Object obj) {
if (obj instanceof Integer)
return value == ((Integer)obj).intValue();
return false;
}
}

Die Umsetzungen sind jetzt nicht besonders spannend. Was allerdings erstaunt, ist, dass bei compareTo() die Variable anotherInteger.value den privaten Inhalt preisgibt, die Programmierer allerdings bei equals() wundersam intValue() verwendeten. Da sind die Programmierer von Kaffe, Transvirtual Technologies, schon konsequenter. Sie schreiben einfach:

public boolean equals( Object obj 
)
{
return ( obj instanceof Integer) &&
(((Integer)obj).value == this.value );
}

Galileo Computing

6.3.3 Zugriffsmethoden für Attribute definieren  downtop

Bisher sind wir davon ausgegangen, dass Attribute eine tolle Sache sind, und dass es für die Nutzer eines Objekts nur Vorteile hat, wenn dieser über die Attribute auf den Zustand des Objekts zugreifen kann. Leider ist das nicht immer ohne Probleme möglich, wie die nachfolgenden Fälle zeigen:

1. Bei machen Variablen gibt es Wertebereiche, die einzuhalten sind. Das Alter einer Person kann nicht kleiner Null sein und Menschen, die älter als zweihundert Jahre sind, werden nur in der Bibel genannt. Wenn wir das Alter privat machen, kann eine Zugriffsfunktion wie setzeAlter(int) mithilfe einer Bereichsprüfung nur bestimmte Werte in die Variable eintragen und den Rest ablehnen. Die öffentliche Methode holeAlter() gibt dann Zugriff auf die Variable.

Wir sehen an diesen Beispielen, dass es gute Gründe dafür gibt, Attribute zu privatisieren und öffentliche Methoden zum Lesen und Schreiben anzubieten. Da diese Methoden auf die Attribute zugreifen, nennen sie sich auch Zugriffsmethoden. Für jedes Attribut wird eine Schreib- und Lesemethode definiert, für die es auch ein Namensschema gibt. Lesemethoden beginnen mit get-, Schreibmethoden mit set-. Hinter der Vorsilbe wird der Name des Attributs gesetzt. Bei boolean-Attributen darf es statt getXXX() auch isXXX() heißen. Da die Programmentwicklung in der Regel mit englischen Bezeichnernamen erfolgt, kommt es nicht zu unschönen Bezeichnern wie getAlter().

Beispiel Das bisher öffentliche Attribut age soll entfernt und durch Zugriffsmethoden ersetzt werden. Eine Konsistenzprüfung soll verhindern, dass es ein Alter kleiner Null gibt.
class HeyAlter
{
private int age;

public int
getAge() { return age; }

public void
setAge( int age ) {
if ( age >= 0 )
this.age = age;
}
}

An den Methoden wird eine weitere Konvention sichtbar. Die Hole-Methode getXXX() besitzt keinen Parameter und der Typ vom Rückgabewert ist der gleiche wie der von der Variablen alter. Die set-Methode hat keinen Rückgabewert, aber genau einen Parameter vom Typ des Attributs. Letzteres gilt für die anfängliche Programmerstellung. Wird später bei der Weiterentwicklung des Programms eine Änderung nötig, beispielsweise muss das Alter ja nicht ganzzahlige Werte annehmen, kann der Typ der internen Variablen geändert werden, und die Welt draußen bekommt davon nichts mit. Lediglich eine kleine Typanpassung muss in der Implementierung von setAge() und getAge() vorgenommen werden. Sicherlich ist es eine gute Idee, bei ungültigen Werten sich nicht taub zu stellen, sondern eine Fehlermeldung zu produzieren. Das kann etwa in Form einer Ausnahme geschehen.


Galileo Computing

6.3.4 Zusammenfassung zur Sichtbarkeit  downtop

1. Die mit public deklarierten Methoden und Variablen sind überall dort sichtbar, wo auch die Klasse verfügbar ist. Natürlich kann auch eine erweiternde Klasse (Unterklasse) auf alle Elemente zugreifen.

Der Einsatz der Sichtbarkeitsstufen über die Schlüsselworte public, private, protected und paketsichbar sollte überlegt erfolgen und objektorientierte Programmierung zeichnet sich durch durchdachten Einsatz von Klassen und deren Beziehungen aus. Am besten ist die einschränkendste Beschreibung. Also nie mehr Öffentlichkeit als notwendig.


Galileo Computing

6.4 Statische Methoden und Variablen  downtop

Exemplar-Variablen sind eng mit ihrem Objekt verbunden. Wird ein Objekt erschaffen, erhält es einen eigenen Satz von Exemplar-Variablen, die zusammen den Zustand des Objekts repräsentieren. Ändert eine Objektmethode den Wert einer Exemplar-Variablen in einem Objekt, so hat dies keine Auswirkungen auf die Daten der anderen Objekte; jedes Objekt speichert eine individuelle Belegung. Es gibt jedoch auch Situationen, in denen Eigenschaften oder Methoden nicht direkt einem individuellen Objekt zugeordnet werden. Dazu gehören zum Beispiel die Methoden

gp  sin(), etwa in Math.sin(Math.PI/2.0)
gp  max(), etwa in Math.max(1,-2)
gp  Color.HSBtoRGB(), zum Konvertieren von Farben in Farbräumen
gp  Integer.parseInt(), Umwandeln von einem String in den Integer oder Variablen, die sogar konstant sein können
gp  MAX_INTEGER, die größte darstellbare Zahl
gp  PI aus Math bestimmt die Zahl 3,1415 ...

Diese genannten Eigenschaften sind weniger einem konkreten Objekt mit seinem ureigenen Objektzustand zuzuordnen, sondern vielmehr der Klasse. Diese Art von Zugehörigkeit wird in Java durch statische Eigenschaften unterstützt. Da sie nicht zu einem Objekt gehören wie Objekteigenschaften, nennen wir sie auch Klasseneigenschaften. Die Sinus-Funktion ist ein Beispiel für eine statische Methode der Mathe-Klasse und MAX_INTEGER ein statisches Attribut der Klasse Integer.


Galileo Computing

6.4.1 Warum statische Eigenschaften sinnvoll sind  downtop

Statische Eigenschaften haben gegenüber Objekteigenschaften den Vorteil, dass sie im Programm ausdrücken, keinen Zustand vom Objekt zu nutzen. Betrachten wir noch einmal Funktionen aus der Mathe-Klasse. Wenn sie Objektmethoden wären, so würden sie in der Regel mit einem Objektzustand arbeiten. Die Funktionen nähmen dann keinen Parameter, sondern arbeiteten mit dem internen Zustand des aufrufenden Objekts. Das macht aber keine Mathefunktion. Um den Sinus eines Winkels zu berechnen, benötigen wir kein spezifisches Mathe-Objekt. Statische Funktionen sind aus diesem Grunde häufiger als statische Variablen, da sie ihre Arbeitswerte ausschließlich aus den Parametern ziehen. Statische Variablen werden in erster Linie als Konstanten verwendet.


Galileo Computing

6.4.2 Statische Eigenschaften mit static  downtop

Um statische Eigenschaften in Java umzusetzen, fügen wir vor der Definition einer Variablen oder einer Methode das Schlüsselwort static hinzu. Für den Zugriff verwenden wir einfach den Klassennamen, den wir wie eine Referenz verwenden. In der UML können statische Eigenschaften durch Unterstreichen markiert werden.

Abbildung 6.4  
Abbildung

Beispiel Eine statische Funktion und eine statische Variable Listing 6.8   LittleHelpers.java
class LittleHelpers
{
static double PI2 = Math.PI*Math.PI;

static double half( double x, double y )
{
return ( x + y ) / 2.0;
}
}

class StaticUser
{
public static void main( String args[] )
{
System.out.println(
LittleHelpers.half( LittleHelpers.PI2, Math.E )
);
}
}

Wir haben in der main()-Funktion das Attribut und die Eigenschaft mit dem Namen LittleHelpers angesprochen. Auch bei Math.PI sehen wir, dass PI ein statisches Attribut der Mathe-Klasse ist.



Galileo Computing

6.4.3 Statische Eigenschaften als Objekteigenschaften nutzen  downtop

Besitzt eine Klasse eine Klasseneigenschaft, so kann es auch wie ein Objektattribut über die Referenz angesprochen werden. Das bedeutet, dass es zwei Möglichkeiten gibt, wenn ein Objektexemplar existiert und die Klasse ein statisches Attribut hat. Bleiben wir bei unserem obigen Beispiel mit der Klasse LittleHelpers. Jetzt können wir für den Zugriff auf PI2 schreiben:

LittleHelpers l = new LittleHelpers();
System.out.println( l.PI2 );
System.out.println( LittleHelpers.PI2 );

Die unteren beiden Anweisungen sind identisch. Betrachten wir alleine dieses Codesegment, so ist für uns nicht sichtbar, dass PI2 eine statische Eigenschaft ist. Aus diesem Grund sei der Tipp gegeben, statische Eigenschaften über ihren Klassennamen anzusprechen.


Galileo Computing

6.4.4 Statische Eigenschaften und Objekteigenschaften  downtop

Wie wir oben gesehen haben, können wir über eine Objektreferenz auch statische Eigenschaften nutzen. Wir wollen uns aber noch einmal vergewissern, wie Objekteigenschaften und statische Eigenschaften gemischt werden können. Erinnern wir uns daran, dass unsere ersten Programme aus der main()-Methode bestanden, aber unsere anderen Methoden auch static sein mussten. Dies ist sinnvoll, denn eine statische Methode kann, ohne explizite Angabe eines aufrufenden Objekts, nur andere statische Methoden aufrufen. Wie sollte auch eine statische Methode eine Objektmethode aufrufen können, wenn es kein dazugehöriges Objekt gibt? Statische Methoden gibt es immer, wenn es die Klasse gibt. Andersherum kann aber jede Objektmethode eine beliebige statische Methode direkt aufrufen. Genauso verhält es sich mit Attributen. Eine statische Methode kann keine Objektattribute nutzen, da es kein implizites Objekt gibt, auf dessen Eigenschaften zugegriffen werden könnte.

this-Referenzen und statische Eigenschaften

Auch der Einsatz der this-Referenz ist bei statischen Eigenschaften nicht möglich. Das trifft in erster Linie statische Methoden, die eine this-Referenz verwenden wollen.

Hinweis In statischen Methoden gibt es kein this

class InStaticNoThis
{
int a;

static void martina()
{
this.a = 1; // Compilerfehler genauso wie a = 1?
}
}

Galileo Computing

6.4.5 Statische Variablen zum Datenaustausch  downtop

Der Wert einer statischen Variablen wird bei dem Klassenobjekt gespeichert und nicht bei einem Exemplar der Klasse. Wie wir aber gesehen haben, kann jedes Exemplar einer Klasse auch auf die statischen Variablen der Klasse zugreifen. Da eine statische Variable aber nur einmal pro Klasse vorliegt, führt dies dazu, dass mehrere Objekte sich eine Variable teilen. Somit wird ein Austausch von Informationen über die Objektgrenze hinaus erlaubt. Doch kein Vorteil ohne Nachteil. Es kann bei nebenläufigen Zugriffen zu Problemen kommen. Deshalb müssen wir spezielle Synchronisationsmechanismen nutzen.

Abbildung

Beispiel Objekte tauschen Daten über eine gemeinsame statische Variable. Listing 6.9   ShareData.java
class ShareData
{
private
static int share;

public void memorize( int i )
{
share = i;
}

public int retrieve ()
{
return share;
}
public static void main( String args[] )
{
ShareData s1 = new ShareData();
ShareData s2 = new ShareData();

s1.memorize( 2 );
System.out.println( s2.retrieve() ); // ist 2
}
}


Galileo Computing

6.4.6 Warum die Groß- und Kleinschreibung wichtig ist  downtop

Die Vorgabe der Namenskonvention sagt, Klassennamen sind mit Großbuchstaben zu vergeben und Variablennamen mit Kleinbuchstaben. Treffen wir auf eine Anweisung wie Math.max(a, b), so wissen wir sofort, dass max() eine statische Methode sein muss, weil davor ein Bezeichner steht, der großgeschrieben ist. Dieser kennzeichnet also keine Referenz, sondern einen Klassenamen. Daher sollten wir in unseren Programmen großgeschriebene Objektnamen meiden.

Beispiel Warum Referenzen mit Kleinbuchstaben beginnen sollten.
String StringModifier = "What is 
the Matrix?";
String t = StringModifier.trim();

Die trim()-Methode ist nicht statisch, wie die Anweisung durch die Großschreibung suggeriert.

Beispiel Das gleiche Problem haben wir, wenn wir Klassen mit Kleinbuchstaben benennen. Auch das kann irritieren.
class modifier
{
static void move() { ... }
}

Jetzt könnte jemand modifier.move() schreiben, und der Leser würde annehmen, dass modifier eine Referenz ist, da sie kleingeschrieben ist und move() eine Objektmethode. Wir sehen an diesem Beispiel, dass es wichtig ist, die Namensgebung zu verfolgen.


Galileo Computing

6.4.7 Konstanten mit dem Schlüsselwort final bei Variablen  downtop

Statische Variablen werden auch verwendet, um Konstanten zu definieren. Dazu dient zusätzlich das Schlüsselwort final. Damit wird dem Compiler mitgeteilt, dass dieser Variablen nur einmal ein Wert zugewiesen werden darf. Für Variablen bedeutet dies: Es sind Konstanten, jeder spätere Schreibzugriff wäre ein Fehler.

class Sockentyp
{
static
final int PUNKTIERT = 1,
GEFLECKT = 2,
GESTREIFT = 3;
}

In der Klasse Sockentyp werden drei Konstanten definiert. Ein Aufzählungstyp mit enum wie in C++ gibt es in Java nicht. Für Konstanten ist es bedenkenswert, die Konstanten relativ zum Vorgänger zu wählen, um das Einfügen zu vereinfachen. Dann wäre etwa GEFLECKT=PUNKTIERT+1.

Tipp Es ist eine gute Idee, die Namen von Konstanten vollständig großzuschreiben, um deren Bedeutung hervorzuheben.


Galileo Computing

6.4.8 Typsicherere Konstanten  downtop

Konstanten sind eine wertvolle Möglichkeit, den Quellcode aussagekräftiger zu machen. Der herkömmliche Weg geht über Ganzzahl-Konstanten:

public final int  KONSTANTE1 = 
0;
public final int KONSTANTE2 = 1;
public final int KONSTANTE3 = 2;

Dieser Weg bringt den Nachteil mit sich, dass die Konstanten nicht unbedingt von jedem angewendet werden müssen und ein Programmierer eventuell direkt die Zahlen oder Zeichenketten einsetzt. Dieses Problem kommt zum Beispiel auf, wenn ein Font-Objekt für die grafische Oberfläche angelegt werden soll, aber unser Gedächtnis versagt, in welcher Reihenfolge die Parameter einzugeben sind. Ein Fallbeispiel:

Font f = new Font( "Dialog",  12, 
Font.BOLD ):

Leider ist dieses falsch, denn die Parameter für die Zeichensatzgröße und den Schriftstil sind vertauscht. Das Problem ist, dass die Konstanten nur Namen für Werte eines frei zugänglichen Grundtyps sind, und nur der Wert an die Funktion übergeben wird. Niemand kann verbieten, dass direkt die Werte eingetragen werden. Das führt dann zu Fehlern, wie im oberen Fall. In diesem ist 12 die Ganzzahl für den Schriftstil, obwohl es dafür nur die Werte 0, 1, 2 geben sollte. Mit Zeichenketten als Wert der Konstanten kommen wir der Lösung auch nicht näher. Eine gute Möglichkeit von Ganzzahlen wegzukommen ist, Objekte einer Klasse als Konstanten einzusetzen. Folgendes bietet sich an:

Listing 6.10   TypsichererKonstanten.java
public class TypsichereKonstanten
{
static void func( Muster k )
{
if ( k ==
Muster.GEKRINGELT )
System.out.println( "GEKRINGELT" );
if ( k ==
Muster.GESTRICHELT )
System.out.println( "GESTRICHELT" );
}

public static void main( String args[] )
{
func( Muster.GESTRICHELT );
}
}

final class Muster
{
static final Muster
GEKRINGELT = new Muster();

static final
Muster GESTRICHELT = new Muster();

private Muster() // von aussen lassen sich keine weiteren
{ // Objekte erzeugen
id = nextId++;
}

private int id;

private static int nextId = 0; // der Deutlichkeit halber
}

Die Klasse Muster definiert die Konstanten als statische Attribute vom Typ Muster. Da die Objekte für die Konstanten aber nur einmal vorliegen, lassen sie sich einfach mit = =, wie in func() gezeigt, vergleichen.


Galileo Computing

6.4.9 Statische Blöcke  downtop

Eine Art Konstuktor für das Klassenobjekt selbst (nicht die Exemplare der Klasse) ist ein static-Block, der in jede Klasse gesetzt werden kann. Der Block wird genau dann ausgeführt, wenn die Klasse vom Klassenlader in die virtuelle Maschine geladen wird. In der Regel geschieht das nur einmal während eines Programmlaufs. Unter gewissen Umständen kann jedoch eine Klasse auch zwischenzeitlich aus dem Speicher entfernt werden.

Beispiel Zwei statische Blöcke mit einer Hauptfunktion.
class StaticBlock
{
static
{
System.out.println( "Eins" );
}
static
{
System.out.println( "Zwei" );
}

public static void main( String args [])
{
System.out.println( "Jetzt geht's los." );
}
}

Lädt der Klassenlader die Klasse StaticBlock, so führt er zuerst den ersten Block mit der Ausgabe »Eins« aus und dann den Block mit der Ausgabe »Zwei«. Da die Klasse StaticBlock auch das main() besitzt, führt die virtuelle Maschine anschließend die Start-Funktion aus.

Beispiel Mit diesem Trick lassen sich auch Programme ohne main()-Funktion schreiben. In den statischen Block wird einfach das Hauptprogramm geschrieben. Da jedoch die virtuelle Maschine immer noch nach dem main() sucht, müssen wir die Laufzeitumgebung schon vorher beenden. Dies geschieht dadurch, dass mit System.exit() die Bearbeitung abgebrochen wird:
class StaticNowMain
{
static
{
System.out.println( "Jetzt bin ich das Hauptprogramm" );
System.exit( 0 );
}
}

Mit diesem Vorgehen ist jedoch der Nachteil verbunden, das bei Fehlern im versteckten Hauptprogramm die JVM unsinnige Fehler meldet. Etwa das die Klasse StaticNowMain nicht gefunden wurde oder auch eine ExceptionInInitializerError, der statt einer vernünftigen Exception kommt.


Galileo Computing

6.5 Objekte anlegen und zerstören  downtop

Wenn Objekte mit dem new-Operator angelegt werden, reserviert die Speicherverwaltung des Laufzeitsystems auf dem System-Heap Speicher. Wird das Objekt nicht mehr referenziert, so räumt der Garbage-Collector (GC) in bestimmten Abständen auf und gibt den Speicher an das Laufzeitsystem zurück.


Galileo Computing

6.5.1 Konstruktoren schreiben  downtop

Ein Konstruktor wird automatisch aufgerufen, wenn ein Objekt mit dem new-Operator angelegt wird. Mit einem eigenen Konstruktor lässt sich erreichen, dass ein Objekt nach seiner Erschaffung einen sinnvollen Anfangszustand aufweist. Dies kann bei Klassen, die Variablen beinhalten, notwendig sein, die ohne vorherige Zuweisung bzw. Initialisierung keinen Sinn machen würden.

Die Konstruktoren tragen denselben Namen wie die Klasse und sehen wie eine Methode ohne Rückgabewert aus. Da mitunter mehrere Konstruktoren mit unterschiedlichen Parameterlisten vorkommen, ist der Konstruktor oft überladen.

Beispiel Konstruktoren für Socken Listing 6.11   Socke.java
class Socke
{
Socke()
{ farbe = "schwarz";

    groesse = 
40;
  }
  Socke( String farbe  )
{ this.farbe = farbe; groesse = 40; } Socke( String farbe, int groesse )
{ this.farbe = farbe;
this.groesse = groesse;
}
private String farbe;
private int groesse;
}

Damit ergibt sich folgendes UML-Diagramm:

Abbildung 6.5   Konstruktoren im UML-Diagramm
Abbildung

Der Standard-Konstruktor

Wenn wir in unseren Klassen keinen Konstruktor angeben, so legt der Compiler automatisch einen Standard-Konstruktor an. Wenn es jedoch mindestens einen ausprogrammierten Konstruktor gibt, wird dieser Standard-Konstruktor nicht mehr automatisch angelegt. Wollen wir daher ein Objekt einfach mit dem Standard-Konstruktor new Klassenname() erzeugen, so müssen wir einen parameterlosen Standard-Konstruktor per Hand hinzufügen. Dass der Standard-Konstruktor dann nicht angelegt wird, hat seinen guten Grund: Es ließe sich sonst ein Objekt anlegen, ohne dass vielleicht wichtige Variablen initialisiert worden wären.


Galileo Computing

6.5.2 Einen anderen Konstruktor der gleichen Klasse aufrufen  downtop

Mitunter werden zwar verschiedene Konstruktoren angeboten, aber nur in einem Konstruktor verbirgt sich die tatsächliche Initialisierung des Objekts. Ein Konstruktor möchte daher einen anderen Konstruktor derselben Klasse – nicht den der Oberklasse – aufrufen, um nicht gleichen Programmcode mehrfach ausprogrammieren zu müssen. Dazu dient wieder das Schlüsselwort this.

class Socke
{
String farbe;

Socke( String farbe )
{
this.farbe = farbe; // this ist hier die Referenz
}

Socke()
{
this( "schwarz" ); // this() leitet an anderen
// Konstruktor weiter
}
}

Die Klasse Socke besitzt zwei Konstruktoren, den Standard-Konstruktor und einen Ein-Parameter-Konstruktor. Wird ein neues Objekt mit new Socke() aufgebaut, wird der Standard-Konstruktor aufgerufen und ihm anschließend die Farbe Schwarz im parametrisierten Konstruktor übergeben – Standardsocken sind einfach schwarz.

Natürlich stellt sich die Frage, warum wir denn einen zweiten Aufruf starten. Viel einfacher wäre doch Folgendes für den Standard-Konstruktor:

Socke()
{
farbe = "schwarz";
}

Das ist in der Tat weniger zu schreiben und auch schneller augeführt, doch diese Implementierung hat einen großen Nachteil. Nehmen wir an, wir hätten zehn Konstruktoren für alle erdenklichen Fälle in genau diesem Stil implementiert. Tritt der unerwünschte Fall ein, dass wir auf einmal in jedem Konstruktor etwas initialisieren müssen, so muss der Programmcode, etwa ein Aufruf der Methode init(), in jedem der Konstruktoren eingefügt werden. Dieses Problem umgehen wir einfach, in dem wir die Arbeit auf einen speziellen Konstruktor verschieben. Ändert sich nun das Programm in der Weise, dass überall beim Initialisieren zusätzlicher Programmcode ausgeführt werden muss, ändern wir eine Zeile in dem konkreten von allen benutzen Konstruktor. Damit fällt für uns wenig Änderungsarbeit an. Unter softwaretechnischen Gesichtspunkten ein großer Vorteil. Überall in den Java-Bibliotheken lässt sich diese Technik wieder erkennen. Ein schönes einfaches Beispiel ist etwa die Point-Klasse. Ein Ausschnitt:

public class Point extends Point2D 
implements
java.io.Serializable
{
public int x, y;

public Point() {
this(0, 0);
}

public Point(Point p) {
this(p.x, p.y);
}

public Point(int x, int y) {
this.x = x;
this.y = y;
}
}

Einschränkungen

Beim Aufruf eines anderen Konstruktors mittels this() gibt es zwei wichtige Beschränkungen:

1. Der Aufruf von this() muss in der ersten Zeile stehen.
class Socke
{
final int ringelAnzahl = 4;
static
final int RINGEL_ANZAHL = 4;
Socke( String g, int anzRing ) { ... }
Socke( String f )
{
// this( f, ringelAnzahl ); // nicht erlaubt
this( f, RINGEL_ANZAHL ); // das geht statt dessen
}
}

Da Objektvariablen bis zu einem bestimmten Punkt noch nicht initialisiert sind, lässt uns der Compiler nicht darauf zugreifen – nur statische Variablen sind als Übergabeparameter erlaubt.


Galileo Computing

6.5.3 Initialisierung der Objekt- und Klassenvariablen  downtop

Eine wichtige Eigenschaft von Programmiersprachen ist ihre Fähigkeit, keine uninitialisierten Zustände zu erzeugen. Bei lokalen Variablen achtet der Compiler auf die Belegung, ob vor dem ersten Lesezugriff schon ein Wert zugewiesen ist. Bei Objektvariablen und Klassenvariablen haben wir bisher festgestellt, dass automatisch die Variablen mit 0 oder mit einem Wert belegt werden. Wir wollen jetzt sehen, wie diess genau funktioniert.

Objektvariablen

Wenn der Compiler eine Klasse mit Objekt- oder Klassenvariablen sieht, dann müssen diese Variablen an irgendeiner Stelle initialisiert werden. Werden sie einfach definiert, und nicht mit einem Wert initialisiert, so regelt die virtuelle Maschine die Vorbelegung. Spannender ist der Fall, wenn den Variablen explizit ein Wert zugewiesen wird (der auch 0 sein kann). Dann erzeugt der Compiler automatisch ein paar zusätzliche Zeilen.

Beachten wir dies zuerst für Objektvariablen.

Listing 6.12   InitObjectVariable.java
class InitObjectVariable
{
int j = 1;

InitObjectVariable()
{
}
InitObjectVariable( int j )
{
this.j = j;
}

InitObjectVariable( int x, int y )
{
}
}

Die Variable j wird mit 1 belegt. Es ist wichtig, zu wissen, an welcher Stelle Variablen ihre Werte bekommen. So erstaunlich das klingt, aber die Zuweisung findet im Konstruktor statt. Das heißt, der Compiler wandelt das Programm bei der Übersetzung eigenmächtig wie folgt um:

class InitObjectVariable
{
int j;

InitObjectVariable()
{
j = 1;
}

InitObjectVariable( int j )
{
this.j = 1;
this.j = j;
}

InitObjectVariable( int x, int y )
{
j = 1;
}
}

Wir erkennen, dass die Variable wirklich nur dann initialisiert wird, wenn auch ein Konstruktor aufgerufen wird. Die Zuweisung steht dabei in der ersten Zeile. Dies kann zur Falle werden, denn problematisch ist etwa die Reihenfolge der Belegung.

Manuelle Nullung

Genau genommen initialisiert die Laufzeitumgebung die Variable erst einmal mit 0 und dann später mit einem Wert. Daher ist die Initialisierung auch ein bisschen langsamer, wenn die Nullung von Hand zusätzlich eingebaut wird, also etwa so:

class InitNullUnnötig
{
int i = 0;
}

Denn der Wert ist automatisch mit 0 belegt und der Compiler würde in jeden Konstruktor die Zuweisung i=0 einsetzen.

Klassenvariablen

Abschließend bleibt die Frage, wo Klassenvariablen initialisiert werden. Im Konstruktor macht dies keinen Sinn, da für Klassenvariablen keine Objekte angelegt werden müssen. Dafür gibt es den static{}-Block. Dieser wird immer dann ausgeführt, wenn der Klassenlader eine Klasse in die Laufzeitumgebung geladen hat. Für eine statische Variable i=2 ergibt sich dann folgendes Bild, das der Compiler wieder selbstständig bei der Übersetzung einfügt:

class StaticInit
{
static int i;

static
{
i = 2;
}
}

Galileo Computing

6.5.4 Finale Werte im Konstruktor setzen  downtop

Bisher sind wir davon ausgegangen, dass finale Werte immer dann gesetzt werden müssen, wenn die Variable deklariert wird. Diese Ansicht können wir etwas erweitern. Bedenken wir, dass alle statischen Variablen in static-Blöcken und alle Objektvariablen in Konstruktoren initialisiert werden, so gilt dies ebenso für finale Werte. Auch sie werden im Konstruktor gesetzt. Wichtig ist, dass finale Werte auf jeden Fall gesetzt werden und das nur ein Schreibzugriff möglich ist. Mit diesem Vorgehen lassen sich auch »variable« Konstanten angeben:

class VariableConstant
{
final static int MWST; // hier steht nicht = irgendwas
final String ISBN; // hier auch nicht.

static
{
if ( 2 > 1 )
MWST = 7;
else
MWST = 16;
}

VariableConstant()
{
ISBN = "3572100100";
}

public static void main( String args[] )
{
System.out.println( MWST ); // 7

System.out.println( new VariableConstant().ISBN );
// 3572100100
}
}

Der Nachteil dieser Variante ist natürlich, dass die Lesbarkeit leidet. Der Leser muss sich mitunter erst durch viele Zeilen Quellcode kämpfen, bis er weiß, wie der Wert belegt ist. Daher sollte diese Programmiertechnik nur zum Einsatz kommen, wenn der Wert nicht direkt berechnet werden kann.


Galileo Computing

6.5.5 Exemplarinitialisierer (Instanzinitialisiererdowntop

Neben den Konstruktoren haben die Sprachschöpfer eine weitere Möglichkeit vorgesehen, um Objekte zu initialisieren. Diese Möglichkeit wird insbesondere bei anonymen, inneren Klassen wichtig, also Klassen, die sich in einer anderen Klasse befinden.

Ein Exemplarinitialisierer ist ein Konstruktor ohne Namen. Er besteht in einer Klassendefinition nur aus einem Paar geschweifter Klammern und gleicht einem statischen Initialisierungsblock ohne das Schlüsselwort static:

class Klasse
{
{
// Exemplarinitialisierer.
}
}

In einer Klasse können mehrere Exemplarinitialisierer auftauchen. Die Exemplarinitialisierer werden der Reihe nach durchlaufen, und zwar vor dem eigentlichen Konstruktor. Damit gilt, dass Objektvariablen noch nicht zwingend initialisiert sind, da einige erst durch den Konstruktor belegt werden.


Galileo Computing

6.5.6 Zerstörung eines Objekts durch den Müllaufsammler  downtop

Glücklicherweise werden wir beim Programmieren von der lästigen Aufgabe befreit, Speicher von Objekten freizugeben. Wird ein Objekt nicht mehr referenziert, dann wird der GC aufgerufen, und dieser kümmert sich um alles Weitere – der Entwicklungsprozess wird dadurch natürlich vereinfacht. Der Einsatz eines GCs verhindert zwei große Probleme:

gp  Ein Objekt kann gelöscht werden, aber die Referenz existiert noch (engl. dangling pointer).
gp  Kein Zeiger verweist auf ein bestimmtes Objekt, dieses existiert aber noch im Speicher (engl. memory leaks).

Dem GC wird es leicht gemacht, wenn nicht mehr benötigte Referenzen sofort mit null überschrieben werden (objRef = null), denn dann weiß der GC, dass zumindest ein Verweis weniger auf das Objekt existiert. War es der letzte Verweis, kann der GC dieses Objekt sofort entfernen, wenn weiterer Speicherplatz für neue Objekte benötigt wird.

Hinweis Einen Destruktor, so wie in C++, gibt es in Java nicht. Wohl können wir eine Funktion finalize() ausprogrammieren, in der Aufräumarbeiten erledigt werden. Die Methode erbt jede Klasse von Object. Im Gegensatz zu C++ mit einer manuellen Freigabe, ist in Java keine Aussage über den Zeitpunkt möglich, zu dem die Routine aufgerufen wird – dies ist von der Implementierung des GCs abhängig. Es kann sein, dass finalize() überhaupt nicht aufgerufen wird, und zwar dann, wenn die VM Fantastillionen Megabyte Speicher hat und dann beendet wird. Insbesondere ist finalize() ungeeignet, um Ressourcen freizugeben, etwa File-Handles oder Grafik-Kontexte des Betriebssystems. finalize() wird nur aufgerufen, wenn Speicher knapp wird und tote Objekte freigegeben werden müssen. Gehen z. B. nur die File-Handles aus, wird der GC nicht aktiv; es erfolgen keine finalize()-Aufrufe und tote Objekte belegen weiter die knappen File-Handles.

Prinzipielle Arbeitsweise

Der GC erscheint hier als ominöses Ding, welches clever die Objekte verwaltet. Doch was ist der GC? Implementiert ist er als Thread (unabhängiger Prozess) mit niedriger Priorität. Er verwaltet die Wurzelobjekte, von denen aus das gesamte Geflecht der lebendigen Objekte erreicht werden kann. Dazu gehören die Wurzel des ThreadGroup-Baums und die lokalen Variablen aller aktiven Methodenaufrufe (Laufzeitkeller aller Threads). In regelmäßigen Abständen werden nicht benötigte Objekte markiert und entfernt. Effiziente GCs sind Teil der Forschung. Sun verwendet einen sehr einfachen Algorithmus, der unter dem Namen »Mark and Sweep« bekannt ist. Das Markieren der nicht mehr verwendeten Objekte nimmt dabei die meiste Zeit in Anspruch. In der Implementierung des GCs unterscheiden sich die Java-Interpreter der verschiedenen Anbieter. So verwendet die VM von Microsoft eine effizientere Strategie zum Erkennen und Entfernen der Objekte. Sie verwendet einen modifizierten »Stop and Copy«-Algorithmus, der schneller ist, als die gewöhnlichen GC-Strategien. Somit wirbt Microsoft nicht ohne Grund damit, dass ihre VM einen Geschwindigkeitsvorteil von einem Faktor 2-4 gegenüber der Sun-Implementierung besitzt (natürlich nicht immer 100 %-ig kompatibel). Insbesondere ist das Anlegen von Objekten bei Microsofts VM flott. Mittlerweile ist auch das Anlegen von Objekten unter der Java VM von Sun dank der Hot-Spot-Technologie schneller geworden. Hot-Spot ist seit Java 1.3 fester Bestandteil des Java SDK. Hot-Spot verwendet einen generationen-orientierten GC, der ausnutzt, dass zwei Gruppen von Objekten mit deutlich unterschiedener Lebensdauer existieren. Die meisten Objekte sterben sehr jung, die wenigen überlebenden Objekte werden hingegen sehr alt. Die Strategie dabei ist, dass Objekte im »Kindergarten« erzeugt werden, der sehr oft nach toten Objekten durchsucht wird und in der Größe auf etwa 64k beschränkt ist. Überlebende Objekte kommen nach einiger Zeit aus dem Kindergarten in eine andere Generation, die nur selten vom GC durchsucht wird.


Galileo Computing

6.5.7 Implizit erzeugte Stringobjekte  downtop

In den bisherigen Beispielen haben wir gesehen, dass ein Objekt mit dem new-Operator gebildet wird. Es gibt aber noch eine versteckte Objekterzeugung bei Zeichenketten. Betrachten wir folgende Zeilen:

Date d = new Date();
String s = "Chicken Run";
String t = "Chicken Run";

Beim Datum erzeugten wir ausdrücklich ein neues Date-Objekt. Die zweite Zeile erzeugt jedoch implizit ein Stringobjekt, das das angegebene Zeichenketten-Litereal speichert. In der dritten Zeile gilt nun etwas Besonderes. Um dies zu erkennen, müssen wir wissen, dass Zeichenketten-Literale in einem Konstantenpool der virtuellen Maschine gehalten werden. Gleiche Zeichenketten bei Stringobjekten für Literale (und nur dort) werden daher auf die gleichen Referenzen gelenkt. Genau in diesem Fall lassen sich mit dem Vergleichsoperator == die Zeichenketten vergleichen. In der dritten Zeile wird demnach also kein neues Stringobjekt erzeugt, sondern die Referenz t ist mit der von s identisch.

Der letzte Fall einer impliziten Objekterzeugung hat wieder mit Zeichenketten zu tun: Der Plus-Operator zur Konkatenation von nicht konstanten Zeichenketten (konstante Zeichenketten werden vom Compiler zusammengefügt) erzeugt einen StringBuffer, dessen Bausteine mit append() angehängt werden. Nach der Aneinanderreihung wird der StringBuffer wieder zu einem String konvertiert:

String s = "Peter Lord " + '&' + " Nick Park"; //


String s = new StringBuffer("Peter 
Lord ").append('&').

Galileo Computing

6.5.8 Zusammenfassung: Konstruktoren und Methoden  downtop

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

Tabelle 6.1   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.


Galileo Computing

6.6 Vererbung  downtop

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.


Galileo Computing

6.6.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 bzw. 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
{
}

Galileo Computing

6.6.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. Wie müsste etwa eine Unterklasse auf die Eigenschaften eine Oberklasse zugreifen, wenn beide Oberklassen das gleiche Attribut definieren? Dazu gesellt sich auch das Diamanten- oder Rauten-Problem. Zwei Klassen K1 und K2 erben von einer Oberklasse eine Eigenschaft. Eine Unterklasse könnte von den Klassen K1 und K2 erben. Was ist mit der Eigenschaft? Da sie eigentlich nur einmal vorliegt, dürfte es kein Grund zur Sorge geben. Dennoch stellt dieses Szenario ein Problem dar, welches durch Einfachvererbung nicht entsteht. Da die Mehrfachvererbung in Java nicht gültig ist, steht letztendlich hinter dem Schlüsselwort extends lediglich eine einzige Klasse.


Galileo Computing

6.6.3 Kleidungsstücke modelliert  downtop

Wir wollen nun eine Klassenhierarchie für Kleidungsstücke aufbauen. Die Hierarchie geht von oben nach unten, von der Superklasse zur Subklasse. Die Klasse Kleidung erbt automatisch alles von Object, Socke erbt alle Eigenschaften von Kleidung. Damit die Socke nicht so alleine ist, geben wir ihr eine Hose als Partner hinzu. Damit ergibt sich das nachfolgende UML-Diagramm. Vererbung ist durch einen Pfeil in Richtung der Oberklasse angegeben:

Abbildung 6.6   Socke und Hose sind Unterklassen von Kleidung
Abbildung

Listing 6.13   SchrankVoll.java, Teil 1
class Kleidung
{
public String farbe;
}

Eine kleine Klasse Kleidung deklariert nur das Attribute farbe. Wird die Unterklasse Kleidung nun zu Socke oder Hose erweitert, so kann jedes Objekt der Unterklasse problemlos auf die Variable zugreifen. Dies haben wir in dem folgenden Beispiel jedoch nicht genutzt.

Listing 6.14   SchrankVoll.java, Teil 2
class Socke extends Kleidung
{
String kennung()
{
return "Ich bin eine Socke";
}
}

class Hose extends Kleidung
{
String kennung()
{
return "Ich bin eine Hose";
}
}

Zu guter Letzt folgt eine Probeklasse, die eine Socke und eine Hose erzeugt und dann die Farbe setzt.

Listing 6.15   SchrankVoll.java, Teil 3
public class SchrankVoll
{
public static void main( String args[] )
{
Socke s = new Socke();
s.farbe = "rot";

Hose h = new Hose();
h.farbe = "grün";

System.out.println( s.farbe );
System.out.println( h.farbe );
}
}

Wir sehen an diesem Beispiel, dass der Nutzer die Farben setzen kann, da eine Hose und eine Socke das Farbattribut erben.


Galileo Computing

6.6.4 Sichtbarkeit  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.


Galileo Computing

6.6.5 Das Substitutionsprinzip  downtop

Abbildung

Stellen wir uns vor, Bekannte kommen ausgehungert von einer Wandertour, und fragen: »Hasse 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 angebotenen werden. Genauer gesagt, wenn eine Unterkasse 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. Der, dem wir mehr übergeben, kann damit zwar nichts anfangen, aber ablehnen wird er das Objekt nicht, da es alle geforderten Eigenschaften aufweist.

Da an Stelle eines Objekts auch ein Objekt der Unterklasse auftauchen kann, sprechen wir vom Substitution. Das Prinzip wurde von Professorin Barbara Liskov formuliert und nennt sich daher auch Liskov’sches Substitutionsprinzip.

Bleiben wir bei unserem Beispiel des Parameters. In der Java-Bibliothek finden sich endlose Beispiele. Häufigstes Anwendungsfeld sind Datenstrukturen. Sie nehmen beliebige Objekte entgegen, denn der Parametertyp ist Object. Die Substitution besagt, dass wir alle Klassen einsetzen können, da sie von Object abgeleitet sind. Für unsere Vererbungsbeziehung heißt das, überall dort, wo Kleidung gefordert ist, können wir eine Socke oder auch eine Hose übergeben.


Galileo Computing

6.6.6 Automatische und Explizite Typanpassung  downtop

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

Socke omi = new Socke();
Kleidung k = omi;

Da eine Socke ein spezielles Kleidungsobjekt ist (Socke ist Unterklasse von Kleidung), funktioniert diese Zuweisung. Auf den ersten Blick erscheint das nicht sonderlich sinnvoll, erfüllt aber einen Zweck: k übernimmt alle Eigenschaften eines Kleidungsobjekts von der mächtigeren Klasse Socke, verzichtet aber auf alle anderen Informationen die eine Socke oder sonstige Unterklasse noch bietet, beispielsweise die Methode kennung().

Die Klasse Kleidung bietet dabei das Attribut farbe an, sodass auch Folgendes problemlos ist:

System.out.println( k.farbe );

Versuchen wir aber eine spezielle Eigenschaft von Socke zu benutzen, etwa die Methode kennung(), so ist dies nicht möglich:

k.kennung();              // geht 
nicht

Hier ist der Typ der Variable k entscheidend. Der Compiler hat k vom Typ Kleidung kennengelernt, daher weiß er nicht, dass k eigentlich ein verkapptes Sockenobjekt ist.

Genauso gut lässt sich keine neue Referenz vom Typ Socke auf die Kleidung legen. Hier gilt wiederum, dass die Typen unvereinbar sind, sodass wir einen Compilerfehler bekommen:

Socke opi = k;            // geht 
nicht

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

Socke opi = (Socke) k;    // geht 
wohl

Wir werden in den folgenden beiden Abschnitten nun kennen lernen, wieso das Sinn macht und ein mächtiges Konzept ist. Wir werden sehen, dass eine Basisklasse geschaffen werden kann und diese verschiedenen Unterklassen Grundfunktionalität beibringen kann. So liefert die Basisklasse einen gemeinsamen Nenner.

Fassen wir die oberen Zeilen noch einmal in einem kompletten Programm, aber mit anderen Klassennamen zusammen:

Listing 6.16   WasIstAllesKleidung.java
class WKleidung
{
public String farbe;
}

class WSocke extends WKleidung
{
String kennung()
{
return "Ich bin eine Socke";
}
}

public class WasIstAllesKleidung
{
public static void main( String args[] )
{
WSocke omi = new WSocke();
omi.farbe = "rot";

WKleidung k = omi;
System.out.println( k.farbe ); // ist rot
// Socke opi = k; // geht nicht
WSocke opi = (WSocke) k; // geht wohl
}
}

Galileo Computing

6.6.7 Finale Klassen  downtop

Soll eine Klasse keine Unterklassen bilden, so 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. Wenn die Klasse String nicht final wäre, wäre nicht sichergestellt, dass Strings unveränderlich bleiben.


Galileo Computing

6.6.8 Unterklassen prüfen mit dem Operator instanceof  downtop

In Java haben die Entwickler einen Operator in den Sprachwortschatz aufgenommen, mit dem Exemplare auf ihre Verwandtschaft mit einer Klasse geprüft werden können. Mit dem Operator instanceof kann zur Laufzeit festgestellt werden, ob ein definiertes Objekt ein Exemplar einer Unterklasse einer anderen Klasse ist. Dies ist sinnvoll, denn durch objektorientiertes Programmieren werden laufend Basisobjekte definiert und erweitert. Ein weiterer Grund zur Einführung des Operators war auch, dass es keine generischen Typen in Java gibt und Daten, die aus einer Datenstruktur kommen, automatisch vom Typ der Basisklasse sind.

boolean b;

String str = "Toll";

b = ( str
instanceof String ); // wahr
b = ( str
instanceof Object ); // wahr
b = ( str
instanceof Date ); // nö

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. Im dritten Fall ist Date keine Basisklasse für String, der Ausdruck ist falsch.


Galileo Computing

6.7 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 UML ergibt sich folgendes Bild:

Abbildung

Listing 6.17   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.7.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 auf 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 dynamisches 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 die Sprechweise sich so eingebürgert hat. Es handelt sich für die Unterklasse um ein Objekt, welches alle Eigenschaften der Oberklasse übernimmt. Das 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.18   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 Hashwert und den Namen der Klasse sehen. Beide zeigen auf das gleiche Objekt.


Galileo Computing

6.7.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, in dem 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.3 Fehlende kovariante Rückgabewerte  downtop

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 nicht-primitver 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 dem 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 einziehen. Auch einige Java-Erweiterungen, unter ihnen der freie GJ-Compiler, erlauben kovariante Rückgabewerte ohne Änderung der JVM durch Einfügen von Typanpassungen.


Galileo Computing

6.8 Die Oberste aller Klassen: Object  downtop

Wie schon an anderer Stelle betont, ist Object – definiert in der Klassendatei java.lang.Object – die Oberste aller Klassen. Somit spielt diese Klasse eine ganz besondere Rolle, da alle anderen Klasse automatisch Unterklassen sind und die Methoden erben beziehungsweise überschreiben.

Abbildung


Galileo Computing

6.8.1 Klassenobjekte  downtop

Zwar ist jedes Objekt ein Exemplar einer Klasse – doch was ist eine Klasse? In Sprachen wie C++ existieren keine Klassen zur Laufzeit und der Compiler übersetzt die Klassenstruktur in ein Programm. Im absoluten Gegensatz dazu steht Smalltalk: Diese Laufzeitumgebung verwaltet alle Klassen als Objekte. Die speziellen Klassen-Objekte bilden dann den Bauplan für neue Exemplare.

Die Klasse Object bietet eine spezielle Methode getClass() an, die eine Referenz auf das Klassenobjekt vom Typ java.lang.Class zurückgibt, die das Objekt konstruiert hat. Der Vorteil liegt klar auf der Hand: Wenn Klassen als Objekte vorliegen, dann können sie auch über das Netz geladen und ausgeführt werden. Ein Nachteil ist die verminderte Geschwindigkeit. Doch das wird gerne in Kauf genommen.

Beispiel Die Objektmethode der Klasse Class.getName() fragt nach dem Namen der Objektklasse.

Die folgende Zeile ergibt die Ausgabe java.lang.String:

System.out.println( "Klaviklack".getClass().getName() );
 

Galileo Computing

6.8.2 Hashcodes  downtop

Die Methode hashCode() soll zu jedem Objekt eine möglichst eindeutige Integerzahl (sowohl positiv als auch negativ) liefern, die das Objekt identifiziert. Inhaltlich gleiche Objekte (gemäß der Methode equals()) müssen denselben Wert bekommen. Eine spezielle Funktion berechnet diesen Wert, der Hashcode oder Hashwert genannt wird. Die Funktionen, die solche Werte berechnen, nennen sich Hashfunktionen.

Hashcodes werden verwendet, um Elemente in Hashtabellen zu speichern. Diese sind Datenstrukturen, die einen effizienten Zugriff auf ihre Elemente erlauben. Die Klasse java.util.HashMap oder java.util.Hashtable bieten eine solche Datenstruktur an.


Galileo Computing

6.8.3 Objektidentifikation mit toString()  downtop

Jedes Objekt sollte sich durch die Methode toString() mit einer Zeichenkette identifizieren und den Inhalt der interessanten Attribute als Zeichenkette liefern.

Beispiel Die Klasse Point definiert toString() so, dass die Koordinaten angegeben werden.

Neue Klassen sollten diese Methode überschreiben. Wenn das nicht der Fall ist, gelangt das Programm zur Standardimplementierung in Object:

public String toString()
{
return getClass().getName()+"@"+
Integer.toHexString(hashCode());
}

Dann wird lediglich der Klassenname und der nichts sagende Hashwert hexadezimal ausgeben.

Das Angenehme ist, dass toString() automatisch aufgerufen wird, wenn die Methode print() oder println() mit einer Objektreferenz als Parameter aufgerufen werden. Ähnliches gilt für den Zeichenketten-Operator + mit einer Objektreferenz als Operanden:

MeinObjekt meinObi = new MeinObjekt();
System.out.println( meinObi );
String s = "Alles bei " + meinObi;

Bei einer eigenen Implementierung müssen wir darauf achten, dass die Sichtbarkeit public ist, da toString() in der Oberklasse public ist und wir die Sichtbarkeit nicht einschränken können.


Galileo Computing

6.8.4 Objektgleichheit mit equals() und Identität  downtop

Ob zwei Referenzen auf das gleiche Objekt zeigen, lässt sich durch den Vergleichsoperator == feststellen. Damit wird aber lediglich die Identität, nicht jedoch automatisch die inhaltliche Gleichheit getestet. So wird der Vergleich

if ( meinName == "Ulli" )

gewiss einen falschen, unbeabsichtigten Effekt haben, obwohl er syntaktisch korrekt ist. An dieser Stelle sollte der inhaltliche Vergleich stattfinden: Stimmen alle Zeichen der Zeichenkette überein?

Mit der Methode equals() aus Object lassen sich Objekte auf Gleichheit prüfen. Unterklassen überschreiben diese Methode, um einen inhaltlichen Vergleich vorzunehmen. Die Methode ist in jeder Klasse auch gut aufgehoben, denn nur ein Objekt weiß, wann es mit einem anderen gleich ist. So besitzt das String-Objekt eine Implementierung, die jedes Zeichen vergleicht:

String meinName = "Ulli";

if ( meinName.equals( "Ulli" ) )
// gefunden

Leider implementiert nicht jede Klasse eine eigene equals()-Methode, sodass die Laufzeitumgebung unbeabsichtigt bei Object und seinem Referenzenvergleich landet. Diese Fehleinschätzung passiert leider bei Exemplaren der Klasse StringBuffer, die kein eigenes equals() implementiert. Wir haben die Diskussion darüber schon in geführt.

Ein weiterer Aspekt von equals() ist Folgender: Das Ergebnis muss über die gesamte Lebensdauer eines Objekts immer gleich bleiben. Ein kleines Problem steckt dabei in equals() der Klasse URL, die vergleicht, ob zwei URL-Adressen auf die gleiche Ressource zeigen. Der Vergleich vertraut auf die IP-Adresse des Rechners, was jedoch sehr problematisch ist. Gerade die Abstraktion von veränderbaren IP-Adressen mit ihren dynamischen Auflösungen machen URLs attraktiv. Eine IP-Adresse eines Rechners kann sich ändern, auch wenn sich die URL nicht ändern. Das heißt, ändert sich die IP-Adresse des Rechners, ist auf einmal ein URL-Objekt nicht mehr mit sich selbst gleich, obwohl die URL unverändert bleibt. Den Fehler sollten wir bei unseren Programmen nicht machen. Wenn der Test auf Gleichheit nicht möglich ist, müssen wir einen Test definieren, der möglich ist (etwa Vergleich der URL-Daten wie Rechnername und Datei), oder auf den Test verzichten und beim Vergleich der Referenzen bleiben.

Equals, die Null und Hashen

Heißt der Vergleich equals(null), so ist das Ergebnis immer false. Die beiden Methoden hashCode() und equals() hängen zusammen, sodass in der Regel bei der Implementierung einer Funktion auch eine Implementierung der anderen Funktion notwendig wird. Denn es gilt, dass bei Gleichheit natürlich auch die Hash-Werte übereinstimmen müssen. Formal gesehen heißt das:

x.equals(y)      ==>    x.hashCode() 
== y.hashCode()

So berechnet sich der Hashcode bei Point-Objekten aus den Koordinaten. Zwei Punktobjekte, die inhaltlich gleich sind, haben die gleichen Koordinaten und damit auch den gleichen Hashcode.

Ein eigenes equals()

Bei selbstdefinierten Methoden ist Vorsicht geboten, denn wir müssen genau auf die Signatur achten. Die Methode muss ein Object akzeptieren und boolean zurückgeben. Wird diese Signatur falsch verwendet, so kommt es anstelle einer Überschreibung der Funktion zu einer Überladung. Dies hat ungeahnte Folgen, denn dann wird einfach die Standardimplementierung aufgerufen. Diese kann über die Gleichheit von Objekten nichts wissen und testet lediglich die Referenzen.

public boolean equals( Object obj 
)
{
return (this == obj);
}
Beispiel Eine korrekte Implementierung der Methode equals() für eine Klasse X.

Das Beispiel ist allgemein gehalten und zeigt, was für fast alle equals()-Methoden gilt. Wir wollen zudem unterscheiden, ob X eine direkte Unterklasse von Object ist oder Unterklasse einer weiteren Klasse O ist.

class X // extends O
{
public boolean equals( Object o )
{
if ( o == null )
return false;
if ( o == this )
return true;

// Fall 1:
// X ist direkte Unterklasse von Object
if ( getClass() != o.getClass() )
return false;
// testet Gleichheit der Klassen einmal direkt unterhalb der
// Klasse Object.

// Fall 2:
// X ist Unterklasse von O
if ( !super.equals(o) )
return false;
// Attribute der Oberklasse (und Klassenzugehörigkeit)
// verglichen.

return equals( (X)o );
}
public boolean equals( X o ) // Sichtbarkeit nach Wunsch.
{
// Vergleich, ob alle Attribute vom eigenen
// Objekt, mit denen von o identisch sind.
// Dann return true, sonst return false.
}

Es ist günstig bei erweiterten Klassen ein neues equals() anzugeben, sodass auch die neuen Attribute in den Test einbezogen werden. Bei hashCode()-Methoden müssen wir eine ähnliche Strategie anwenden, was wir hier nicht zeigen wollen.


Galileo Computing

6.8.5 Klonen eines Objekts mit clone()  downtop

Ein Objekt kann sich selbst klonen. Nach dem Aufrufen der clone()-Methode erzeugt das Laufzeitsystem eine neues Exemplar der Klasse und kopiert elementweise die Daten des aktuellen Objekts in das neue. Jede Klasse bestimmt jedoch eigenständig, welche Attribute kopiert werden. Die Methode gibt eine Referenz auf das neue Objekt zurück. Zwei Fehler sind denkbar: Es gibt keinen freien Speicher mehr (OutOfMemoryError) oder das Objekt verbietet das Klonen (CloneNotSupportedException).

clone() erzeugt flache Kopien, das heißt, bei untergeordneten Objekten werden nur die Referenzen kopiert, und Originalobjekt sowie Kopie verweisen anschließend auf dieselben untergeordneten Objekte (verwenden diese gemeinsam).


Galileo Computing

6.8.6 Aufräumen mit finalize()  downtop

Eine spezielle Methode finalize() wird immer dann aufgerufen, wenn der GC ein Objekt entfernen möchte. Objekte sollten diese Funktion überschreiben, wenn sie beispielsweise noch Dateien schließen müssen. Achtung! Wenn noch genügend Speicherplatz vorhanden ist, wird womöglich der GC nie aufgerufen.

Überschreiben wir in einer Unterklasse diese Methode, dann müssen wir auch gewährleisten, dass finalize() der Oberklasse aufgerufen wird. Das erreichen wir mit der Referenz super.

Beispiel Wir bilden eine Unterklasse von Font, um unsere eigenen Zeichensätze zu verwalten.

Font definiert eine finalize()-Methode und unsere Klasse soll auch finalize() implementieren:

class MehrAlsFontKann extends Font
{
MehrAlsFontKann () // Font hat keinen Standard-Konstruktor
{
super( null, PLAIN, 10 );
}

protected void
finalize() throws Throwable
{
/*
MehrAlsFontKann Dinge freigeben
...
*/
super.finalize();
}
}


Galileo Computing

6.8.7 Synchronisation  downtop

Threads können miteinander kommunizieren und dabei Daten teilen. Sie können außerdem auf das Eintreten bestimmter Bedingungen warten, z. B. auf neue Eingabedaten. Die Klasse Object definiert fünf Versionen der Methoden wait() und notify() bzw. nofiyAll() zur Beendigungssynchronisation von Threads. Ein Sonderkapitel geht näher auf die Programmierung von Threads ein.


Galileo Computing

6.9 Die Oberklasse gibt Funktionalität vor  downtop

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- 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 Socken- und Hosen-Klassen ein wenig. Damit wir die neuen Klassen unterscheiden können, lassen wir sie mit einem P beginnen. Aus Kleidung wird PKleidung, aus Hose wird PHose und aus Socke wird PSocke. Zunächst führen wir in der Klasse PKleidung die Methode kennung() ein:

Listing 6.19   Polymorphie.java, Teil 1
class PKleidung
{
public String farbe;

String kennung()
{
return "Keine Ahnung was ich mal werde";
}
}

Vielleicht ist an dieser Stelle noch nicht ganz klar, wieso wir die Methode kennung() einführen, hatten doch die Klasse Hose und Socke schon eine Methode kennung(). Das ist zwar richtig, war aber zu unflexibel gewesen, da Hose und Socke keine Verbindung hatten. Beide Klassen enthielten nur »zufällig« jeweils eine Methode mit dem Namen kennung(), die jedoch nichts miteinander zu tun hatten. Mit der vorgenommenen Modellierung schaffen wir aber eine Gemeinsamkeit, da Hose und Socke nun automatisch eine Methode kennung() haben.

In UML sieht das mit den Klassen PSocke und PHose und der Vererbung von PKleidung wie folgt aus:

Abbildung

Die Implementierung von Socke und Hose bleibt, nur fügen die Klassen dann diese Methode nicht neu hinzu, sondern überschreiben die geerbte Implementierung aus der Oberklasse Kleidung. In Quellcode sieht das dann so aus:

Listing 6.20   Polymorphie.java, Teil 2
class PSocke extends PKleidung
{
String kennung()
{
return "Ich bin eine Socke";
}
}

class PHose extends PKleidung
{
String kennung()
{
return "Ich bin eine Hose";
}
}

Es fehlen noch ein paar kleine Testzeilen:

PSocke s = new PSocke();
PHose h = new PHose();
System.out.println( s.kennung() );
System.out.println( h.kennung() );

Die angegebenen Zeilen sind leicht zu verstehen. Die Laufzeitumgebung sucht nun von unten nach oben in der Vererbungshierarchie nach der Methode kennung() und findet sie in PHose bzw. PSocke. Würden wir eine neue Klasse schaffen und kennung() nicht überschreiben, so würde die Laufzeitumgebung kennung() in PKleidung finden.


Galileo Computing

6.9.1 Dynamisches Binden als Beispiel für Polymorphie  downtop

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

PKleidung k1 = new PSocke();
PKleidung k2 = new PHose();

Dies geht auf jeden Fall in Ordnung. PSocke und PHose sind Unterklassen von PKleidung, und k1 und k2 verzichten auf die möglicherweise hinzugefügten Attribute der Unterklassen.

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

System.out.println( k1.kennung() 
);
System.out.println( k2.kennung() );

Jetzt ist die spannendste Frage in der gesamten Objektorientierung Folgende: Was passiert bei dem Methodenaufruf? Es gibt zwei Möglichkeiten. Da der Typ der Variablen k1 und k2 Kleidung ist, wird die Methode auf PKleidung aufgerufen und in beiden Fällen ist die Ausgabe »Keine Ahnung, was ich mal werde«. Eine andere Lösung ist, dass sich die Laufzeitumgebung erinnert, dass der Typ der Variablen zwar PKleidung ist, aber das dahinter ein Socken- bzw. Hosen-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 kennung() 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 beiden Begriffe synonym verwenden.

Werfen wir einen Blick auf ein Programm, welches dynamisches Binden noch deutlicher macht.

Listing 6.21   Polymorphie.java, Teil 3
public class Polymorphie
{
public static void main( String args[] )
{
PKleidung k[] = new PKleidung[4];

k[0] = new PSocke();
k[1] = new PHose();
k[2] = new PHose();
k[3] = new PSocke();

kennungenAusgeben( k );
}

Wir erzeugen vier konkrete Kleidungsstücke, die wir in ein Feld einsortieren. Anschließen übergeben wir der Methode kennungenAusgeben() das Feld mit den Kleidungsobjekten:

 static void kennungenAusgeben( 
PKleidung k[] )
{
for ( int i=0; i<k.length; i++ )
// Hier die Polymorphie
System.out.println( k[i].kennung() );
}
}

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

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

Listing 6.22   NoPolyWithPrivata.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, das 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 Methoden 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 Methoden 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.9.3 Konstruktoren in der Vererbung  downtop

Obwohl Konstruktoren Ähnlichkeiten 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, kann diskutiert werden, in der 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.

Der Aufruf wird meistens automatisch vom Compiler eingefügt und eine Modifikation des Bytecodes würde die Aufrufreihenfolge empfindlich stören. Denn im Bytecode gibt es diese Verpflichtung nicht. Die Sprache sieht für den ausdrücklichen Aufruf des Konstruktors der Oberklasse die Anweisung super() vor. Die Referenz super zeigt nur auf ein Objekt der Oberklasse. Mit Klammern ist immer ein Aufruf verbunden. Erinnern wir uns an dieser Stelle noch einmal an this und this().

Ein Beispiel mit Konstruktorweiterleitung

Schauen wir uns noch einmal die Konstruktorverkettung an:

class Kleidung
{
}

class Sakko extends Kleidung
{
}

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

Kleidung()
{
super(); // für Object()
}

Sakko()
{
super(); // für Kleidung()
}

Wir sehen, dass wir nicht ausdrücklich super() schreiben müssen, da es der Compiler macht.

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

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

public Vector(int initialCapacity, 
int 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 Parameter aufrufen

Mitunter ist es nötig, nicht nur den Standard-Konstruktor anzusteuern, sondern einen anderen der Oberklasse, den wir uns aussuchen wollen. Dazu kann super() mit Parametern gefüllt werden. Gründe dafür könnten sein:

1. Ein parametrisierter Konstruktor der Unterklasse leitet oft die Parameter an die Oberklasse weiter.

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);
}
}

Variableninitialisierung

Dass ein Konstruktor der Unterklasse zuerst den Konstruktor der Oberklasse aufruft, kann die Initialisierung der Variablen in der Unterklasse anfällig stören.

Nehmen wir an, wir hätten in Sakko eine Objektvariable alter=3 eingesetzt. Wo wird diese initialisiert? Vor oder hinter dem super()? 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 ja 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 Kleidung) nicht auf die Variablen der Unterklasse (hier alter) zugreifen kann. Aber wir können in der Oberklasse eine Methode der Unterklasse aufufen, nämlich genau die, die die Unterklase aus der Oberklasse überschreibt. Da Methodenaufrufe dynamisch gebunden werden, kann eine Methode den Wert auslesen.

Beispiel Noch nicht korrekt initialisierte Objekte. Listing 6.23   Sakko.java
class Kleidung
{
Kleidung()
{
wasBinIch();
}

void wasBinIch()
{
System.out.println("Ich weiß es noch nicht :-(");
}
}

public class Sakko extends Kleidung
{
String was = "Ich bin ein Sakko";

void wasBinIch()
{
System.out.println( was );
}

public static void main( String args[] )
{
Kleidung k = new Kleidung();

Sakko blau = new Sakko();
blau.wasBinIch();
  }

Die Ausgabe ist nun Folgende:

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

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

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

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.


Galileo Computing

6.10 Abstrakte Klassen  downtop

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 die 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. Eine Exemplar der Oberklasse selbst muss nicht existieren.

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

Beispiel Eine abstrakte Klasse Kleidung ist die Oberklasse für konkrete Kleidungsstücke.
abstract 
class Kleidung
{
public void wasche() {
System.out.println( "Werde sauber" );
}
public void trockne() {
System.out.println( "Werde trocken" );
}
}

Mit dieser abstrakten Klasse Kleidung drücken wir aus, dass es eine allgemeine Klasse ist, zu der keine konkreten Objekte existieren. Es gibt in der realen Welt schließlich keinen Gegenstand, der nur ein allgemeines, unspezifiziertes Kleidungsstück ist, sondern nur spezielle Unterarten von Kleidungsstücken, zum Beispiel eine Hose oder eine Socke. Es macht also keinen Sinn, ein Exemplar der Klasse Kleidung zu bilden. Die Klasse soll nur in der Hierarchie auftauchen, um alle konkreten Kleidungsklassen als Typ von Kleidung 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.

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 Hose extends Kleidung
{
}

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

Hose     h1   = new Hose();
Kleidung h2 = new Hose();
Kleidung ks[] = new Kleidung[]{ new Hose(), new Hose() };


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.

Beispiel Eine vollständig abstrakte Klasse mit nur einer abstrakten Methode.
abstract class Material
{
abstract int gewicht()
;
}

Die Definition einer abstrakten Methode wird mit einem Semikolon abgeschlossen.


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 bekommen 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 nur eine InstantiationException ein.

In UML gibt es mehrere Notationen für abstrakte Klassen. Eine ist, den Namen der Klasse und die abstrakten Attribute kursiv zu setzen.

Abbildung 6.7   Abstrakte Methoden werden kursiv dargestellt
Abbildung

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.
Beispiel Eine tierische Hierarchie 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

Listing 6.24   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 auf Grund der nun bekannten Informationen nur die Methode istSäuger(), die Methode ausgeben() 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.

Es ist durch die automatische Typanpassung ohne weiteres möglich, einem Tier-Objekt eine Referenz für ein Mensch- bzw. 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, sodass alter auf -1 gesetzt wird.

Beispiel Wir rufen ausgabe() von einem Mensch- und Delfin-Objekt auf. Listing 6.25   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  downtop

Wenn wir eine Klasse als final deklarieren, so bedeutet es, 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, sodass 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 der Zukunft in Java auch abstract final-Klassen geben.


Galileo Computing

6.11 Schnittstellen  downtop

Eine Schnittstelle enthält keine Implementierungen, sondern nur Namen und Signaturen der enthaltenen Methoden. Anders gesagt, sie sind vollständig abstrakte Klassen. Obwohl keine Funktionen ausprogrammiert werden und keine Objektvariablen deklariert werden dürfen, sind static final-Variablen (benannte Konstanten) in einer Schnittstelle erlaubt. Möchte eine Klasse eine Schnittstelle verwenden, so folgt hinter dem Klassennamen das Schlüsselwort implements. Eine Klasse kann mehrere Schnittstellen implementieren.

Abbildung

Obwohl Schnittstellen auf den ersten Blick nichts bringen – Programmierer wollen gerne etwas vererbt bekommen, damit sie Implementierungsarbeit sparen können – sind sie eine enorm wichtige Erfindung. Denn über Schnittstellen lässt sich eine Sicht auf ein Objekt beschreiben. Jede Schnittstelle definiert solch eine Sicht, eine Art Rolle. Implementiert eine Klasse diverse Schnittstellen, so können ihre Exemplare in verschiedenen Rollen auftreten. Hier wird erneut das Substitutionsprinzip wichtig, bei dem ein mächtigeres Objekt verwendet wird, auch wenn weniger erwartet wird.

Hinweis Bei einer Implementierung einer Schnittstelle müssen die Methoden in den Unterklassen öffentlich implementiert werden, da die Methoden in Schnittstellen immer automatisch public sind.

Beispiel Die Schnittstelle Material ist mit drei Konstanten gefüllt. Daneben gibt es eine nicht ausprogrammierte Funktion berechneGewicht(). Diese wird nicht als abstract gekennzeichnet, da alle Methoden in Schnittstellen automatisch abstrakt sind. Der Compiler akzeptiert natürlich redundante Angaben von abstract und public.

Listing 6.26   Materialien.java, Teil 1
interface Materialeigenschaft
{
int HART = 0,
WEICH = HART+1,
FLAUSCHIG = WEICH+1;

int berechneGewicht();
}
class Wolle
implements Materialeigenschaft
{
public int berechneGewicht() { return 234; }
}

class Synthetik
implements Materialeigenschaft
{
public int berechneGewicht() { return 452; }
}

abstract class Stahl
implements Materialeigenschaft
{
boolean glänzend;
}

Die Klassen Wolle und Synthetik implementieren Material und müssen daher nicht zwingend abstrakt sein, da es eine Implementierung der abstrakten Methode aus der Schnittstelle Materialeigenschaft gibt. Wolle und Synthetik überschreiben die Methode und definieren die Funktion berechneGewicht() beide anders. Dies ist einleuchtend, denn alle Materialien haben ein unterschiedliches Gewicht. Eine Ausnahme bildet die Klasse Stahl, die die Methode nicht überschreibt. Sie erbt eine abstrakte Methode, ohne sie zu implementieren – deshalb muss Stahl auch abstrakt sein.

Hinweis zur Sprechweise: Klassen werden vererbt und Schnittstellen implementiert.

Jetzt ist es kein Problem, ein konkretes Wolle- oder Synthetik-Objekt zu konstruieren. Da in der Vererbungskette etwa gilt, Wolle ist vom Typ Materialeigenschaft (und auch vom Typ Object, wie alles), lässt sich schreiben:

Listing 6.27   Materialien.java, Teil 1
public class Materialien
{
Materialeigenschaft wolle = new Wolle();
Materialeigenschaft stahl = new Synthetik();
}

Ein Polymorphie-Beispiel mit Schnittstellen

An dieser Stelle sei noch einmal an die Möglichkeit erinnert, Funktionen auf Objekten auszuführen, die eine gemeinsame Basis haben.

Beispiel Anwendung der gemeinsamen Methodenbasis.

Unterschiedliche Klassen, die die Schnittstelle Atom implementieren, definieren unterschiedliche Protonenwerte. So verstehen alle Klassen die Methode protonen(), die die Schnittstelle vorschreibt. Die UML-Abbildung soll das verdeutlichen.


Abbildung

Listing 6.28   AtomInterface.java, Teil 1
interface Atom
{
int protonen();
}

class Wasserstoff implements Atom {
public int protonen() { return 1; }
}

class Helium implements Atom {
public int protonen() { return 2; }
}

class Lithium implements Atom {
public int protonen() { return 3; }
}

class Sauerstoff implements Atom {
public int protonen() { return 8; }
}

Im Beispiel implementieren verschiedene Stoffe das Interface Atom. Jeder Stoff informiert über die Methode protonen() über seine Anzahl von Protonen.

Das Testprogramm verbindet ein Kürzel mit einem Objekt. Als Typ reicht lediglich Atom aus. Die Methode protonen() landet dann dynamisch gebunden beim richtigen Objekt.

Listing 6.29   AtomInterface.java, Teil 2
public class AtomInterface
{
public static void main( String[] args )
{
char atomKürzel = 'h'; // setze Kürzel mit irgendeinem Wert

Atom atom = kürzelZuAtom( atomKürzel );

System.out.println( atom.protonen() );
}

static Atom kürzelZuAtom( char kürzel )
{
switch ( kürzel )
{
case 'h': return new Helium();
case 'o': return new Sauerstoff();
case 'l': return new Lithium();
}
return new Wasserstoff();
}
}

Im Zusammenhang mit Schnittstellen bleibt zusammenfassend zu sagen, das hier dynamisches Binden pur auftaucht.


Galileo Computing

6.11.1 Die Mehrfachvererbung bei Schnittstellen  downtop

Bei Klassen gibt es die Einschränkung, dass nur von einer direkten Oberklasse abgeleitet werden darf. Wird hingegen eine Schnittstelle implementiert, dann werden nicht mehr aus verschiedenen Quellen unterschiedliche Implementierungen für dieselbe Methode angeboten, was zu Problemen führen kann. Der Unterschied zu Klassen ist jetzt, dass mehrere Schnittstellen von einer einzigen Klasse implementiert werden können. Dies wird gelegentlich als »Mehrfachvererbung in Java« bezeichnet.

Dabei ist für w2 die Methode compareTo() und für w3 die Methode protonen() nicht sichtbar. Für w4 sind dagegen alle Methoden sichtbar.

Beispiel Sollte Wasserstoff nicht nur die Methoden von Atom implementieren, sondern auch von java.lang.Comparable, damit ein Atom mit einem anderen Atom verglichen werden kann, schreiben wir:
class Wasserstoff implements Atom, 
Comparable
{
public int protonen() { return 1; } // Aus Atom

public int compareTo( Object o ) {/*...*/} // Aus Comparable
}

Durch diese Mehrfachvererbung bekommt Wasserstoff zwei Obertypen, sodass sich je nach der Sichtweise schreiben lässt:

Object      w1 = new Wasserstoff();

Atom w2 = new Wasserstoff();

Comparable w3 = new Wasserstoff();

Wasserstoff w4 = new Wasserstoff();


Galileo Computing

6.11.2 Erweitern von Interfaces – Subinterfaces  downtop

Ein Subinterface ist die Erweiterung eines anderen Interfaces. Diese Erweiterung erfolgt – wie bei der Vererbung – durch das Schlüsselwort extends.

Beispiel Eine Schnittstelle erbt von einer anderen Schnittstelle.
interface SchönesAtom extends 
Atom
{
double atomgewicht();
}

Eine Klasse, die nun SchönesAtom implementiert, muss die Methoden von beiden Schnittstellen implementieren, demnach die Methode atomgewicht() aus SchönesAtom sowie die Methode protonen(), die in Atom angegeben wurde.


Galileo Computing

6.11.3 Vererbte Konstanten bei Schnittstellen  downtop

Schnittstellen können Variablen besitzen, die jedoch, wie wir gesehen haben, immer automatisch statisch und final, also Konstanten, sind. Diese Konstanten können einer anderen Schnittstelle vererbt werden. Es gibt dabei einige kleine Einschränkungen.

Wir wollen an einem Beispiel sehen, wie sich die Vererbung auswirkt, wenn gleiche Bezeichner in den Unter-Schnittstellen erneut verwendet werden. Das nachfolgende Programm implementiert folgendes UML-Diagramm:

Abbildung

Listing 6.30   VererbteSchnittstellen.java
interface Grundfarben
{
int ROT = 1;
int GRÜN = 2;
int BLAU = 3;
}

interface Sockenfarben extends Grundfarben
{
int SCHWARZ = 10;
int LILA = 11;
}

interface Hosenfarben extends Grundfarben
{
int LILA = 11;
int SCHWARZ = 20;
int BRAUN = 21;
}

interface Allefarben extends Sockenfarben, Hosenfarben
{
int BRAUN = 30;
}

public class VererbteSchnittstellen
{
public static void main( String args[] )
{
System.out.println( Sockenfarben.ROT ); // 1
System.out.println( Allefarben.ROT ); // 1
System.out.println( Hosenfarben.SCHWARZ ); // 20

// System.out.println( Allefarben.SCHWARZ );

// The field name "SCHWARZ" is an ambiguous name
//found in the types "Sockenfarben" and "Hosenfarben".

// System.out.println( Allefarben.LILA );
}
}

Die Definitionen der Schnittstellen können ohne Fehler übersetzt werden. Das Programm zeigt im Wesentlichen vier Eigenschaften:

1. Schnittstellen vererben ihre Eigenschaften an die Unter-Schnittstellen. Es erbt Sockenfarben das Attribut ROT aus Grundfarben.

Galileo Computing

6.11.4 Vordefinierte Methoden einer Schnittstelle  downtop

Der Typ eines Objekts bei der Deklaration einer Referenz kann entweder ein Objekt oder eine Schnittstelle sein. Ist die Referenz vom Typ einer Schnittstelle, dann ist es bemerkenswert, dass der Compiler erlaubt, Methoden der Klasse Object für diese Referenz aufzurufen.

Beispiel Wir definieren eine Schnittstelle ohne besondere Methoden und eine Klasse, die keine besonderen Methoden hinzufügt.
interface I
{
}

class C implements I
{
}

Erzeugen wir ein Objekt vom Typ C, so kennt C automatisch alle Methoden aus C (keine) und zusätzlich, auf Grund des impliziten extends Object, auch die Methoden aus Object. Daher lässt sich schreiben:

C ref = new C();
ref.toString();

Die Schnittstelle spielt hier noch keine Rolle. Was jedoch auch funktioniert ist Folgendes:

I ref = new C();
ref.toString();

Es ist zu erwarten, dass ref nur Methoden aus I nutzen kann und das sind keine! Da allerdings jede Klasse automatisch von Object erbt und damit die Methoden besitzt, erlaubt der Compiler Zugriff auf diese Eigenschaften. So lässt sich vereinfacht sagen, dass alle Methoden von Object erlaubt sind, auch wenn ein Interface selbst diese Methoden nicht besitzt. Jede Schnittstelle ist somit eine indirekte Erweiterung von Object.

Schnittstellen-Methoden, die nicht implementiert werden müssen

Bis auf eine Ausnahme muss eine Klasse, zu der Exemplare erzeugt werden sollen, alle Methoden der Schnittstellen implementieren. Eine Ausnahme ergibt sich wieder aus der Tatsache, dass jede Schnittstelle die Methoden von Object annimmt. Schauen wir uns den Programmcode der Schnittstelle Comparator an, die im Paket java.util definiert ist:

package java.util;

public interface Comparator
{
int compare( Object o1, Object o2 );

boolean equals( Object obj );
}

Wir entdecken, dass dort die equals()-Methode vorgeschrieben wird. Der erste Gedanke ist, nun eine Klasse zu schreiben, die compare() und equals() implementieren muss. Dies ist hier allerdings nicht nötig. Denn equals() ist schon eine Methode, die jedes Objekt besitzt. Daraus ergibt sich, dass nicht alle Methoden ausprogrammiert werden müssen. (Eventuell überschreiben wir equals(), wenn uns die Semantik von equals() in Object nicht gefällt.) Weiter lässt sich eine Schnittstelle angeben, die die Methoden von Object auflistet. Auch dann müsste keine Methode implementiert werden. Bleibt die Frage, warum denn Comparator eine equals()-Methode vorschreibt, wenn diese doch nicht implementiert zu werden braucht. Um uns zu verwirren? Nein. Der Sinn besteht einfach in der genauen Angabe der Funktionsweise in der Dokumentation. Eine Java-Dokumentation kann nur generiert werden, wenn auch eine Funktion im Quellcode vorhanden ist. Die Entwickler wollten bei equals() in der Schnittstelle Comparator noch einmal bewusst auf die Funktion hinweisen, dass equals() zwei Comparator-Objekte daraufhin vergleicht, ob beide die gleiche Sortierfolge verwenden, und nicht, wie wir annehmen könnten, zwei Objekte auf Gleichheit testet.


Galileo Computing

6.11.5 CharSequence als Beispiel einer Schnittstelle  downtop

Bisher kennen wir die Klassen String und StringBuffer, um Zeichenketten zu speichern und weiterzugeben. Ein String ist ein Wertobjekt und ein wichtiges Hilfsmittel in Programmen, da durch dieses unveränderliche Zeichenketten-Werte repräsentiert werden, während StringBuffer veränderliche Zeichenfolgen unfasst. Aber wie sieht es aus, wenn eine Teilzeichenkette gefordert ist, bei der es egal sein soll, ob das Original als String- oder StringBuffer-Objekt vorliegt?

Eine Lösung ist, alles in ein String-Objekt zu konvertieren. Möchte ein Programm eine Teilfolge liefern, auf die jemand lesend zugreifen möchte, sie aber nicht verändern können soll, ist ein String zu träge. Aus den beliebigen Zeichenfolgen müsste zuerst ein String-Objekt konstruiert werden. Daher haben die Entwickler seit dem SDK 1.4 die Schnittstelle CharSequence eingefügt, die eine unveränderliche, nur lesbare Sequenz von Zeichen realisiert. Die Schnittstelle wird von StringBuffer und String implementiert, sodass sich alle Zeichenketten dieser Klassen als CharSequence auszeichnen. Funktionen müssen sich also nicht mehr für String oder StringBuffer entscheiden, sondern können einfach ein Char Sequence-Objekt als Parameter akzeptieren. Ein String und StringBuffer-Objekt können zwar mehr als CharSequence vorschreibt, aber beide lassen sich als CharSequence einsetzen, wenn das »Mehr« an Funktionalität nicht benötigt wird.

Abbildung


gp  char charAt( int index )
Liefert das Zeichen an der Stelle index.
gp  int length()
Gibt die Länge der Zeichensequenz zurück.
gp  CharSequence subSequence( int start, int end )
Liefert eine neue CharSequence von start bis end.
gp  String toString()
Gibt einen String der Sequenz zurück. Die Länge des toString()-Strings entspricht genau der Länge der Sequenz.
Beispiel Möchte eine Methode eine Zeichenkette bekommen und die Herkunft ist egal, so schreiben wir etwa:
void giveMeAText( CharSequence 
s )
{

}

anstatt

void giveMeAText( String s )
{
...
}
void giveMeAText( StringBuffer s )
{
void giveMeAText( new String(s) ); // oder Ähnliches
}

Anwendung von CharSequence in String

In den Klassen String und StringBuffer existiert eine Methode subSequence(), die ein CharSequence-Objekt liefert. Die Signatur ist in beiden Fällen die Gleiche. Die Funktion macht im Prinzip nichts anderes als ein substring(begin, end).

class java.lang.String implements CharSequence, Serializable


gp  CharSequence subSequence( int beginIndex, int endIndex )
Liefert eine neue Zeichensequenz von String bzw. StringBuffer.

Die Implementierung sieht so aus, dass mit substring() ein neuer Teilstring zurückgeliefert wird. Das ist eine einfache Lösung, aber nicht unbedingt die schnellste. Für String-Objekte ist das Erzeugen von Substrings ziemlich schnell, da die Methode speziell optimiert ist. Da Strings unveränderlich sind, wird einfach das gleiche char-Feld wie im Original-String verwendet, nur eine Verschiebung und ein Längenwert werden angepasst.


Galileo Computing

6.12 Innere Klassen  downtop

Bisher haben wir nur Klassen kennen gelernt, die entweder in Paketen organisiert waren oder in einer Datei. Diese Form von Klassen heißen »Top-Level-Klassen«. Es gibt darüber hinaus die Möglichkeit, eine Klasse in eine andere Klasse hineinzunehmen und sie damit noch enger aneinander zu binden. Eine Klasse, die so eingebunden wird, heißt »innere Klasse«. Im Allgemeinen sieht dies wie folgt aus:

class Außen {
class Innen {
}
}

Die Sprache definiert vier Typen von inneren Klassen, die im Folgenden beschrieben werden.


Galileo Computing

6.12.1 Geschachtelte Top-Level Klassen und Schnittstellen  downtop

Die einfachste Variante einer inneren Klasse oder Schnittstelle wird wie eine statische Eigenschaft in die Klasse eingesetzt. Wegen der Schachtelung wird dieser Typ im Englischen nested top-level class genannt.

Beispiel Lampe ist die äußere Klasse und Birne ist eine innere Klasse, die in Lampe geschachtelt ist.

Listing 6.31   Lampe.java
public class Lampe
{
static String s = "Huhu";
int i = 1;

static class Birne
{
void grüßGott()
{
System.out.println( s );
// System.out.println( i ); // Fehler
}
}
}

Die Eigenschaften der inneren Klasse Birne besitzen Zugriff auf alle anderen statischen Eigenschaften der äußeren Klasse Lampe. Zugriff auf Objektvariablen ist aus der statischen inneren Klasse nicht möglich, da sie als extra Klasse gezählt wird, die im gleichen Paket liegt. Die innere Klasse muss einen anderen Namen als die äußere haben. Obwohl es ungewöhnlich aussieht, führen innere statische Schnittstellen nicht zu einem Compilerfehler

Umsetzung der inneren Klassen

Es ist eine gelungene Arbeit der Sun-Entwickler, die Einführung von inneren Klassen ohne Änderung der virtuelle Maschine über die Bühne gebracht zu haben. Der Compiler generiert aus den inneren Klassen nämlich einfach normale Klassen, die jedoch mit einigen Spezialfunktionen ausgestattet sind. Für die entschachtelten inneren Klassen generiert der Compiler neue Namen nach dem Muster: ÄußereKlasse$InnereKlasse, das heißt, ein Dollar-Zeichen trennt die Namen von äußerer und innerer Klasse.


Galileo Computing

6.12.2 Mitglieds- oder Elementklassen  downtop

Eine Mitgliedsklasse (engl. member class), auch Elementklasse genannt, ist ebenfalls vergleichbar mit einem Attribut, nur ist es nicht statisch. Die innere Klasse kann zusätzlich auf alle Attribute der äußeren Klasse zugreifen. Dazu zählen auch die privaten Eigenschaften, eine Designentscheidung, die sehr umstritten ist und kontrovers diskutiert wird.

Beispiel Rahmen besitzt eine innere Mitgliedsklasse Muster. Listing 6.32   Rahmen.java
public class Rahmen
{
String s = "kringelich";

class Muster
{
void standard()
{
System.out.println( s );
}
// static void immer() { } // Fehler
}
}

Ein Exemplar der Klasse Muster hat Zugriff auf alle Eigenschaften von Rahmen. Um innerhalb der äußeren Klasse Rahmen ein Exemplar von Muster zu erzeugen, muss ein Exemplar der äußeren Klasse existieren. Das ist eine wichtige Unterscheidung gegenüber den statischen inneren Klassen von weiter oben. Statische innere Klassen existieren auch ohne Objekt der äußeren Klasse. Eine zweite wichtige Eigenschaft ist, dass innere Mitgliedsklassen selbst keine statischen Eigenschaften definieren dürfen.

Beispiel Für das Beispiel Rahmen und Muster erzeugt der Compiler die Dateien Rahmen.class und Rahmen$Muster.class. Damit ohne Änderung der virtuellen Maschine die innere Klasse an die Attribute der äußeren kommt, wird in jedem Exemplar der inneren Klasse eine Referenz auf das zugehörige Objekt der äußeren Klasse gelegt. Damit kann die innere Klasse auch auf nicht statische Attribute der äußeren zugreifen. Für die innere Klasse ergibt sich folgendes Bild in Rahmen$Muster.class:
class Rahmen$Muster
{
private Rahmen this$0;
// ...
}

Die Variable this$0 ist eine Kopie der Referenz auf Rahmen.this. Die Konstruktoren der inneren Klasse erhalten einen zusätzlichen Parameter vom Typ Rahmen, mit dem die this$0-Variable initialisiert wird.

Innere Klassen von außen erzeugen

Innerhalb der äußeren Klassen kann einfach mit dem new-Operator ein Exemplar der inneren Klasse erzeugt werden. Kommen wir von außerhalb und wollen Exemplare der inneren Klasse erzeugen, so müssen wir bei Elementklassen sicherstellen, dass es ein Exemplar der äußeren Klasse gibt. Die Sprache schreibt eine neue Form für die Erzeugung mit new vor, die das allgemeine Format

ref.new InnereKlasse(...)

besitzt. Dabei ist ref eine Referenz der äußeren Klasse.

Beispiel Die Klasse Haus besitzt die innere Klasse Zimmer.
class Haus
{
class Zimmer
{
}
}

Um von außen ein Objekt von Zimmer aufzubauen, schreiben wir:

Haus h = new Haus();
Zimmer z = h.new Zimmer();

oder auch in einer Zeile:

Zimmer z = new Haus().new Zimmer();

Die this-Referenz

Möchte eine innere Klasse In auf die this-Referenz der umgebenden Klasse Out zugreifen, so schreiben wir Out.this. Wenn sich Variablen überdecken, so schreiben wir Out.this.Eigenschaft, um an die Eigenschaften der äußeren Klasse zu gelangen. Das Schlüsselwort outer ist zwar im Sprachstandard reserviert, wird aber dafür nicht eingesetzt.

Beispiel Elementklassen können beliebig geschachtelt sein, und da der Name eindeutig ist, gelangen wir immer mit dem Klassenname.this an die jeweilige Eigenschaft.

Listing 6.33   Haus.java
class Haus
{
String s = "Haus";

class Zimmer
{
String s = "Zimmer";

class Stuhl
{
String s = "Stuhl";

void ausgabe()
{
System.out.println( s ); // Stuhl
System.out.println( this.s ); // Stuhl
System.out.println( Stuhl.this.s ); // Stuhl
System.out.println( Zimmer.this.s ); // Zimmer
System.out.println( Haus.this.s ); // Haus
}
}
}

public static void main( String args[] )
{
new Haus().new Zimmer().new Stuhl().ausgabe();
}
}

Betrachten wir das obere Beispiel, dann lassen sich Objekte für die inneren Klassen Haus, Zimmer und Stuhl wie folgt erstellen:

Haus a = new Haus;             
       // Exemplar von Haus
Haus.Zimmer b = a.new Zimmer(); // Exemplar von Zimmer in a
Haus.Zimmer.Stuhl c = b.new Stuhl(); // Exemplar von Stuhl in b
c.ausgabe(); // Methode vom Stuhl

Damit ist auch deutlich geworden, dass die Qualifizierung mit dem Punkt bei Haus.Zimmer.Stuhl nicht automatisch bedeutet, dass Haus ein Paket mit dem Unterpaket Zimmer ist, in dem die Klasse Stuhl existiert. Das macht es für die Lesbarkeit nicht gerade einfacher und es droht eine Verwechselungsgefahr zwischen inneren Klassen und Paketen. Deshalb sollte die Namenskonvention befolgt werden: Klassennamen beginnen mit Großbuchstaben am Anfang, Paketnamen mit Kleinbuchstaben.


Galileo Computing

6.12.3 Lokale Klassen  downtop

Lokale Klassen sind auch innere Klassen, die jedoch nicht als Eigenschaft direkt in einer Klasse eingesetzt werden. Diese Form der inneren Klasse befindet sich in Anweisungsblöcken von Methoden oder Initialisierungsblöcken. Lokale Schnittstellen sind nicht möglich.

Beispiel Die main()-Methode besitzt eine innere Klasse mit einem Konstruktor, der auf die finale Variable j zugreift.

Listing 6.34   DrinnenMachtSpass.java
public class DrinnenMachtSpass
{
public static void main( String args[] )
{
int i = 2;
final int j = 3;

class In
{
In() {
System.out.println( j );
// System.out.println( i ); // Fehler
}
}
new In();
}
}

Die Definition der inneren Klasse In ist wie eine Anweisung eingesetzt. Jede lokale Klasse kann auf Methoden der äußeren Klasse zugreifen und zusätzlich auf die lokalen Variablen und Parameter, die mit dem Modifizierer final als unveränderlich ausgezeichnet sind. Liegt die innere Klasse in einer statischen Methode, kann sie jedoch keine Objektmethode aufrufen. Eine weitere Einschränkung im Vergleich zu den Elementklassen ist, dass die Modifizierer public, protected, private und static nicht erlaubt sind.


Galileo Computing

6.12.4 Anonyme innere Klassen  downtop

Anonyme Klassen gehen noch einen Schritt weiter als lokale Klassen. Sie haben keinen Namen und erzeugen immer automatisch genau ein Objekt. Klassendefinition und Objekt-erzeugung sind zu einem Sprachkonstrukt verbunden. Die allgemeine Notation ist Folgende:

new KlasseOderSchnitstelle() { 
/* innere Klasse */ }

Dazu gibt es zwei unterschiedliche Ausprägungen:

gp  Wenn hinter new der Klassenname A steht, dann ist die anonyme Klasse eine Unterklasse von A.
gp  Wenn hinter new der Name einer Schnittstelle S steht, dann implementiert die anonyme Klasse die Schnittstelle S und erbt von Object.

Für anonyme innere Klassen gilt die Einschränkung, dass keine zusätzlichen extends- oder implements-Angaben möglich sind.

Beispiel Wir wollen eine innere Klasse schreiben, die Unterklasse von Date ist. Sie soll die to toString()-Methode überschreiben.

Listing 6.35   InnerToStringDate.java
import java.util.*;

public class InnerToStringDate
{
public static void main( String args[] )
{
int i = 2;
final int j = 3;

Date d;

d =
new Date() {
public String toString() {
return getDate() + "." + (getMonth()+1) + "." +
(1900+getYear());
}
};

System.out.println( d );
}
}

Da sofort eine Unterklasse von Date definiert wird, fehlt der Name der inneren Klasse. Das einzige Exemplar dieser anonymen Klasse lässt sich über die Variable d weiterverwenden.

Hinweis Es lässt sich leicht ausmalen, dass eine innere Klasse zwar nützliche Methoden der Oberklasse überschreiben oder Schnittstellen implementieren kann. Neue Eigenschaften anzubieten wäre zwar legal, aber nicht sinnvoll. Denn von außen wären diese Methoden und Attribute unbekannt, da die zugänglichen Eigenschaften der Oberklasse den Zugriff festlegen. Deshalb sind auch anonyme Unterklassen von Object (ohne weitere implementierte Schnittstellen) nur selten nützlich.

Schauen wir uns ein weiteres Beispiel für die Implementierung von Schnittstellen an. Um nebenläufige Programme zu implementieren, gibt es die Klasse Thread oder die Schnittstelle Runnable. Programmbausteine, die parallel ausgeführt werden sollen, werden bei einer Klasse in eine Methode run() gesetzt und dann ein Exemplar im Konstruktor eines Thread-Objekts übergeben. Der Thread wird mit start() angekurbelt. Das geht schön mit einer inneren Klasse, die die Schnittstelle Runnable implementiert:

new Thread( new Runnable() {
public void run() {
for ( int i=0; i<10; i++ )
System.out.println( i );
};
}).start();

for ( int i=0; i<10; i++ )
System.out.println( i );

In der Ausgabe wird zum Beispiel Folgendes erscheinen (hier komprimiert):

0 1 2 3 4 5 6 7 8 0 1 2 3 4 5 6 
7 8 9 9

Der erste Thread beginnt recht schnell seine Zahlen auszugeben. Aber an der doppelten Zahl 9 sehen wir, dass die beiden Teile wirklich parallel arbeiten. Ausführliche Informationen finden sich im Thread-Kapitel.

Konstruktoren

Da anonyme Klassen keinen Namen haben, muss für Konstruktoren ein anderer Weg gefunden werden. Dazu dienen die so genannten Exemplarinitialisierungsblöcke. Das sind Blöcke in geschweiften Klammen direkt innerhalb einer Klasse.

Beispiel Die anonyme Klasse ist eine Unterklasse von Point und initialisiert im Konstruktor einen Punkt mit den Koordinaten -1, -1. Aus diesem speziellen Punkt-Objekt lesen wir dann die Koordinaten wieder aus.

Listing 6.36   AnonymUndInnen.java
import java.awt.*;

public class AnonymUndInnen
{
public static void main( String args[] )
{
System.out.println( new Point() {
{ x = -1; y = -1; }
}.getLocation() );

System.out.println( new Point(-1,0) {
{ y = -1; }
}.getLocation() );
}
}

Diesen Effekt können wir natürlich auch ohne anonyme Klasse haben:

new Point(-1,-1).getLocation()

super()

Innerhalb eines Konstruktors kann kein super() verwendet werden, um den Konstruktor der Oberklasse aufzurufen. Dies liegt daran, dass automatisch ein super() in den Initialisierungsblock eingesetzt wird. Die Parameter für die gewünschte Variante des (überladenen) Oberklassen-Konstruktors werden am Anfang der Definition der anonymen Klasse angegeben. Dies zeigt das zweite Beispiel im Programm AnonymUndInnen.java:

System.out.println( new Point(-1,0) 
{ { y = -1; } }.getLocation() );
Beispiel Wir initialisieren ein Objekt BigDecimal, welches beliebig große Ganzzahlen aufnehmen kann. Im Konstruktor der anonymen Unterklasse geben wir anschließend den Wert mit der geerbten toString()-Methode aus.
new java.math.BigDecimal("12345678901234567890") 
{
{ System.out.println(toString()); }
};


Galileo Computing

6.12.5 Eine Sich-Selbst-Implementierung  downtop

Eine Klasse kann entweder von einer Klasse erben oder eine (bzw. mehrere) Schnittstellen implementieren. Es ergibt sich ein Sonderfall, wenn wir eine Schnittstelle implementieren, die innerhalb derjenigen Klasse definiert ist, die die Schnittstelle implementiert. Das sieht etwa so aus:

class Outer implements 
Outer.InterfaceInner
{
interface
InterfaceInner
{
void knexDirDeineWelt();
}

public void knexDirDeineWelt()
{
}
}

Prinzipiell spricht erst einmal nichts gegen diese Implementierung. Innere Klassen, wie InterfaceInner, werden auf eine extra Klassendatei abgebildet, da es innere Klassen beziehungsweise Schnittstellen für die Laufzeitumgebung sowieso nicht gibt. In unserem Fall könnte der Compiler die Datei Outer$InterfaceInner erzeugen. Im nächsten Schritt würde dann Outer diese Schnittstelle erweitern und wie im Beispiel eine Methode überschreiben.

So schön dies auch aussieht: Es funktioniert nicht! Frühere Compiler erlaubten diese Konstellation, aber sie führt zu zirkulären Abhängigkeiten:

cyclic inheritance involving Outer

Wenn InterfaceInner zuerst übersetzt würde und dann Outer, wäre es noch zu verstehen, doch Probleme verursachen zum Beispiel Definitionen in der inneren Klasse, die von der äußeren Klasse abhängig sind. Wir könnten etwa den Rückgabewert von knexDirDeineWelt()so ändern, dass es ein Outer-Objekt zurückliefert:

class Outer implements Outer.InterfaceInner
{
interface InterfaceInner
{
Outer knexDirDeineWelt();
}

public Outer knexDirDeineWelt()
{
}
}

Jetzt sehen wir: Ohne InterfaceInner kein Outer, da dies knexDirDeineWelt() vorschreibt und ohne Outer kein InterfaceInner, da sonst der Rückgabewert nicht bekannt ist. Mitunter wäre das Problem sogar lösbar, doch hier lässt der Compiler lieber die Finger von.

Innere Klasse vor der äußeren Klasse

Dass es nicht unmöglich ist, eine innere Klasse von der äußeren abzuleiten, zeigt folgendes Beispiel:

interface I
{
void boo();
interface J extends I
{
}

J foo();
}

Es ist möglich, dass die innere Schnittstelle die äußere erweitert. Es ist auch möglich, dass eine innere Klasse eine äußere erweitert:

class O
{
class I extends O
{
void bar()
{
}
}

void bar() { }
}

Galileo Computing

6.12.6 this und Vererbung  downtop

Wenn wir ein qualifiziertes this verwenden, dann bezeichnet C.this die äußere Klasse, also das umschließende Exemplar. Gilt jedoch die Beziehung C1.C2. ... Ci. ... Cn., dann haben wir mit Ci.this ein Problem, wenn Ci eine Oberklasse von Cn ist. Es geht also um den Fall, dass eine textuell umgebende Klasse zugleich auch Oberklasse ist. Das eigentliche Problem liegt darin, dass hier zweidimensionale Namensräume hierarchisch kombiniert werden. Die eine Dimension sind die Bezeichner bzw. Methoden aus den lexikalisch umgebenden Klassen, die andere Dimension sind die ererbten Eigenschaften aus der Oberklasse. Hier sind beliebige Überlappungen und Mehrdeutigkeiten denkbar. Durch diese ungenaue Beziehung zwischen inneren Klassen und Vererbung kam es unter JDK 1.1 und 1.2 zu unterschiedlichen Ergebnissen.

Beispiel In der Klasse Schuh erweitert die innere Klasse Fuss den Schuh und überschreibt die Methode wasBinIch().

Listing 6.37   Schuh.java
public class Schuh
{
void wasBinIch()
{
System.out.println( "Ich bin ein Schuh" );
}

class Fuss extends Schuh
{
void spannung()
{
Schuh.this.wasBinIch();
}
void wasBinIch()
{
System.out.println( "Ich bin ein Schuh.Fuss" );
}
}
public static void main( String args[] )
{
new Schuh().new Fuss().spannung();
}
}

Legen wir in der main()-Funktion ein Objekt der Klasse Fuss an, dann landen wir in der Klasse Fuss und nicht in Schuh. Das heißt, die Ausgabe ist:

Ich bin ein Schuh.Fuss
 

Das bedeutet, dass in spannung() durch Schuh.this zwar das zum Fuss-Objekt gehörende Schuh-Exemplar gemeint ist, wir aber durch die Überschreibung dennoch in der Methode aus der Klasse Fuss landen. Vor 1.2 kam als Ergebnis die erste Zeichenkette heraus. Das Ergebnis unter JDK 1.2 ist analog zu ((Schuh)this).wasBinIch().


Galileo Computing

6.12.7 Implementierung einer verketteten Liste  downtop

Verkette Listen gibt es in Java seit der Java ist auch eine Insel-Plattform als vordefinierte Klasse, sodass wir eigentlich nicht auf die Implementierung schauen müssten. Da es für viele Leser jedoch noch ein Geheimnis ist, wie die dazu benötigten Pointer in Java abgebildet werden, schauen wir uns eine einfache Implementierung an. Zunächst benötigen wir eine Zelle, die Daten und eine Referenz auf das folgende Listenelement speichert. Die Zelle wird durch die Klasse Cell modelliert. Im UML-Diagramm taucht die innere Klasse dann nicht auf.

Abbildung 6.8   Definition einer verketteten Liste
Abbildung

Listing 6.38   LinkedListDemo.java
class LinkedList
{
private class Cell
{
Object data;
Cell next;

public Cell( Object o )
{
data = o;
}
}

private Cell head, tail;

public void add( Object o )
{
Cell n = new Cell( o );

if ( tail == null )
head = tail = n;
else
{
tail.next = n;
tail = n;
}
}

public String toString()
{
String s = "";

Cell cell = head;

while ( cell != null )
{
s = s + cell.data + " ";
cell = cell.next;
}

return s;
}
}

Eine Liste besteht nun aus einer Menge von Cell-Elementen. Da diese Objekte fest mit der Liste verbunden sind, ist hier der Einsatz von geschachtelten Klassen sinnvoll. Cell kann aber auch eine statische innere Klasse sein, das spart Platz und Zeit in der JVM. Die Liste selbst benötigt zum Einfügen nur einen Verweis auf den Kopf (erstes Element) und auf das Ende (letztes Element). Um nun ein Element dieser Liste hinzuzufügen, erzeugen wir zunächst eine neue Zelle n. Ist tail und head gleich null, heißt dies, dass es noch keine Elemente in der Liste gibt. Danach legen wir die Referenzen für Listenanfang und -ende auf das neue Objekt. Werden nun später Elemente eingefügt, hängen sie sich hinter tail. Wenn es schon Elemente in der Liste gibt, dann ist tail nicht gleich null, und es zeigt auf das letzte Element. Seine next-Referenz zeigt auf null und wird dann mit einem neuen Wert belegt, nämlich mit dem des neu beschafften Objekts n. Nun hängt es in der Liste drin und das Ende muss noch angepasst werden. Daher legen wir die Referenz tail auch noch auf das neue Objekt.

Listing 6.39   LinkedListDemo.java
public class LinkedListDemo
{
public static void main( String args[] )
{
LinkedList l = new LinkedList();

l.add( "Hallo" );
l.add( "Otto" );

System.out.println( l );
}
}

Galileo Computing

6.12.8 Funktionszeiger  downtop

Das folgende Beispiel implementiert Funktionszeiger über Schnittstellen. Das Interface Function definiert eine Funktion calc, die von zwei Prozeduren ausprogrammiert wird. Wir benutzen als Testprogramme zwei innere Klassen, die im Interface eingebettet sind. Wir implementieren damit den ersten Teil unseres UML-Diagramms.

Abbildung

Listing 6.40   Function.java
public interface Function
{
public int calc( int num );

class FunctionOne implements Function
{
public int calc( int num )
{
return num*2;
}
}

class FunctionTwo implements Function
{
public int calc( int num )
{
return num*4;
}
}
}

Die beiden Funktionen FunctionOne und FunctionTwo implementieren Function jeweils so, dass calc die als Parameter übergebene Zahl, die mit zwei bzw. vier multipliziert wird, ausgibt. Eine Klasse FunctionTest sortiert beide Funktionen in ein Feld func ein und ruft die beiden Funktionen anschließend auf.

Listing 6.41   FunctionTest.java
public class FunctionTest
{
final int MAX = 2;
final Function[] func = new Function[MAX];

// Constructor

FunctionTest()
{
func[0] = new Function.FunctionOne();
func[1] = new Function.FunctionTwo();
}

void calcAll( int n )
{
System.out.println( "Funktion 0" + func[0].calc( n ) );
System.out.println( "Funktion 1" + func[1].calc( n ) );
}

// Main program

public static void main( String args[] )
{
FunctionTest ft = new FunctionTest();

ft.calcAll( 42 );
}
}

Galileo Computing

6.13 Gegenseitige Abhängigkeiten von Klassen  downtop

In Java brauchen wir uns keine Gedanken um die Reihenfolge der Deklarationen zu machen. Wo es in anderen Sprachen genau auf die Reihenfolge ankommt, kann in Java eine Klasse eine andere benutzen, auch wenn diese erst später im Programmtext definiert ist. In C ist dies ein bekanntes Problemfeld. Wir wollen eine Funktion nutzen, müssen diese aber vor dem Aufruf schon definiert haben. Noch schlimmer ist dies bei verketteten Listen und ähnlichen Datenstrukturen. Dann wird dort erst deklariert (zum Beispiel class Element) und später definiert und implementiert. In Java können wir uns ganz und gar auf den Compiler verlassen – es ist seine Aufgabe mit den Abhängigkeiten zurechtzukommen.

Ein gewisses Problem bereiten die Abhängigkeiten dennoch, zum Beispiel die eines Angestelltenverhältnisses. Jeder Arbeiter hat einen Vorarbeiter, aber auch ein Vorarbeiter ist ein Arbeiter. Wie sollen nun die Klassen implementiert werden? Definieren wir die Arbeiter-Klasse zuerst, kann der Vorarbeiter sie erweitern. Aber dann kennt der Arbeiter noch keinen Vorarbeiter! Dieses Problem ist glücklicherweise nur in C oder C++ problematisch. In Java hilft uns der Compiler, denn dieser macht während des Compile-Vorgangs eine Vorausschau in den Dateien der importierten Pakete und in den eigenen Dateien. Die Beziehung zwischen Arbeiter und Vorarbeiter kann auf zwei Wegen gelöst werden: Jede Klasse wird in eine Datei gekapselt. So etwa in die Datei Arbeiter.java und in die Datei Vorarbeiter.java. Alternativ können beide Klassen in einer Datei wie folgt definiert sein:

class Arbeiter
{
Vorabeiter vorarbeiter;
// was einen Arbeiter so auszeichnet
}

Im Falle der Dateitrennung wird der Compiler in die jeweils andere Quelltext-Datei schauen. Bei der Lösung mit nur einer Datei findet der Compiler die andere Klasse in der gleichen Datei.

class Vorarbeiter extends Arbeiter
{
// und hier, was er alles mehr hat
}

Galileo Computing

6.14 Pakete  toptop

Ein Paket ist eine Gruppe von thematisch zusammengehörigen Klassen, die sich normalerweise10  in einem Verzeichnis befinden. Den Verzeichnisnamen gibt ein Paketname an:

package süßigkeiten;

class Zucker
{
...
}

public class Schokolade extends Zucker
{
...
}

Alle Klassen, die in dieser Datei implementiert werden, gehören zum Paket süßigkeiten. Die Zugehörigkeit wird durch das Schlüsselwort package ausgedrückt.

Um die Pakete zu nutzen, wird innerhalb einer Klasse mit import auf die Klassen im Paket aufmerksam gemacht. Importiert ein Paket ein anderes, so können die Klassen schnell referenziert werden:

package leckereien;

import süßigkeiten.Schokolade;

class Weihnachtsmann
{
Schokolade s; // sonst süßigkeiten.Schokolade
}

Damit nicht alle Klassen eines Pakets einzeln aufgeführt werden müssen, lässt sich mit dem Sternchen als einer Art Wildcard auf alle public-Klassen zugreifen. Häufig Gebrauch machen davon Programme mit grafischer Oberfläche; in den ersten Zeilen findet sich dann:

import java.awt.*

In den ersten Java-Versionen ließ sich auch etwa import java.awt; anstatt import java.awt.*; schreiben.

Natürlich müssen wir diesen import nicht schreiben. Er dient lediglich als Abkürzung für die Klassenbezeichnung. Ein Problem gibt es bei mehreren gleich benannten Klassen in verschiedenen Paketen, hier ist eine volle Qualifizierung nötig. Ab dem SDK 1.2 gibt es im Paket java.awt und java.util eine Liste. Ein einfaches import java.awt.* und java.util.* hilft da nicht, denn der Compiler weiß nicht, ob die GUI-Komponente oder die Datenstruktur gemeint ist. Auch sagt ein import nichts darüber aus, ob die Klassen in der importierenden Datei jemals gebraucht werden.

Pakete sind oft in Hierarchien geordnet. Dies wird auch durch die Abbildung auf die Verzeichnisstruktur des Dateisystems deutlich. Daher gehören zu einem Paket oft verschiedene Unterpakete. Es werden durch import java.* nicht automatisch alle Klassen der Unterpakete mit eingebunden. Die import-Anweisung bezieht sich nur auf ein Verzeichnis und schließt die Unterverzeichnisse nicht mit ein.

 






1    Leider konnte eine frühe Version des freien IBM-Compiler Jikes nicht erkennen, dass PUNKTIERT+1 zur Übersetzungszeit eine Konstante ist.

2    Wir wollen hier den Fall, dass der Konstruktor der Oberklasse i einen Wert ungleich 0 setzt, nicht betrachten.

3    Lange Tradition hat der Garbage-Collector unter LISP und unter Smalltalk, aber auch Visual Basic benutzt einen GC.

4    Die Erfinder von Wallace&Gromit und Chicken Run. Für den neuen Film haben 40 Kneter in drei Jahren zwei Tonnen Plastilin geformt

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

6    Die Mathematiker werden sich freuen, denn die Methode equals bildet eine Äquivalenzrelation. Sie ist, wenn wir die null-Referenz außen vor lassen, reflexiv, symmetrisch und transitiv.

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

8    Äquivalent zu denen in Objective-C.

9    Oder mit extern oder Prototypen diese Definition vorziehen.

10    Ich schreibe »normalerweise«, da die Paketstruktur nicht zwingend auf Verzeichnisse abgebildet werden muss. Pakete könnten beispielsweise vom Klassenlader aus einer Datenbank gelesen werden.

  

Java 2




Copyright © Galileo Press GmbH 2002
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, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de