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

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

Java ist auch eine Insel
5., akt. und erw. Auflage
1454 S., mit CD, 49,90 Euro
Galileo Computing
ISBN 3-89842-747-1
gp Kapitel 6 Eigene Klassen schreiben
  gp 6.1 Eigene Klassen definieren
    gp 6.1.1 Methodenaufrufe und Nebeneffekte
    gp 6.1.2 Argumentübergabe mit Referenzen
    gp 6.1.3 Die this-Referenz
    gp 6.1.4 Überdeckte Objektvariablen nutzen
  gp 6.2 Assoziationen zwischen Objekten
    gp 6.2.1 Gegenseitige Abhängigkeiten von Klassen
  gp 6.3 Privatsphäre und Sichtbarkeit
    gp 6.3.1 Wieso nicht freie Methoden und Variablen für alle?
    gp 6.3.2 Privat ist nicht ganz privat: Es kommt darauf an, wer’s sieht
    gp 6.3.3 Zugriffsmethoden für Attribute definieren
  gp 6.4 Statische Methoden und statische Attribute
    gp 6.4.1 Warum statische Eigenschaften sinnvoll sind
    gp 6.4.2 Statische Eigenschaften mit static
    gp 6.4.3 Statische Eigenschaften über Referenzen nutzen?
    gp 6.4.4 Warum die Groß- und Kleinschreibung wichtig ist
    gp 6.4.5 Statische Eigenschaften und Objekteigenschaften
    gp 6.4.6 Statische Variablen zum Datenaustausch
    gp 6.4.7 Statische Blöcke als Klasseninitialisierer
  gp 6.5 Konstanten und Aufzählungen
    gp 6.5.1 Konstanten über öffentliche statische final-Variablen
    gp 6.5.2 Problem mit finalen Klassenvariablen
    gp 6.5.3 Typsicherere Konstanten
    gp 6.5.4 Aufzählungen mit enum in Java 5
    gp 6.5.5 enum-Konstanten in switch
  gp 6.6 Objekte anlegen und zerstören
    gp 6.6.1 Konstruktoren schreiben
    gp 6.6.2 Einen anderen Konstruktor der gleichen Klasse aufrufen
    gp 6.6.3 Initialisierung der Objekt- und Klassenvariablen
    gp 6.6.4 Finale Werte im Konstruktor und in statischen Blöcken setzen
    gp 6.6.5 Exemplarinitialisierer (Instanzinitialisierer)
    gp 6.6.6 Zerstörung eines Objekts durch den Müllaufsammler
    gp 6.6.7 Implizit erzeugte String-Objekte
    gp 6.6.8 Private Konstruktoren, Utility-Klassen, Singleton und Fabriken
  gp 6.7 Vererbung
    gp 6.7.1 Vererbung in Java
    gp 6.7.2 Einfach- und Mehrfachvererbung
    gp 6.7.3 Gebäude modelliert
    gp 6.7.4 Konstruktoren in der Vererbung
    gp 6.7.5 Sichtbarkeit protected
    gp 6.7.6 Das Substitutionsprinzip
    gp 6.7.7 Automatische und explizite Typanpassung
    gp 6.7.8 Typen testen mit dem binären Operator instanceof
    gp 6.7.9 Array-Typen und Kovarianz
    gp 6.7.10 Methoden überschreiben
    gp 6.7.11 Mit super eine Methode der Oberklasse aufrufen
    gp 6.7.12 Kovariante Rückgabetypen
    gp 6.7.13 Finale Klassen
    gp 6.7.14 Nicht überschreibbare Funktionen
    gp 6.7.15 Zusammenfassung zur Sichtbarkeit
    gp 6.7.16 Sichtbarkeit in der UML
    gp 6.7.17 Zusammenfassung: Konstruktoren und Methoden
  gp 6.8 Object ist die Mutter aller Oberklassen
    gp 6.8.1 Klassenobjekte
    gp 6.8.2 Objektidentifikation mit toString()
    gp 6.8.3 Objektgleichheit mit equals() und Identität
    gp 6.8.4 Klonen eines Objekts mit clone()
    gp 6.8.5 Hashcodes
    gp 6.8.6 Aufräumen mit finalize()
    gp 6.8.7 Synchronisation
  gp 6.9 Die Oberklasse gibt Funktionalität vor
    gp 6.9.1 Dynamisches Binden als Beispiel für Polymorphie
    gp 6.9.2 Keine Polymorphie bei privaten, statischen und finalen Methoden
    gp 6.9.3 Polymorphie bei Konstruktoraufrufen
  gp 6.10 Abstrakte Klassen und abstrakte Methoden
    gp 6.10.1 Abstrakte Klassen
    gp 6.10.2 Abstrakte Methoden
  gp 6.11 Schnittstellen
    gp 6.11.1 Ein Polymorphie-Beispiel mit Schnittstellen
    gp 6.11.2 Die Mehrfachvererbung bei Schnittstellen
    gp 6.11.3 Erweitern von Interfaces – Subinterfaces
    gp 6.11.4 Vererbte Konstanten bei Schnittstellen
    gp 6.11.5 Vordefinierte Methoden einer Schnittstelle
    gp 6.11.6 Abstrakte Klassen und Schnittstellen im Vergleich
    gp 6.11.7 CharSequence als Beispiel einer Schnittstelle
    gp 6.11.8 Die Schnittstelle Iterable
  gp 6.12 Innere Klassen
    gp 6.12.1 Statische innere Klassen und Schnittstellen
    gp 6.12.2 Mitglieds- oder Elementklassen
    gp 6.12.3 Lokale Klassen
    gp 6.12.4 Anonyme innere Klassen
    gp 6.12.5 this und Vererbung
    gp 6.12.6 Implementierung einer verketteten Liste
    gp 6.12.7 Funktionszeiger
  gp 6.13 Generische Datentypen
    gp 6.13.1 Einfache Klassenschablonen
    gp 6.13.2 Einfache Methodenschablonen
    gp 6.13.3 Umsetzen der Generics, Typlöschung und Raw-Types
    gp 6.13.4 Einschränken der Typen
    gp 6.13.5 Generics und Vererbung, Invarianz
    gp 6.13.6 Wildcards
  gp 6.14 Die Spezial-Oberklasse Enum
    gp 6.14.1 Methoden auf Enum-Objekten
    gp 6.14.2 enum mit eigenen Konstruktoren und Methoden
  gp 6.15 Dokumentationskommentare mit javaDoc
    gp 6.15.1 Einen Dokumentationskommentar setzen
    gp 6.15.2 Mit javadoc eine Dokumentation erstellen
    gp 6.15.3 HTML-Tags in Dokumentationskommentaren
    gp 6.15.4 Generierte Dateien
    gp 6.15.5 Weitere Dokumentationskommentare
    gp 6.15.6 javaDoc und Doclets
    gp 6.15.7 Veraltete (deprecated) Klassen, Konstruktoren und Methoden


