Rheinwerk Computing < openbook >


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


Download:

- Listings, ca. 2,7 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 11 Generics<T>
Pfeil 11.1 Einführung in Java Generics
Pfeil 11.1.1 Mensch versus Maschine – Typprüfung des Compilers und der Laufzeitumgebung
Pfeil 11.1.2 Raketen
Pfeil 11.1.3 Generische Typen deklarieren
Pfeil 11.1.4 Generics nutzen
Pfeil 11.1.5 Diamonds are forever
Pfeil 11.1.6 Generische Schnittstellen
Pfeil 11.1.7 Generische Methoden/Konstruktoren und Typ-Inferenz
Pfeil 11.2 Umsetzen der Generics, Typlöschung und Raw-Types
Pfeil 11.2.1 Realisierungsmöglichkeiten
Pfeil 11.2.2 Typlöschung (Type Erasure)
Pfeil 11.2.3 Probleme der Typlöschung
Pfeil 11.2.4 Raw-Type
Pfeil 11.3 Die Typen über Bounds einschränken
Pfeil 11.3.1 Einfache Einschränkungen mit extends
Pfeil 11.3.2 Weitere Obertypen mit &
Pfeil 11.4 Typparameter in der throws-Klausel *
Pfeil 11.4.1 Deklaration einer Klasse mit Typvariable <E extends Exception>
Pfeil 11.4.2 Parametrisierter Typ bei Typvariable <E extends Exception>
Pfeil 11.5 Generics und Vererbung, Invarianz
Pfeil 11.5.1 Arrays sind kovariant
Pfeil 11.5.2 Generics sind nicht kovariant, sondern invariant
Pfeil 11.5.3 Wildcards mit ?
Pfeil 11.5.4 Bounded Wildcards
Pfeil 11.5.5 Bounded-Wildcard-Typen und Bounded-Typvariablen
Pfeil 11.5.6 Das LESS-Prinzip
Pfeil 11.5.7 Enum<E extends Enum<E>> *
Pfeil 11.6 Konsequenzen der Typlöschung: Typ-Token, Arrays und Brücken *
Pfeil 11.6.1 Typ-Token
Pfeil 11.6.2 Super-Type-Token
Pfeil 11.6.3 Generics und Arrays
Pfeil 11.6.4 Brückenmethoden
Pfeil 11.7 Zum Weiterlesen
 

Zum Seitenanfang

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

11.5.1    Arrays 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 und Object[] über allen nichtprimitiven Felder liegt, nennt sich Kovarianz. Doch lässt sich das auf Generics übertragen?

 

Zum Seitenanfang

11.5.2    Generics 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 image 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 Rocket und den Wrapper-Klassen. Auch wenn Number die Oberklasse von Integer ist, so gilt dennoch nicht, dass Rocket<Number> ein Obertyp von Rocket<Integer> ist. Wäre es das, wäre Folgendes möglich und zur Laufzeit ein Problem:

Rocket<Number> r;

r = new Rocket<Integer>(); // Ist das OK?

r.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 Rocket<Integer> ja eine Rakete für Integer aufgebaut haben, und ein Double darf nicht in die Integer-Rakete. Daher folgt: Die Ableitungsbeziehung zwischen Typen überträgt sich nicht auf generische Klassen. Ein Rocket<Number> ist also keine Oberklasse, die alle erdenklichen numerischen Typen in der Rakete erlaubt. Der Compiler meckert bei diesem Versuch sofort:

Rocket<Number> r;

r = new Rocket<Integer>(); // inline image Type mismatch: cannot convert from Rocket<Integer>

// to Rocket<Number>

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

Rocket<Integer> r1 = new Rocket<>();

Rocket<Number> r2 = r1; // inline image Type mismatch: cannot convert

// from Rocket<Integer> to Rocket<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:

Rocket<Number> r = new Rocket<Integer>( 1 );

Number n = r.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 Basistyp, 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

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

Wir wollen eine Methode isOneRocketEmpty(…) schreiben, die eine variable Anzahl von Raketen bekommt – und dabei soll es egal sein, was die Rakete transportiert. Die Methode soll auch testen, ob eine Rakete leer ist. Ein Aufruf könnte so aussehen:

Rocket<String>  r1 = new Rocket<>( "Bad-Bank" );

Rocket<Integer> r2 = new Rocket<>( 1500000 );

System.out.println( isOneRocketEmpty( r1, r2 ) ); // false

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

public static boolean isOneRocketEmpty( Rocket<Object>... rockets )

