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

 
Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Objektorientierte Beziehungsfragen
7 Ausnahmen müssen sein
8 Äußere.innere Klassen
9 Besondere Typen der Java SE
10 Generics<T>
11 Lambda-Ausdrücke und funktionale Programmierung
12 Architektur, Design und angewandte Objektorientierung
13 Komponenten, JavaBeans und Module
14 Die Klassenbibliothek
15 Einführung in die nebenläufige Programmierung
16 Einführung in Datenstrukturen und Algorithmen
17 Einführung in grafische Oberflächen
18 Einführung in Dateien und Datenströme
19 Einführung ins Datenbankmanagement mit JDBC
20 Einführung in <XML>
21 Testen mit JUnit
22 Bits und Bytes und Mathematisches
23 Die Werkzeuge des JDK
A Java SE-Paketübersicht
Stichwortverzeichnis


Download:

- Beispielprogramme, ca. 35,4 MB


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 10 Generics<T>
Pfeil 10.1 Einführung in Java Generics
Pfeil 10.1.1 Mensch versus Maschine – Typprüfung des Compilers und der Laufzeitumgebung
Pfeil 10.1.2 Taschen
Pfeil 10.1.3 Generische Typen deklarieren
Pfeil 10.1.4 Generics nutzen
Pfeil 10.1.5 Diamonds are forever
Pfeil 10.1.6 Generische Schnittstellen
Pfeil 10.1.7 Generische Methoden/Konstruktoren und Typ-Inferenz
Pfeil 10.2 Umsetzen der Generics, Typlöschung und Raw-Types
Pfeil 10.2.1 Realisierungsmöglichkeiten
Pfeil 10.2.2 Typlöschung (Type Erasure)
Pfeil 10.2.3 Probleme der Typlöschung
Pfeil 10.2.4 Raw-Type
Pfeil 10.3 Einschränken der Typen über Bounds
Pfeil 10.3.1 Einfache Einschränkungen mit extends
Pfeil 10.3.2 Weitere Obertypen mit &
Pfeil 10.4 Typparameter in der throws-Klausel *
Pfeil 10.4.1 Deklaration einer Klasse mit Typvariable <E extends Exception>
Pfeil 10.4.2 Parametrisierter Typ bei Typvariable <E extends Exception>
Pfeil 10.5 Generics und Vererbung, Invarianz
Pfeil 10.5.1 Arrays sind kovariant
Pfeil 10.5.2 Generics sind nicht kovariant, sondern invariant
Pfeil 10.5.3 Wildcards mit ?
Pfeil 10.5.4 Bounded Wildcards
Pfeil 10.5.5 Bounded-Wildcard-Typen und Bounded-Typvariablen
Pfeil 10.5.6 Das LESS-Prinzip
Pfeil 10.5.7 Enum<E extends Enum<E>> *
Pfeil 10.6 Konsequenzen der Typlöschung: Typ-Token, Arrays und Brücken *
Pfeil 10.6.1 Typ-Token
Pfeil 10.6.2 Super-Type-Token
Pfeil 10.6.3 Generics und Arrays
Pfeil 10.6.4 Brückenmethoden
Pfeil 10.6.5 Zum Weiterlesen
 

Zum Seitenanfang

10.5Generics und Vererbung, Invarianz Zur vorigen ÜberschriftZur nächsten Überschrift

Vererbung und Substitution ist für Java-Entwickler alltäglich, sodass diese Eigenschaft nicht weiter verwunderlich ist. Die toString()-Methode zum Beispiel wird ganz natürlich auf allen Objekten aufgerufen, und Entwicklern ist klar, dass der Aufruf dynamisch gebunden ist. Genauso lässt sich bei String.toString(Object o) jedes Objekt übergeben, und die statische Methode ruft die Objektmethode toString() auf.

 

Zum Seitenanfang

10.5.1Arrays sind kovariant Zur vorigen ÜberschriftZur nächsten Überschrift

Nehmen wir als Beispiel die Hierarchie der bekannten Wrapper-Klassen. Natürlich steht Object oben. Die numerischen Wrapper-Klassen erweitern alle die abstrakte Klasse Number. Darunter stehen dann etwa Integer, Double und die anderen numerischen Wrapper. Folgendes bereitet keine Kopfschmerzen:

Number number = Integer.valueOf( 10 );

number = Double.valueOf( 1.1 );

Einmal zeigt number auf ein Integer, dann auf ein Double-Objekt.