Galileo Computing

6.13 Generische Datentypedowntop

Nehmen wir an, wir wollten einen Container für eine einfache Zahl implementieren; der Datentyp soll int sein.

class IntBox
{
  private   int   val;
  void setValue(   int   val )
  {
    this.val = val;
  }
    int   getValue()
  {
    return val;
  }
}

Es gibt einen automatischen Standard-Konstruktor, und mit setValue() können wir eine Zahl angeben und den Wert über die Zugriffsfunktion auslesen. Was ist, wenn wir eine zweite Box für Strings implementieren wollen? Wir müssten Quellcode duplizieren, und überall, wo int steht, den Datentyp String einsetzen:

class StringBox
{
  private   String   val;
  void setValue(   String   val )
  {
    this.val = val;
  }
    String   getValue()
  {
    return val;
  }
}

In unserem Beispiel sind das drei Änderungen, aber der Rest des Gerüstes bleibt gleich. Lediglich für einen neuen Datentyp muss eine neue Klasse her – die Logik bleibt die gleiche. Es gibt eine ganze Reihe von Beispielen, in denen Speicherstrukturen wie unsere Box nicht nur für einen Datentyp sinnvoll sind. Das gilt auch für Algorithmen, etwa einen, der Zahlen sortiert. Wenn zwei Zahlen größer oder kleiner sein können, muss ein Algorithmus lediglich diese Eigenschaft nutzen können, aber es wäre egal, ob die Zahlen der Größe byte, short, int, long oder auch Gleitkommazahlen sind – der Algorithmus selbst ist davon nicht betroffen.