Doch halt! Da Rocket<Object> nicht Raketen mit allen Typen umfasst, sondern nur exakt eine Rakete trifft, die ein Object-Objekt enthält, ist das keine sinnvolle Parametrisierung für isOneRocketEmpty(…). 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 Raketen mit allen Inhalten akzeptieren würde, so könnte einer Rakete leicht ein Wert mit falschem Typ untergeschoben werden. Wird isOneRocketEmpty(…) mit einem Rocket<String> aufgerufen, so würde wegen isOneRocketEmpty(Rocket<Object>... rockets) auch der Aufruf von set(12) auf der Rocket gültig sein, und dann stünde plötzlich statt des gewünschten Inhalts der Rakete String nun ein Integer in der Rakete. 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 isOneRocketEmpty(…) beliebige Raketeninhalte entgegennehmen, aber gleichzeitig soll es der Methode auch verboten sein, falsche Dinge in die Rakete zu setzen. Ein isOneRocketEmpty(Rocket... rockets) 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 Rocket<Object> nicht der Basistyp aller Raketeninhalte ist, dann ist es Rocket<?>. Es ist wichtig zu verstehen, dass ? nicht für Object steht, sondern für einen unbekannten Typ! Damit lässt sich isOneRocketEmpty(…) realisieren:

Listing 11.16    src/main/java/com/tutego/insel/generic/RocketsEmpty.java

public static boolean isOneRocketEmpty( Rocket<?>... rockets ) {

for ( Rocket<?> rocket : rockets )

if ( rocket.isEmpty() )

return true;



return false;

}



public static void main( String[] args ) {

Rocket<String> r1 = new Rocket<>( "Bad-Bank" );

Rocket<Integer> r2 = new Rocket<>( 1500000 );

System.out.println( isOneRocketEmpty( r1, r2 ) ); // false

System.out.println( isOneRocketEmpty( r1, r2, new Rocket<Byte>() ) ); // true

}

Dass der Aufruf von isOneRocketEmpty() bei keiner übergebenen Rakete 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!)

Rocket<?> r = new Rocket<Byte>();

Rocket<?> r = new Rocket<?>();

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

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

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

Auswirkungen auf Lese-/Schreiboperationen

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

Rocket<?>        r1 = new Rocket<Integer>();

oder

Rocket<Integer>  r2 = new Rocket<Integer>();

Rocket<?> r3 = r2;

dann ist über die wirklichen Typargumente bei r1 und r3 nichts bekannt. Das hat wichtige Auswirkungen auf die Methoden, die wir auf Rocket aufrufen können:

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

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

 

Zum Seitenanfang

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

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

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

Wildcard

Bezeichnung

Typargument

?

Wildcard-Typ

Ist beliebig.

? extends Typ

Upper-bounded Wildcard-Typ

alles, was Typ erweitert, also Untertypen, und Typ selbst

? super Typ

Lower-bounded Wildcard-Typ

alle Obertypen von Typ und Typ selbst

Tabelle 11.9    Die drei Wildcard-Typen

Eine Wildcard beschreibt also die Eigenschaft eines Typarguments. Wenn es

Rocket<? extends Number> r;

heißt, dann können in der Rakete 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 11.10    Einige 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 finden sich in den Sortiermethoden der Datenstrukturen und Algorithmen:

Beispiel

Bedeutung

Rocket<?> p;

Raketen mit beliebigem Inhalt

Rocket<? extends Number> p;

Raketen 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 11.11    Beispiel 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 Rakete. Die Raketen 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 11.17    src/main/java/com/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 11.18    src/main/java/com/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 werden. Sie kann aber nicht verändert werden.

 

Zum Seitenanfang

11.5.5    Bounded-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 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 Typparameter (etwa T) nur in der Signatur auftaucht (die Signatur ergibt sich aus dem Methodennamen, der Parameterliste und den Ausnahmen) und es in der Methode selbst keinen Rückgriff auf den Typ T gibt, sollte man die Variante mit der Wildcard wählen.

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). Es soll das leichteste Objekt in einer Sammlung von Raketen zurückgeben:

Listing 11.19    src/main/java/com/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 11.20    src/main/java/com/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

11.5.6    Das 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[ 216 ](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 image 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: Es kann jeder Typ sein, 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 11.21    src/main/java/com/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 11.22    src/main/java/com/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 image Compilerfehler

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

list.set( j, tmp );

}

}

Es bleibt uns nichts anderes übrig, 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 sogenannten Wildcard-Captures funktioniert die Abbildung von einer Wildcard auf eine Typvariable.

Listing 11.23    src/main/java/com/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

11.5.7    Enum<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 parametrisierten Typ genau diese Klasse nennt: Page extends Enum<Page>. Vergleichen wir das mit der generischen Typdeklaration Enum<E extends Enum<E>>, so ist das Typargument 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 11.24    java/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, zwei Aufzählungen zu vergleichen und zum Beispiel A4.compareTo(A3) zu schreiben. Java erlaubt dabei nur, zwei Aufzählungen vom gleichen Typ zu vergleichen: 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.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java ist auch eine Insel Java ist auch eine Insel

Jetzt Buch bestellen


 Buchempfehlungen
Zum Rheinwerk-Shop: Captain CiaoCiao erobert Java

Captain CiaoCiao erobert Java




Zum Rheinwerk-Shop: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Objektorientierte Programmierung

Objektorientierte Programmierung




 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2021

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



Cookie-Einstellungen ändern