Wie verhält es sich nun mit Arrays? Da ist ein Number-Array der Basistyp eines Double-Arrays:

Number[] numbers = new Double[ 100 ];

numbers[ 0 ] = 1.1;

Dass ein Array vom Typ Double[] ein Untertyp von Number[] ist, nennt sich Kovarianz. Doch lässt sich das auf Generics übertragen?

 

Zum Seitenanfang

10.5.2Generics sind nicht kovariant, sondern invariant Zur vorigen ÜberschriftZur nächsten Überschrift

Es funktioniert, Folgendes zu schreiben:

Set<String> set = new HashSet<String>();

Ein HashSet mit Strings ist eine Art von Set mit Strings. Aber ein HashSet mit Strings ist kein HashSet mit Objects. Damit wäre Folgendes falsch:

HashSet<Object> set = new HashSet<String>(); // inline Compilerfehler!

Generics sind nicht kovariant, sie sind invariant. Diese Eigenschaft ist auf den ersten Blick nicht intuitiv, doch ein Beispiel rückt diesen Eindruck schnell gerade. Bleiben wir bei unserem Beispiel Pocket und den Wrapper-Klassen. Auch wenn Number die Oberklasse von Integer ist, so gilt dennoch nicht, dass Pocket<Number> ein Obertyp von Pocket<Integer> ist. Wäre es das, wäre Folgendes möglich und zur Laufzeit ein Problem:

Pocket<Number> p;

p = new Pocket<Integer>(); // Ist das OK?

p.set( 2.2 );

Das Argument 2.2 ist über Autoboxing ein Double, und daher scheint es auf Number zu passen. Allerdings sollte Double gar nicht erlaubt sein, da wir mit Pocket<Integer> ja eine Tasche für Integer aufgebaut haben, und ein Double darf nicht in die Integer-Tasche. Daher folgt: Die Ableitungsbeziehung zwischen Typen überträgt sich nicht auf generische Klassen. Ein Pocket<Number> ist also keine Oberklasse, die alle erdenklichen numerischen Typen in der Tasche erlaubt. Der Compiler meckert bei diesem Versuch sofort:

Pocket<Number> p;

p = new Pocket<Integer>(); // inline Type mismatch: cannot convert from Pocket<Integer>

// to Pocket<Number>

Auch durch eine alternative Schreibweise lässt sich der Compiler nicht in die Irre führen:

Pocket<Integer> p = new Pocket<>();

Pocket<Number> p2 = p; // inline Type mismatch: cannot convert

// from Pocket<Integer> to Pocket<Number>

[»]Hinweis

Im Fall von immutablen Objekten mit Nur-Lese-Zugriff bestünde eigentlich kein Grund für Kovarianz. Nehmen wir an, die folgende Deklaration wäre korrekt:

Pocket<Number> p = new Pocket<Integer>( 1 );

Number n = p.get();

Dann haben wir gezeigt, dass p.set(2.2) zum Beispiel nicht in Ordnung ist, da Double nicht mit Integer kompatibel ist. Aber wenn das Objekt etwa über den Konstruktor initialisiert würde, spräche nichts dagegen, mit einem kleineren Typ, also hier Number, daraus zu lesen. Jedoch kann Java nicht erkennen, ob ein Typ immutable ist, und kann daher auch bei den Generics solche Ausnahmen nicht machen. Der Compiler nimmt immer an, Zugriffe wären lesend und schreibend.

 

Zum Seitenanfang

10.5.3Wildcards mit ? Zur vorigen ÜberschriftZur nächsten Überschrift

Wir wollen eine Methode isOnePocketEmpty(…) schreiben, die eine variable Anzahl von beliebigen Tascheninhalten bekommt und testet, ob eine davon leer ist. Ein Aufruf könnte so aussehen:

Pocket<String> p1 = new Pocket<>( "Bad-Bank" );

Pocket<Integer> p2 = new Pocket<>( 1500000 );

System.out.println( isOnePocketEmpty( p1, p2 ) ); // false

Die erste Idee für den Methodenkopf sieht so aus:

public static boolean isOnePocketEmpty( Pocket<Object>... pockets )