Ein Algorithmus, der von einem Datentyp unabhängig programmiert werden kann, heißt generisch, und die Möglichkeit, in Java mit generischen Typen zu arbeiten, Generics Leider funktioniert das Beispiel mit der IntBox in Java nicht, denn generische Typen können in Java nur Objekte sein, aber keine primitiven Datentypen. Das schränkt die Möglichkeiten zwar ein, weil es Autoboxing gibt, lässt sich aber damit leben.

Die Implementierung der Java-Generics basiert auf einem Projekt namens Pizza. Später wurde aus Pizza GJ, und schließlich wurde dies zur Basis des Java Specification Request 14: »Add Generic Types To The Java Programming Language«.


Galileo Computing

6.13.1 Einfache Klassenschablonen  downtop

Wenn wir die StringBox als Basis nehmen, haben wir bei der Übersetzung der IntBox in die StringBox drei Stellen gefunden, an der wir den Typ ändern müssen. Wollen wir die Box in eine generische Klasse umbauen, so müssen wir an den drei Stellen, an denen der konkrete Typ String vorkam, einen Typ-Stellvertreter, eine so genante Typ-Variable, einsetzen. Der Name der Typ-Variablen muss in der Klassendefinition angegeben werden, denn es kann durchaus mehr als einen Stellvertreter geben. Die Syntax bei den Generics ist für unsere Box folgende:

class Box<  T  >
{
  private   T   val;
  void setValue(   T   val )
  {
    this.val = val;
  }
    T   getValue()
  {
    return val;
  }
}

An Stelle des konkreten Typs String steht nun einfach T. Die Definition des Typnamens steht nur einmal zu Beginn der Klasse in eckigen Klammern hinter dem Klassennamen. Die Abkürzung ist oft T, das für Typ steht.

Um die Box nutzen zu können, müssen wir sie mit einem speziellen Typ erzeugen. Dazu wird hinter dem Klassennamen, wie auch in der Definition der Klasse, in spitzen Klammern der konkrete Typ angegeben, etwa String oder Integer oder Point.

Box<String>  stringBox = new Box<String>();
Box<Integer> intBox    = new Box<Integer>();
Box<Point>   pointBox  = new Box<Point>();

Das Schöne dabei ist, dass nun alle generischen Eigenschaften mit dem angegebenen Typ durchgeführt werden. Wenn wir etwa aus pointBox mit getValue() auf das Element zugreifen, ist es vom Typ Point.

double x = pointBox.getValue().getX();

Typanpassungen

Ein mit Generics spezifizierter Typ lässt sich durch Typanpassung auf eine allgemeine Form bringen.

List<Integer> list = new ArrayList<Integer>();
List nixMitGenericsList = (List) list;

Bei der nixMitGenericsList prüft der Compiler natürlich gar keine Typen mehr, denn er hat sie ja »vergessen«. Eine Anweisung, die einen Nicht-Integer-Typ in die Liste einfügt, bringt keinen Fehler zur Compile-Zeit, und so kann über die Hintertür ein falscher Typ in die Datenstruktur kommen.

nixMitGenericsList.add( "Das macht man aber nicht" );

Die Generics bieten uns Möglichkeiten, den Quellcode sicherer zu machen. Wir sollten diese Sicherheit nicht durch ungetypte Schreibweisen kaputtmachen.


Galileo Computing

6.13.2 Einfache Methodenschablonen  downtop

Eine Klasse kann ganz normal ohne Schablone definiert werden, aber mit Methoden, die die Typen generisch vorschreiben. Das gilt für Objektmethoden und Klassenmethoden. Interessant ist dies für Utility-Klassen, die nur statische Funktionen anbieten, aber selbst nicht als Objekt vorliegen.

class Util
{
  public static   <T> T zufall( T mT n )
    {
    return Math.random() > 0.5 ? m : n;
  }
}

Hier wird die Funktion zufall() auf einem beliebigen Typ deklariert; die Angabe von <T> beim Klassennamen entfällt und verschiebt sich auf die Definition der Methode.

Die Benutzung kann ohne Angabe des Typs – aber doch überprüft – geschehen:

String s = Util.zufall( "Essen""Schlafen" );

Geben wir an Stelle eines Strings den Typ Integer an, wird der Compiler einen Fehler melden.


Galileo Computing

6.13.3 Umsetzen der Generics, Typlöschung und Raw-Types  downtop

Es gibt zwei Realisierungsmöglichkeiten von generischen Datentypen:

gp  Heterogene Variante. Für jeden Typ (etwa String, Integer, Point) wird individueller Code erzeugt, also drei Klassen;
gp  Homogene Übersetzung. Für jede parametrisierte Klasse wird eine Klasse erzeugt, die statt des generischen Typs Object einsetzt. Für einen konkreten Typ werden Typanpassungen in die Anweisungen eingebaut.

Java nutzt die homogene Übersetzung.

Typlöschung

Übersetzt der Java-Compiler die generischen Anwendungen, so löscht er dabei alle Typinformationen (engl. typ erasure). Das ist zwar für die Laufzeitumgebung praktisch, weil sie nicht auf die Generics großartig angepasst werden muss, sonst aber stellt das ein riesiges Problem dar, weil dann die Typinformationen zur Laufzeit nicht vorhanden sind. Programmierer müssen das wissen, da zum Beispiel der instanceof-Operator so nicht funktioniert:

Box<String> stringBox = new Box<String>();
if( stringBox instanceof Box<Number> )       // illegal generic type for instanceof
  System.out.println( "Jepp" );

Der Compiler meldet zu Recht einen Fehler, weil es den Typ Box<String> und Box<Number> zur Laufzeit gar nicht gibt – es sind nur typgelöschte Box-Objekte. Das zeigt auch das folgende Beispiel:

Box<String>  box1 = new Box<String>();
Box<Integer> box2 = new Box<Integer>();
System.out.println( box1.getClass() == box2.getClass() );     // true

Dass diese Informationen übrigens nicht vorliegen, wird auch damit begründet, dass die Laufzeit leiden könnte. Microsoft war das aber egal, dort besteht Generizität in der Common Language Runtime (CLR). Microsoft ist damit einen klaren Schritt voraus.

Raw-Type

Generische Klassen müssen nicht unbedingt parametrisiert werden; sie sind dem Datentyp Object weiterhin gütig. Das ist auch wichtig, denn sonst könnten viele parametrisierte neue Klassen nicht mehr mit altem Programmcode verwendet werden.

Box stringBox = new Box<String>();
Box objectBox = new Box();

Ein parametrisierter Typ ohne Typ heißt Raw-Type. In unserem Beispiel ist Box der Raw-Type von Box<T>. Raw-Types bieten die gleiche Funktionalität wie parametrisierte Typen, nur dann nicht überprüft. Im ersten Fall liefert getValue() den korrekten Typ String, im zweiten Fall nur Object, da im zweiten Fall jeder Bezug zum Typ fehlt.

Doch was ist eigentlich mit setValue()? Diese Funktion ist so ausgelegt, dass sie ein Argument von mindestens dem Typ akzeptiert, mit dem sie parametrisiert wurde. Fehlt durch die Verwendung des Raw-Typs der konkrete Typ, bleibt Object, doch in diesem Fall gibt der Compiler eine Warnung.