Doch halt! Da Pocket<Object> nicht Taschen mit allen Typen umfasst, sondern nur exakt eine Tasche trifft, die ein Object-Objekt enthält, ist das keine sinnvolle Parametrisierung für isOnePocketEmpty(…). Das hatten wir im oberen Abschnitt schon festgestellt. Denn wäre das möglich, würde es die Typsicherheit gefährden. Wenn diese Methode tatsächlich Taschen mit allen Inhalten akzeptieren würde, so könnte einer Tasche leicht ein Wert mit falschem Typ untergeschoben werden. Wird isOnePocketEmpty(…) mit einem Pocket<String> aufgerufen, so würde wegen isOnePocketEmpty(Pocket<Object>... pockets) auch der Aufruf von set(12) auf dem Pocket gültig sein, und dann stünde plötzlich statt des gewünschten Inhalts vom Taschentyp String nun ein Integer in der Tasche. Das darf nicht gültig sein!

Ist der Typ egal, könnten wir an den Originaltyp (Raw-Type) denken. Doch die Raw-Types haben den Nachteil, dass bei ihnen der Compiler überhaupt nichts prüft, wir aber eine gewisse Prüfung möchten. So soll die Methode isOnePocketEmpty(…) beliebige Taschen entgegennehmen, aber gleichzeitig soll es der Methode auch verboten sein, falsche Dinge in die Taschen zu setzen. Ein isOnePocketEmpty(Pocket... pockets) ist also keine gute Idee und führt außerdem zu diversen Warnungen.

Die Lösung besteht im Einsatz des Wildcard-Typs ?. Er repräsentiert dann eine Familie von Typen. Wenn schon Pocket<Object> nicht der Basistyp aller Tascheninhalte ist, dann ist es Pocket<?>. Es ist wichtig zu verstehen, dass ? nicht für Object steht, sondern für einen unbekannten Typ! Damit lässt sich isOnePocketEmpty(…) realisieren:

Listing 10.16com/tutego/insel/generic/PocketsEmpty.java

public static boolean isOnePocketEmpty( Pocket<?>... pockets ) {

for ( Pocket<?> pocket : pockets )

if ( pocket.isEmpty() )

return true;



return false;

}



public static void main( String[] args ) {

Pocket<String> p1 = new Pocket<>( "Bad-Bank" );

Pocket<Integer> p2 = new Pocket<>( 1500000 );

System.out.println( isOnePocketEmpty( p1, p2 ) ); // false

System.out.println( isOnePocketEmpty( p1, p2, new Pocket<Byte>() ) ); // true

}

Dass der Aufruf von isOnePocketEmpty() bei keiner übergebenen Tasche zu false führt, soll an dieser Stelle als gegeben gelten.

Wir müssen Wildcards von Typvariablen gedanklich streng trennen. Instanziierungen mit Wildcards sind nicht erlaubt, da eine Wildcard ja eben nicht für einen konkreten Typ, sondern für eine ganze Reihe von möglichen Typen steht. Wildcards können auch nicht wie Typvariablen in Methoden genutzt werden, auch wenn der Typ beliebig ist.

Korrekt mit Typvariable

Falsch mit Wildcard (Compilerfehler!)

Pocket<?> pocket = new Pocket<Byte>();

Pocket<?> pocket = new Pocket<?>();

static <T> T random( T m, T n ) { }

static <?> ? random( ? m, ? n ) { }

Tabelle 10.8Möglicher und unmöglicher Einsatz von Wildcards

Auswirkungen auf Lese-/Schreiboperationen

Ist der Wildcard-Typ bei Pocket<?> im Einsatz, wissen wir nichts über den Typ, und dem Compiler gehen alle Informationen verloren. Deklarieren wir etwa

Pocket<?> p1 = new Pocket<Integer>();

oder

Pocket<Integer> p2 = new Pocket<Integer>();

Pocket<?> p3 = p2;

dann ist über die wirklichen Typparameter bei p1 und p3 nichts bekannt. Das hat wichtige Auswirkungen auf die Methoden, die wir auf Pocket aufrufen können:

  • Ein Aufruf von p1.get() ist legal, denn alles, was die Methode liefern wird, ist immer ein Object, auch wenn es null ist. Die Anweisung Object v = p1.get(); ist dementsprechend korrekt.

  • Ein p1.set(value) ist nicht erlaubt, da dem Compiler die Typparameter von p1 fehlen und er keine Typen prüfen kann. In p1 dürfen wir kein Double einsetzen, da Pocket nur Integer speichern soll. Die einzige Ausnahme ist null, da null jeden Typ hat. p1.set(null) ist also eine zulässige Anweisung. Das heißt ebenso, dass mit <?> aufgebaute Objekte nicht automatisch immutable sind.

 

Zum Seitenanfang

10.5.4Bounded Wildcards Zur vorigen ÜberschriftZur nächsten Überschrift