objectBox.setValue( "Unsafe type operation: " +
    "Should not invoke the method setValue(T) of raw type Box. " +
    "References to generic type Box<T> should be parameterized" );

Der Hinweis ist, dass die Box typisiert hätte werden müssen. Achten wir darauf nicht, kann es schnell zu Problemen führen:

Box<String> stringBox = new Box<String>();
Box         objectBox = stringBox;

Der Compiler gibt keinen Fehler und auch keine Warnung aus. Die zweite Zeile ist allerdings hochgradig problematisch, denn über die nicht parametrisierte Objekt-Box können wir beliebige Objekte in die Box packen. Da aber objectBox in Wirklichkeit ein typgelöschter Verweis auf die String-Box ist, haben wir ein Typproblem:

objectBox.setValue( new java.util.Date() );   // Warnung vor der Unsafe type operation
System.out.println( stringBox.getValue() );   // führt zu einer ClassCastException

Galileo Computing

6.13.4 Einschränken der Typen  downtop

Bei der Definition einer Schablone können die Typen weiter eingeschränkt werden. Es kann vorgeschrieben werden, dass der Typ eine konkrete Schnittstelle implementieren muss. Unsere Definition von zufall() sah keine Einschränkungen vor, sodass zum Beispiel auch Folgendes möglich ist:

Serializable s1 = new Serializable() { };
Serializable s2 = new Point();
Util.zufall( s1s2 );

Da der Typ beliebig ist, können auch Objekte aufgenommen werden, die vielleicht wenig Sinn ergeben, wie unsere Klassen, die lediglich Serializable implementieren.

Einschränkung mit extends

Um dies zu vermeiden, kann der Typ bei der Deklaration eingeschränkt werden. Wollen wir nur Klassen, die CharSequence implementieren (also Zeichenfolgen wie String und StringBuffer/StringBuilder), so schreiben wir das in die Definition mit hinein.

public static <  T extends CharSequence  > T zufall( T mT n )
{
  return Math.random() > 0.5 ? m : n;
}

Jetzt wird der Compiler mit übergebenen Serializable einen Fehler melden. Einen Aufruf wie Util.zufall( "Kino", "Lesen" ); lässt der Compiler korrekterweise durch.

Sinnvoll ist diese Einschränkung, wenn es Schnittstellen sind, die Methoden vorschreiben, denn diese lassen sich direkt nutzen. Das ist logisch, denn bei einer Einschränkung des Typs wird der Compiler sicherstellen, dass die konkreten Typen die vorgeschriebene Schnittstelle haben und damit die Methoden existieren.

Nehmen wir an, wir wollten ein typsicheres max() implementieren. Es soll den größeren der beiden Werte zurückgeben. Vergleiche lassen sich einfach tätigen, wenn die Objekte Comparable implementieren, denn compareTo() liefert einen Rückgabewert, der aussagt, welches Objekt nach der definierten Metrik kleiner, größer oder gleich ist.

public static <T extends Comparable> T max( T mT n )
{
  return m.compareTo( n ) > 0 ? m : n;
}

Die Nutzung ist einfach:

System.out.println( Util.max( "Kino""Lesen" ) );                    // Lesen
System.out.println( Util.max( new Integer(12)new Integer(100) ) );  // 100

Einschränkung mit super

Neben extends kann der Typ auch mit super eingeschränkt werden. Dann sind nicht mehr alle Untertypen erlaubt, sondern der genannte Typ bestimmt den maximalen Obertyp. Während also <T extends CharSequence> eine Typisierung mit Klassen ermöglicht, die CharSequence implementieren, also mindestens vom Typ CharSequence sind, würde <T super String> die Typen einschließen, die in der Vererbungshierarchie unter String liegen. Die Einschränkung wird so wenig benötigt und ist im Allgemeinen sinnvoll für Wildcards, die wir gleich kennen lernen werden.

Weitere Obertypen mit &

Soll der konkrete Typ zu mehreren Typen passen, lassen sich mit einem & noch weitere Obertypen hinzunehmen. Wichtig ist aber, dass nur eine Klassen-Vererbungsangabe stattfinden kann, also nur ein extends stehen darf. Der Rest müssen implementierte Schnittstellen sein. Nehmen wir eine Oberklasse O und Schnittstellen I1 und I2 an. Dann sind die folgenden Deklarationen erlaubt:

gp  <T extends O>
gp  <T extends I1 & I2>
gp  <T extends O & I1>
gp  <T extends O & I1 & I2>

Falsch wäre etwa <T extends O1 & T extends O2>.


Galileo Computing

6.13.5 Generics und Vererbung, Invarianz  downtop

In Zusammenhang mit Generics und Vererbung muss beachtet werden, dass die übliche Substitution nicht funktioniert. Wenn Disko eine Unterklasse von Gebaeude ist, so ist Box<Disko> keine Unterklasse von Box<Gebaeude>, genauso wie Box<Object> nicht die Basisklasse von Box<Gebaeude> ist, beziehungsweise eine Box, die alle erdenklichen Typen ermöglicht.

Der Compiler meckert diesen Versuch sofort an.

Box<Object> box;
box = new Box<Disko>();      // incompatible types

Die Erklärung ist schnell an einem Beispiel gezeigt. Da nun die Box in setValue() alles vom Typ Object aufnimmt – und eine String ist ein Object – scheint zu funktionieren:

box.setValue( "Invarianz" );

Doch da die Box tatsächlich mit Diskotheken aufgebaut wurde, passt der String hier nicht hinein.

Die Ableitungsbeziehung zwischen Typargumenten überträgt sich nicht auf generische Klassen. Das wäre ein Fehler und daher gibt es keine Kovarianz bei Generics; die Tatsache nennt sich Invarianz. Bei Funktionsaufrufen ist das besonders lästig, doch später werden wir mit den Wildcards eine Lösung kennen lernen.

void out( Box<Gebauede> g )  // Klappt nicht! Wirklich nur Box<Gebauede>

Galileo Computing

6.13.6 Wildcards  toptop

Ein bewusstes »Vergessen« der Typinformationen lässt sich durch das Wildcard ? erreichen. Es erlaubt, verschiedene Unterklassen zusammenzuführen. Wir haben schon oben gesehen, dass etwa Folgendes nicht funktioniert:

Box<Number>  b;
Box<Integer> bI = new Box<Integer>();
Box<Double>  bD = new Box<Double>();
b = bI;                                   // Nein

Über Wildcard ist es jedoch möglich, den Typ von b frei zu lassen. Das ist jetzt aber nicht so gemeint, dass wir die Box als Box b definieren, sondern als

Box<?>  b;

Das Fragezeichen in der Angabe steht für alle Boxen und für alle Untertypen T im Fall von Box<T>. Es ist wichtig zu verstehen, dass ? nicht für Object steht, sondern für einen unbekannten Typ!

Da die Wildcards auch mit Typeinschränkung arbeiten, können wir auf diese Weise eine Box von Number-Objekten aufbauen:

Box<extends Number> b;
Box<Integer> bI = new Box<Integer>();
Box<Double>  bD = new Box<Double>();
Box<String>  bS = new Box<String>();

Die Schreibweise <? extends Klasse> nennt sich upper bound wildcard. (Eine Einschränkung der Art <? super Klasse> heißt lower bound wildcard.) So wird es funktionieren:

b = bI;   // oder b = bD

Aber nicht so:

b = bS;

Die Anwendung der upper bound wildcard und lower bound wildcard zeigen die Kapitel über Datenstrukturen.




1  In C(++) werden diese Typen von Klassen parametrisierte Klassen oder Templates (Schablonen) genannt.

 << zurück




Copyright © Galileo Press GmbH 2005
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de