Die Angabe des konkreten Typparameters wie Pocket<Integer> und die Wildcard-Form Pocket<?> bilden Extreme. Die Tasche Pocket<Integer> nimmt nur Ganzzahlen auf, Pocket <?> auf der anderen Seite alles. Es muss aber auch etwas dazwischen geben, um zum Beispiel auszudrücken, dass die Tasche nur eine Zahl oder eine Zeichenkette enthalten soll.

Daher sind Typ-Einschränkungen mit extends und super möglich. Damit ergeben sich drei Arten von Wildcards:

Wildcard

Bezeichnung

Typparameter

?

Wildcard-Typ

ist beliebig

? extends Typ

Upper-bounded Wildcard-Typ

muss Typ erweitern

? super Typ

Lower-bounded Wildcard-Typ

muss über Typ stehen

Tabelle 10.9Die drei Wildcard-Typen

Die Wildcard beschreibt also die Eigenschaft eines Typparameters. Wenn es

Pocket<? extends Number> p;

heißt, dann können in der Tasche p alle möglichen Number-Objekte sein.

Machen wir extends und super noch an einem anderen Beispiel deutlich, das zeigt, welche Familie von Typen die Syntax beschreibt:

? extends CharSequence

? super String

CharSequence

String

String

CharSequence

StringBuffer

Object

StringBuilder

Tabelle 10.10Einige eingeschlossene Typen bei extends und super

Die erste Tabellenzeile (nach dem Tabellenkopf) macht deutlich, dass extends und super den angegebenen Typ selbst mit einschließen. In <? extends CharSequence> ist CharSequence genau der Upper-Bound der Wildcard, und in <? super String> ist String der Lower-Bound der Wildcard. Während die Anzahl der Typen beim Lower-Bound beschränkt ist (die Anzahl der Oberklassen kann sich nicht erweitern), ist die Anzahl der Typen mit Upper-Bound im Prinzip unbekannt, da es immer wieder neue Unterklassen geben kann.

Einsatzgebiete

Jeder der drei Wildcard-Typen hat seine Einsatzgebiete. Weitere Anwendungen der Upper-bounded Wildcard und der Lower-bounded Wildcard zeigen die Sortiermethoden der Datenstrukturen und Algorithmen:

Beispiel

Bedeutung

Pocket<?> p;

Taschen mit beliebigem Inhalt

Pocket<? extends Number> p;

Taschen nur mit Zahlen, also Unterklassen von Number, wie Integer, Double, BigDecimal

Comparator<? super String> comp;

Comparator, der Objekte vom Typ String, Object oder CharSequence vergleicht, also Obertypen von String. Idee: Ein Comparator, der zum Beispiel CharSequence-Objekte vergleichen kann, kann auch Strings vergleichen, denn durch die Vererbung ist ein String eine Art von CharSequence. Alle Comparator-Typen <? super String> können (irgendwie) Strings vergleichen.

Tabelle 10.11Beispiel für alle drei Wildcard-Typen

Beispiel mit Upper-bounded-Wildcard-Typ

Die Upper-bounded Wildcard ist häufiger zu finden als die Lower-bounded-Variante. Daher wollen wir ein Beispiel aufführen, an dem gut der übliche Einsatz für den Upper-Bound abzulesen ist. Unser Player hatte eine rechte und eine linke Tasche. Die Taschen sollen aber nun nicht alles Mögliche speichern können, sondern nur besondere Spielobjekte vom Typ Portable (engl. für tragbar). Portable ist eine Schnittstelle, die ein Gewicht für die tragbaren Objekte vorschreibt. Zwei Typen sollen tragbar sein: Pen und Cup. Die Implementierung sieht so aus:

Listing 10.17com/tutego/insel/generic/PortableDemo.java, Ausschnitt

interface Portable {

double getWeight();

void setWeight( double weight );

}



abstract class AbstractPortable implements Portable {



private double weight;



@Override public double getWeight() { return weight; }



@Override public void setWeight( double weight ) { this.weight = weight; }



@Override public String toString() { return getClass().getName() +

"[weight=" + weight + "]"; };

}

class Pen extends AbstractPortable { }



class Cup extends AbstractPortable { }

Um zu testen, ob der Spieler nicht zu viele Sachen trägt, soll eine Methode areLighterThan(…) prüfen, ob das Gewicht einer Liste von tragbaren Dingen unter einer gegebenen Grenze bleibt. Der erste Versuch könnte so aussehen:

boolean areLighterThan( List<Portable> collection, double maxWeight )

Moment! Das würde wieder ausschließlich Portable-Objekte akzeptieren, denn Kovarianz gilt ja nicht. Selbst wenn es gehen würde, könnte das bedeuten, dass in einer Methode dann vielleicht über collection.add(…) ein Pen hinzugefügt werden kann, auch wenn die übergebene Liste mit Cup deklariert wurde. Dann stände in der Liste plötzlich etwas Falsches. Außerdem ist Portable eine Schnittstelle, sodass die areLighterThan(…)-Methode mit einem Parametertyp List<Portable> überhaupt keinen Sinn ergibt. Eine vernünftige Schreibweise ist nur mit einem Upper-bounded-Wildcard-Typ möglich:

boolean areLighterThan( List<? extends Portable> list, double maxWeight )

Somit nimmt die Methode nur Listen mit Portable-Objekten an, und das ist auch nötig, denn Portable-Objekte haben ein Gewicht, und diese Eigenschaft brauchen wir.

Listing 10.18com/tutego/insel/generic/PortableDemo.java, Ausschnitt

class PortableUtils {



public static boolean areLighterThan( List<? extends Portable> list,

double maxWeight ) {

double accumulatedWeight = 0.0;



for ( Portable portable : list )

accumulatedWeight += portable.getWeight();



return accumulatedWeight < maxWeight;

}

}



public class PortableDemo {



public static void main( String[] args ) {

Pen pen = new Pen();

pen.setWeight( 10 );

Cup cup = new Cup();

cup.setWeight( 100 );

System.out.println( PortableUtils.areLighterThan( Arrays.asList( pen, cup ), 10 ) ); //false

System.out.println( PortableUtils.areLighterThan( Arrays.asList( pen, cup ), 120 ) ); //true

}

}

Wie schon besprochen wurde, kann aus der mit den Upper-bounded Wildcards deklarierten Datenstruktur List<? extends Portable> nur gelesen, aber nicht verändert werden.

 

Zum Seitenanfang

10.5.5Bounded-Wildcard-Typen und Bounded-Typvariablen Zur vorigen ÜberschriftZur nächsten Überschrift

Zwischen Bounded-Wildcard-Typen und Bounded-Typvariablen gibt es natürlich einen Zusammenhang, und bei der Deklaration sind zwei Varianten wählbar. Warum das so ist, kann unsere Methode areLighterThan(…) demonstrieren. Statt

boolean areLighterThan( List<? extends Portable> list, double maxWeight )

hätten wir auch einen formalen Typparameter lokal für die Methode deklarieren können:

<T extends Portable> boolean areLighterThan( List<T> list, double maxWeight )

Beide Varianten erfüllen den gleichen Zweck. Doch ist die erste Variante der zweiten vorzuziehen.

Best Practice

Immer dann, wenn der formale Typparameter (etwa T) nur in der Signatur auftaucht – die Signatur ergibt sich auf dem Methodennamen, Parameterliste, Ausnahmen – und es in der Methode selbst keinen Rückgriff auf den Typ T gibt, wähle die Variante mit der Wildcard.

Mit Typparametern lassen sich gut Abhängigkeiten zwischen den einzelnen Argumenten oder dem Rückgabetyp herstellen. Das zeigt das folgende Beispiel (mit einigen Methoden, die bisher noch nicht vorgestellt wurden), das das leichteste Objekt in einer Sammlung von Taschen zurückgeben soll:

Listing 10.19com/tutego/insel/generic/PortableDemo.java, PortableUtils

public static <T extends Portable> T lightest( Collection<T> collection ) {

Iterator<T> iterator = collection.iterator();

T lightest = iterator.next();



while ( iterator.hasNext() ) {

T next = iterator.next();



if ( next.getWeight() < lightest.getWeight() )

lightest = next;

}



return lightest;

}

Der Compiler achtet darauf, dass der Typ der Rückgabe mit dem Typ der Sammlung übereinstimmt.

Auf Bounded-Wildcard-Typen in Rückgaben verzichten

Wenn es möglich ist, Bounded-Wildcard-Typen oder Bounded-Typvariablen zu nutzen, sind Bounded-Typvariablen immer vorzuziehen – es sei denn, es greift die Best Practice. Wildcard-Typen liefern keine Typinformation, und es ist immer besser, sich vom Compiler über die Typ-Inferenz einen genaueren Typ geben zu lassen.

Nehmen wir eine statische Methode leftSublist(…) an, die von einer Liste eine Unterliste zurückgibt. Die Unterliste geht von der ersten Position bis zur Hälfte.

Versuch 1:

public static List<?> leftSublist( List<? extends Portable> list ) {

return list.subList( 0, list.size() / 2 );

}

Der Rückgabetyp List<?> ist so ziemlich der schlechteste, den wir wählen können, denn der Aufrufer der Methode kann mit der Rückgabe überhaupt nichts anfangen: Er weiß nichts über den Inhalt der Liste.

Versuch 2:

public static List<? extends Portable> leftSublist( List<? extends Portable> list )

Das ist schon ein wenig besser, denn hier bekommt der Empfänger wenigstens die Information zurück, dass die Liste irgendwelche tragbaren Dinge enthält.

Noch besser ist natürlich, auf die Typ-Inferenz des Compilers zu setzen und dem Aufrufer genau den Typ wieder zurückzugeben, mit dem er den Parametertyp spickte. Dazu müssen wir aber eine Typvariable einsetzen. Der Grund ist: Deklariert eine Methode Parameter oder eine Rückgabe mit mehreren Wildcard-Typen, so sind die wirklichen Typargumente völlig frei wählbar und ohne Zusammenhang.

Listing 10.20com/tutego/insel/generic/PortableDemo.java, PortableUtils

public static <T extends Portable> List<T> leftSublist( List<T> list ) {

return list.subList( 0, list.size() / 2 );

}

Nun ist der Typ der Liste, die reinkommt, gleich dem Typ der Liste, die rauskommt. Mit extends ist die Liste zwar nur lesbar, aber das liegt in der Natur der Sache.

[»]Hinweis

Insbesondere in der Klasse Collections aus der Java-Standard-API könnten viele Methoden auch anders geschrieben werden. Ein Beispiel: Statt <T extends E> boolean addAll(Collection<T> c) wählten die Autoren boolean addAll(Collection<? extends E> c).

 

Zum Seitenanfang

10.5.6Das LESS-Prinzip Zur vorigen ÜberschriftZur nächsten Überschrift

Während die mit extends eingeschränkten Familien Leseoperationen zulassen, gilt für super das Gegenteil. Hier ist Lesen nicht erlaubt, aber Schreiben. Als Merkhilfe lässt sich das als LESS-Prinzip[ 204 ](Im Englischen ist auch der Ausdruck PECS (producer-extends, consumer-super) in Umlauf. ) festhalten:

Lesen = Extends, Schreiben = Super (LESS)

Ein Beispiel ist auch hier hilfreich. Eine statische Methode copyLighterThan(…) soll nur die Elemente aus einer Liste in eine andere kopieren, die leichter als eine bestimmte Obergrenze sind. Der erste Versuch:

public static void copyLighterThan( List<? extends Portable> src,

List<? extends Portable> dest, double maxWeight ) {

for ( Portable portable : src )

if ( portable.getWeight() < maxWeight )

dest.add( portable ); // inline Compilerfehler !!

}

Auf den ersten Blick sieht es gut aus, aber das Programm lässt sich nicht übersetzen. Das Problem ist die Anweisung dest.add(portable). Wir erinnern uns: Mit einer Upper-bounded Wildcard lässt sich nicht schreiben. Das ergibt Sinn, denn die Liste src kann ja zum Beispiel eine Liste von Cup-Objekten sein und dest eine Liste von Pen-Objekten. Beide sind Portable, aber dennoch inkompatibel, da Cups nicht in Pens kopiert werden können. Die Frage ist also, wie der Typ der Ergebnisliste aussehen soll. Beginnen wir bei der Quellliste. Hier ist List <? extends Portable> schon korrekt, denn die Liste kann ja alles enthalten, was tragbar ist. Doch welche Anforderungen gibt es an die Zielliste? Wie muss der Typ sein, sodass sich alles vom Typ Portable, wie Cup oder Pen, oder sogar noch Unterklassen speichern lassen? Die Antwort ist einfach: Jeder Typ, der über Portable liegt! Das sind Portable selbst und Object, also alle Obertypen. Dies ist aber der Lower-bounded-Wildcard-Typ, den wir mit super schreiben. Damit folgt:

Listing 10.21com/tutego/insel/generic/PortableDemo.java, PortableUtils

public static void copyLighterThan( List<? extends Portable> src,

List<? super Portable> dest, double maxWeight ) {

for ( Portable portable : src )

if ( portable.getWeight() < maxWeight )

dest.add( portable );

}

Ein Beispiel für den Aufruf:

Listing 10.22com/tutego/insel/generic/PortableDemo.java, Ausschnitt main()

List<? extends Portable> src = Arrays.asList( pen, cup );

List<? super Portable> dest = new ArrayList<>();

PortableUtils.copyLighterThan( src, dest, 20 );

System.out.println( dest.size() ); // 1

Object result = dest.get( 0 );

System.out.println( result ); // com.tutego.insel.generic.Pen[weight=10.0]

Die Liste dest ist schreibbar, aber der lesbare Typ ist lediglich Object – der Compiler weiß nicht, was hier tatsächlich in der Liste steckt, er weiß nur, dass es beliebige Obertypen von Portable sein können. Und da bleibt als allgemeinster Typ eben nur Object.

Wildcard-Capture

Das LESS-Prinzip hat eine wichtige Konsequenz, die insbesondere bei Listenoperationen auffällt. Eine mit einer Wildcard parametrisierte Liste kann nicht verändert werden. Doch wie lässt sich zum Beispiel eine Methode schreiben, die eine Liste umdreht? Vom API-Design her könnte eine Methode reverse(…) wie folgt aussehen:

public static void reverse( List<?> list );

Oder so:

public static <T> void reverse( List<T> list );

Nach unserem Verständnis, dass wir bei völlig freien Typen die Wildcard-Schreibweise bevorzugen wollen, stehen wir vor einem Dilemma:

public static <T> void reverse( List<?> list ) {

for ( int i = 0; i < list.size() / 2; i++ ) {

int j = list.size() – i – 1;

? tmp = list.get( i ); // inline Compilerfehler

list.set( i, list.get( j ) );

list.set( j, tmp );

}

}

Es bleibt uns nichts anderes, als doch die Variante mit der Typvariablen zu wählen, sodass wir Zugriff auf den Typ T haben.

Da nun vom API-Design reverse(List<?> list) bevorzugt wird, aber reverse(List<T> list) in der Implementierung nötig ist, stellt sich die Frage, ob beides miteinander vereinbar ist. Die gute Nachricht: Ja, mit einem Trick, denn reverse(List<?> list) kann auf eine interne Umdrehmethode reverse_(List<T>) weiterleiten. Zwar müssen die Methoden anders benannt werden, aber wegen des so genannten Wildcard-Captures funktioniert die Abbildung von einer Wildcard auf eine Typvariable.

Listing 10.23com/tutego/insel/generic/WildcardCapture, WildcardCapture

public class WildcardCapture {



private static <T> void reverse_( List<T> list ) {

for ( int i = 0; i < list.size() / 2; i++ ) {

int j = list.size() – i – 1;

T tmp = list.get( i );

list.set( i, list.get( j ) );

list.set( j, tmp );

}

}



public static void reverse( List<?> list ) {

reverse_( list );

}

}

Der Compiler »fängt« bei reverse(list) den unbekannten Typ der Liste ein und »füllt« die Typvariable bei reverse_(list).

 

Zum Seitenanfang

10.5.7Enum<E extends Enum<E>> * Zur vorigen ÜberschriftZur nächsten Überschrift

Die generische Deklaration der Klasse Enum besitzt eine Besonderheit, die wir uns kurz vornehmen wollen:

public abstract class Enum<E extends Enum<E>>

implements Comparable<E>, Serializable

Ein konkreterer parametrisierter Typ muss also die Typvariable E so wählen, dass sie einen Untertyp von Enum beschreibt.

Das Ganze lässt sich am besten an einem Beispiel erklären. Die Klasse Enum ist eine besondere Klasse, die der Compiler immer dann verwendet, wenn er eine enum-Aufzählung umsetzen soll. Angenommen, Page deklariert zwei Seitengrößen:

public enum Page { A4, A3 }

Ohne dass wir genau auf die Methodenrümpfe schauen, generiert der Compiler folgenden Programmcode:

public final class Page extends java.lang.Enum<Page> {

public static final Page A4 = ...

public static final Page A3 = ...

public static Page[] values() { ... }

public static Page valueOf(String s) { ... }

...

}

Aus einem Aufzählungstyp entsteht also eine Klasse, die Enum erweitert und als parametrisierter Typ genau diese Klasse nennt: Page extends Enum<Page>. Vergleichen wir das mit der generischen Typdeklaration Enum<E extends Enum<E>>, so ist der Typparameter Page eine Instanziierung der Typvariablen E. Und Page ist eine Unterklasse von Enum (Page extends Enum), genauso wie die Typvariable E das mit dem Typparameter-Bound vorschreibt: E extends Enum.

Was wir bisher gesehen haben, zeigt, dass die Deklaration »passt«. Aber warum ist sie so gewählt? Die Typvariable E ist so deklariert, dass sie für Enum-Unterklassen steht, also für die konkrete Aufzählung selbst, wie es Page zeigt. Das ist wichtig für Vergleiche. Dazu schauen wir uns einen Ausschnitt aus der Deklaration der abstrakten Klasse Enum noch einmal an, und zwar genau die Teile, die etwas mit dem Typ E einfordern; das sind zwei Methoden:

Listing 10.24java/lang/Enum.java, Ausschnitt

public abstract class Enum<E extends Enum<E>>

implements Comparable<E>, Serializable {

public final int compareTo(E o) { … }

public final Class<E> getDeclaringClass() { … }

...

}

Bleiben wir bei der Vergleichsmethode: compareTo(…) ermöglicht es, dass wir zwei Aufzählungen vergleichen und zum Beispiel A4.compareTo(A3) schreiben können. Java erlaubt dabei nur, dass zwei Aufzählungen vom gleichen Typ verglichen werden können: Vergleiche der Art A4.compareTo(Thread.State.NEW) führen zu einem Compilerfehler. Damit sind wir der Lösung schon nah. Die Deklaration der compareTo(…)-Methode befindet sich in Enum und wird den Unterklassen vererbt – die Methode wird nicht vom Compiler magisch in die Unterklassen gesetzt, wie etwa values() oder valueOf(String). Damit bei compareTo(E o) jetzt nur eine Unterklasse von Enum, nämlich die konkrete Aufzählung, erlaubt ist, fordert Enum eben E extends Enum<E>.

Die abschließende Frage ist, ob auch eine andere Deklaration für Enum möglich gewesen wäre, ohne dass es zu einem Nachteil kommen würde. Die Antwort ist: Ja, im Prinzip ist auch class Enum<E> möglich. Auf den ersten Blick scheint das aber falsch zu sein. Spielen wir diese Deklaration statt Enum<E extends Enum<E>> kurz durch. Dann könnte ein Entwickler schreiben: class Page extends Enum<Bunny> – die geerbte Vergleichsmethode von Page hieße dann compareTo(Bunny o), was falsch wäre. Mit der korrekten Deklaration Enum<E extends Enum<E>> ist nur ein class Page extends Enum<Page> möglich.

Jetzt kommt aber die große Einschränkung: Wir dürfen keine Unterklassen von Enum aufbauen, sondern nur der Compiler darf das tun. Ein eigenmächtiger Versuch wird vom Compiler abgestraft. Der unfehlbare Compiler könnte mit einer Deklaration class Enum<E> arbeiten, denn er würde für E den Aufzählungstyp einsetzen, also Programmcode für class Page extends Enum<Page> generieren. So stände in compareTo(…) der richtige Typ, denn E wäre mit Page instanziiert, was zu dem gewollten compareTo(Page o) führt. Und auch die in Enum deklarierte Methode getDeclaringClass() liefert Page. Einschränkungen der möglichen Typparameter helfen Entwicklern, Typfehler zu minimieren, aber der Compiler macht keine Fehler, für ihn ist die Präzisierung nicht nötig. Aber es gibt für die Java-API-Designer keinen Grund, Enum schwächer zu deklarieren als nötig. Außerdem gibt es einen Unterschied im Bytecode, der sich durch die Typlöschung ergibt: Bei Enum<E> ist die Umsetzung von E getDeclaringClass() im Bytecode nur Object getDeclaringClass(), doch mit Enum<E extends Enum<E>> ist sie immerhin Enum getDeclaringClass(), was besser ist.

 


Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
 Zum Katalog
Zum Katalog: Java ist auch eine Insel Java ist auch eine Insel

Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?

Ihre Meinung



 Buchempfehlungen
Zum Katalog: Java ist auch eine Insel

Java ist auch eine Insel




Zum Katalog: Java SE 9-Standard-Bibliothek

Java SE 9-Standard-Bibliothek




Zum Katalog: Professionell entwickeln mit Java EE 8

Professionell entwickeln mit Java EE 8




Zum Katalog: Entwurfsmuster

Entwurfsmuster




Zum Katalog: IT-Projektmanagement

IT-Projektmanagement




 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2017

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.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de