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    Generics<T> Zur vorigen ÜberschriftZur nächsten Überschrift

»Irdisches Glück heißt: Das Unglück besucht uns nicht zu regelmäßig.«

– Karl Gutzkow (1811–1878)

 

Zum Seitenanfang

11.1    Einführung in Java Generics Zur vorigen ÜberschriftZur nächsten Überschrift

Generics zählen zu den komplexesten Sprachkonstrukten in Java. Wir wollen uns Generics in zwei Schritten nähern: von der Seite des Nutzers und von der Seite des API-Designers. Das Nutzen von generisch deklarierten Typen ist deutlich einfacher, sodass wir diese niedrig hängende Frucht zuerst pflücken wollen.

 

Zum Seitenanfang

11.1.1    Mensch versus Maschine – Typprüfung des Compilers und der Laufzeitumgebung Zur vorigen ÜberschriftZur nächsten Überschrift

Eine wichtige Eigenschaft von Java ist, dass der Compiler die Typen prüft und so weiß, welche Eigenschaften vorhanden sind und welche nicht. Hier unterscheidet sich Java von dynamischen Programmiersprachen wie Python oder PHP, die erst spät eine Prüfung zur Laufzeit vornehmen.

In Java gibt es zwei Instanzen, die die Typen prüfen, und diese sind unterschiedlich schlau. Wir haben die JVM mit der absoluten Typintelligenz, die unsere Anwendung ausführt und als letzte Instanz prüft, ob wir ein Objekt nicht einem falschen Typ zuweisen. Dann haben wir noch den Compiler, der zwar gut prüft, aber teilweise etwas zu gutgläubig ist und dem Entwickler folgt. Macht der Entwickler Fehler, kann dieser Fehler die JVM ins Verderben stürzen und zu einer Exception führen. Alles hat mit der expliziten Typumwandlung zu tun.

Ein zunächst unkompliziertes Beispiel:

Object o = "String";

String s = (String) o;

Dem Compiler wird über die explizite Typumwandlung das Object o als ein String verkauft. Das ist in Ordnung, weil ja o tatsächlich ein String-Objekt referenziert. Problematisch wird es, wenn der Typ nicht auf String gebracht werden kann, wir dem Compiler aber eine Typumwandlung anweisen:

Object o = Integer.valueOf( 42 );       // oder mit Autoboxing: Object o = 42;

String s = (String) o;

Der Compiler akzeptiert die Typumwandlung, und es folgt kein Fehler zur Übersetzungszeit. Es ist jedoch klar, dass diese Anpassung von der JVM nicht durchgeführt werden kann – daher folgt zur Laufzeit eine ClassCastException, da eben ein Integer nicht auf String gebracht werden kann.

Bei Generics geht es nun darum, dem Compiler mehr Informationen über die Typen zu geben und ClassCastException-Fehler zu vermeiden.

 

Zum Seitenanfang

11.1.2    Raketen Zur vorigen ÜberschriftZur nächsten Überschrift

In unseren vorangegangenen Beispielen drehte sich alles um Spieler und in einem Raum platzierte Spielobjekte. Stellen wir uns vor, der Spieler hat eine Rakete (engl. rocket), die etwas transportiert. Da nicht bekannt ist, was genau die Rakete trägt, müssen wir einen Basistyp nehmen, der alle möglichen Objekttypen repräsentiert. Das soll in unserem ersten Beispiel der allgemeinste Basistyp Object sein, sodass der Benutzer alles mit seiner Rakete transportieren kann:[ 208 ](Primitive Datentypen können über Wrapper-Objekte gespeichert werden. )

Listing 11.1    src/main/java/com/tutego/insel/nongeneric/Rocket.java, Rocket

public class Rocket {

private Object value;

public Rocket() {}

public Rocket( Object value ) { this.value = value; }

public void set( Object value ) { this.value = value; }

public Object get() { return value; }

public boolean isEmpty() { return value == null; }

public void empty() { value = null; }

}

Es gibt einen Standard- sowie einen parametrisierten Konstruktor. Mit set(…) lassen sich Objekte in die Rakete setzen und über die Zugriffsmethode get() wieder auslesen.

Geben wir einem Spieler eine rechte und eine linke Rakete, damit er genug Schub hat:

Listing 11.2    src/main/java/com/tutego/insel/nongeneric/Player.java, Player

public class Player {

public String name;

public Rocket rightRocket;

public Rocket leftRocket;

}

Zusammen mit einem Spieler, der eine rechte und eine linke Rakete hat, ist ein Beispiel schnell geschrieben. Unser Spieler michael soll in beiden Raketen Zahlen transportieren. Dann wollen wir sehen, in welcher Rakete er die größere Zahl versteckt hat.

Listing 11.3    src/main/java/com/tutego/insel/nongeneric/PlayerRocketDemo.java, main()

Player michael = new Player();

michael.name = "Omar Arnold";

Rocket rocket = new Rocket();

Long aBigNumber = 11111111111111L;

rocket.set( aBigNumber ); // (1)

michael.leftRocket = rocket;

michael.rightRocket = new Rocket( 2222222222222222222L );



System.out.println( michael.name + " transportiert in der Rakete " +

michael.leftRocket.get() + " und " + michael.rightRocket.get() );



Long val1 = (Long) michael.leftRocket.get(); // (2)

Long val2 = (Long) michael.rightRocket.get();



System.out.println( val1.compareTo( val2 ) > 0 ? "Links" : "Rechts" );

Das Beispiel hat keine besonderen Fallen, allerdings fallen zwei Dinge auf, die prinzipiell unschön sind. Beide haben damit zu tun, dass die Klasse Rocket mit dem Typ Object zum Speichern der Raketeninhalte sehr allgemein deklariert wurde und alles aufnehmen kann:

  • Beim Initialisieren wäre es gut, zu sagen, dass die Rakete nur einen bestimmten Typ (etwa Long) aufnehmen kann. Wäre eine solche Einschränkung möglich, dann lassen sich wie in Zeile (1) auch wirklich nur Long-Objekte in die Rakete setzen und nichts anderes, etwa Integer-Objekte.

  • Beim Entnehmen (2) des Rakenteninhalts mit get() müssen wir uns daran erinnern, was wir hineingelegt haben. Fordern Datenstrukturen besondere Typen, dann sollte dies auch dokumentiert sein. Doch wenn der Compiler wüsste, dass in der Rakete auf jeden Fall ein Long gespeichert ist, dann könnte die Typumwandlung wegfallen, und der Programmcode wäre kürzer. Auch könnte uns der Compiler warnen, wenn wir versuchen würden, das Long als Integer aus der Rakete zu ziehen. Unser Wissen möchten wir gerne dem Compiler geben! Denn wenn in der Rakete ein Long-Objekt ist, wir es aber als Integer annehmen und eine explizite Typumwandlung auf Integer setzen, meldet der Compiler zwar keinen Fehler, aber zur Laufzeit gibt es eine böse ClassCastException.

Um es auf den Punkt zu bringen: Der Compiler berücksichtigt im oberen Beispiel die Typsicherheit nicht ausreichend. Explizite Typumwandlungen sind in der Regel unschön und sollten vermieden werden. Aber wie können wir die Raketen typsicher machen?

Eine Lösung wäre, eine neue Klasse für jeden in der Rakete zu speichernden Typ zu deklarieren, also einmal eine RocketLong für den Datentyp long, dann vielleicht RocketInteger für int, RocketString für String usw. Das Problem bei diesem Ansatz ist, dass viel Code kopiert wird – fast identischer Code. Das ist keine vernünftige Lösung; wir können nicht für jeden Datentyp eine neue Klasse schreiben, wobei die Logik die gleiche bleibt. Wir wollen wenig schreiben, aber Typsicherheit beim Compilieren bekommen und nicht erst die Typsicherheit zur Laufzeit, wo uns vielleicht eine ClassCastException überrascht. Es wäre gut, wenn wir den Typ bei der Deklaration frei, allgemein, also »generisch« halten können, und sobald wir die Rakete benutzen, den Compiler dazu bringen könnten, auf diesen dann angegebenen Typ zu achten und die Korrektheit der Nutzung sicherzustellen.

Die Lösung für dieses Problem heißt Generics.[ 209 ](In C++ werden diese Typen von Klassen parametrisierte Klassen oder Templates (Schablonen) genannt. ) Sie bietet Entwicklern ganz neue Möglichkeiten, um Datenstrukturen und Algorithmen zu programmieren, die von einem Datentyp unabhängig, somit generisch sind.

 

Zum Seitenanfang

11.1.3    Generische Typen deklarieren Zur vorigen ÜberschriftZur nächsten Überschrift

Wollen wir Rocket in einen generischen Typ umbauen, so müssen wir an den Stellen, an denen Object vorkam, einen Typstellvertreter, einen sogenannten Typparameter, einsetzen, der durch eine Typvariable repräsentiert wird. Der Name der Typvariablen muss in der Klassendeklaration angegeben werden.

Die Syntax für den generischen Typ von Rocket ist folgende:

Listing 11.4    src/main/java/com/tutego/insel/generic/Rocket.java, Rocket

public class Rocket<T> {

private T value;

public Rocket() {}

public Rocket( T value ) { this.value = value; }

public void set( T value ) { this.value = value; }

public T get() { return value; }

public boolean isEmpty() { return value == null; }

public void empty() { value = null; }

}

Wir haben die Typvariable T definiert und verwenden sie jetzt anstelle von Object in der Rocket-Klasse.

Bei generischen Typen steht die Angabe der Typvariablen nur einmal zu Beginn der Klassendeklaration in spitzen Klammern hinter dem Klassennamen. Der Typparameter kann nun fast[ 210 ](T t = new T(); ist zum Beispiel nicht möglich. ) überall dort genutzt werden, wo auch ein herkömmlicher Typ stand. In unserem Beispiel ersetzen wir direkt Object durch T, und fertig ist die generische Klasse.

[»]  Namenskonvention

Typparameter sind in der Regel einzelne Großbuchstaben wie T (steht für Typ), E (Element), K (Key/Schlüssel) oder V (Value/Wert). Sie sind nur Platzhalter und keine wirklichen Typen. Möglich wäre etwa auch Folgendes, doch davon ist absolut abzuraten, da Elf viel zu sehr nach einem echten Klassentyp als nach einem Typparameter aussieht:

public class Rocket<Elf> {

private Elf value;

public void set( Elf value ) { this.value = value; }

public Elf get() { return value; }

}

Es dürfen nicht nur Elfen in die Klasse, sondern alle Typen.

Wofür Generics noch gut sind

Es gibt eine ganze Reihe von Beispielen, in denen Speicherstrukturen wie unsere Rakete nicht nur für einen Datentyp Long sinnvoll sind, sondern grundsätzlich für alle Typen, wobei aber die Implementierung (relativ) unabhängig vom Typ der Elemente ist. Das gilt zum Beispiel für einen Sortieralgorithmus, der mit der Ordnung der Elemente arbeitet. Wenn ein Element größer, kleiner oder gleich einem anderen ist, muss ein Algorithmus lediglich diese Ordnung nutzen können. Es ist dabei egal, ob es Zahlen vom Typ Long, Double oder auch Strings oder Kunden sind – der Algorithmus selbst ist davon nicht betroffen. Der häufigste Einsatz von Generics sind Container, die typsicher gestaltet werden sollen.

[»]  Geschichtsstunde

Die Idee, Generics in Java einzuführen, ist schon älter und geht auf das Projekt Pizza bzw. das Teilprojekt GJ (A Generic Java Language Extension) von Martin Odersky (der auch der Schöpfer der Programmiersprache Scala ist), Gilad Bracha, David Stoutamire und Philip Wadler zurück. GJ wurde dann die Basis des JSR 14, »Add Generic Types To The Java Programming Language«.

 

Zum Seitenanfang

11.1.4    Generics nutzen Zur vorigen ÜberschriftZur nächsten Überschrift

Um die neue Rocket-Klasse nutzen zu können, müssen wir sie zusammen mit einem Typargument angeben. Dadurch entstehen parametrisierte Typen:

Listing 11.5    src/main/java/com/tutego/insel/generic/RocketPlayer.java, main(), Teil 1

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

Rocket<String> stringRocket = new Rocket<String>();

Der konkrete Typ steht immer in spitzen Klammern hinter dem Klassen-/Schnittstellennamen.[ 211 ](Dass auch XML in spitzen Klammern daherkommt und XML als groß und aufgebläht gilt, wollen wir nicht als Parallele zu Javas Generics sehen. ) Die Rakete intRocket ist eine Instanz eines generischen Typs mit dem konkreten Typargument Integer. Diese Rakete kann jetzt offiziell nur Integer-Werte enthalten, und die Rakete stringRocket enthält nur Zeichenketten. Das prüft der Compiler auch, und wir benötigen keine Typumwandlung mehr:

Listing 11.6    src/main/java/com/tutego/insel/generic/RocketPlayer.java, main(), Teil 2

intRocket.set( 1 );

int x = intRocket.get(); // Keine Typumwandlung mehr nötig

stringRocket.set( "Selbstzerstörungsauslösungsschalterhintergrundbeleuchtung" );

String s = stringRocket.get();

Der Entwickler macht so im Programmcode sehr deutlich, dass die Raketen einen Integer enthalten und nichts anderes. Da Programmcode häufiger gelesen als geschrieben wird, sollten Autoren immer so viele Informationen wie möglich über den Kontext in den Programmcode legen. Zwar leidet die Lesbarkeit etwas, da insbesondere beim Instanziieren der Typ sowohl rechts wie auch links angegeben werden muss und die Syntax bei geschachtelten Generics lang werden kann. Doch wie wir in Abschnitt 11.1.5, »Diamonds are forever«, sehen werden, lässt sich das abkürzen.

Das Schöne an der Typsicherheit ist, dass alle Eigenschaften mit dem angegebenen Typ geprüft werden. Wenn wir z. B. aus intRocket mit get() auf das Element zugreifen, ist es vom Typ Integer (und durch Unboxing gleich int), und set(…) erlaubt auch nur ein Integer. Das macht den Programmcode robuster und durch den Wegfall der Typumwandlungen kürzer und lesbarer.

[»]  Keine Primitiven

Typargumente können in Java Klassen, Schnittstellen, Aufzählungen oder Arrays davon sein, aber keine primitiven Datentypen. Das schränkt die Möglichkeiten zwar ein, doch da es Autoboxing gibt, lässt sich damit leben. Und wenn null in der Rocket<Integer> liegt, führt ein Unboxing zur Laufzeit zur NullPointerException.

Begriff

Beispiel

generischer Typ (engl. generic type)

Rocket<T>

Typparameter oder Typvariable (engl. formal type parameter)

T

parametrisierter Typ (engl. parameterized type)

Rocket<Long>

Typargument (engl. actual type parameter)

Long

Originaltyp (engl. raw type)

Rocket

Tabelle 11.1    Zusammenfassung der bisherigen Generics-Begriffe

Geschachtelte Generics

Ist ein generischer Typ wie Rocket<T> gegeben, gibt es erst einmal keine Einschränkung für T. So beschränkt sich T nicht auf einfache Klassen- oder Schnittstellentypen, sondern kann auch wieder ein generischer Typ sein. Das ist logisch, denn jeder generische Typ ist ja ein eigenständiger Typ, der (fast) wie jeder andere Typ genutzt werden kann:

Listing 11.7    src/main/java/com/tutego/insel/generic/RocketPlayer.java, main(), Teil 3

Rocket<Rocket<String>> rocketOfRockets = new Rocket<Rocket<String>>();

rocketOfRockets.set( new Rocket<String>() );

rocketOfRockets.get().set( "Inner Rocket<String>" );

System.out.println( rocketOfRockets.get().get() ); // Inner Rocket<String>

Hier enthält die Rakete eine »Innenrakete«, die eine Zeichenkette "Inner Rocket<String>" speichert. Bei Dingen wie diesen ist schnell offensichtlich, wie hilfreich Generics für den Compiler (und uns) sind. Ohne Generics sähen eben alle Raketen gleich aus.

Präzise mit Generics

Unpräzise ohne Generics

Rocket<String> stringRocket;

Rocket stringRocket;

Rocket<Integer> intRocket;

Rocket intRocket;

Rocket<Rocket<String>> rocketOfRockets;

Rocket rocketOfRockets;

Tabelle 11.2    Präzisierung durch Generics

Nur ein gut gewählter Name und eine präzise Dokumentation können bei nichtgenerisch deklarierten Variablen helfen. Vor der Einführung von Generics behalfen sich Entwickler damit, mithilfe eines Blockkommentars Typinformationen anzudeuten, zum Beispiel in Rocket/*<String>*/ stringRocket.

[»]  Keine Arrays von parametrisierten Typen

Die folgende Anweisung bereitet eine einzige Rakete mit einem Array von Strings vor:

Rocket<String[]> rocketForArray = new Rocket<String[]>();

Aber lässt sich auch ein Array von mehreren Raketen deklarieren, die jeweils Strings enthalten? Ja. Doch während die Deklaration noch möglich ist, ist die Initialisierung ungültig:

Rocket<String>[] arrayOfRocket;

arrayOfRocket = new Rocket<String>[2]; // inline image Compilerfehler

Der Grund liegt in der Umsetzung in Bytecode verborgen, und die beste Lösung ist, die komfortablen Datenstrukturen aus dem java.util-Paket zu nutzen. Für Entwickler ist ein List<Rocket<String>> sowieso nützlicher als ein Rocket<String>[]. Laufzeiteinbußen sind kaum zu erwarten. Da Arrays aber vom Compiler automatisch bei variablen Argumentlisten eingesetzt werden, gibt es ein Problem, wenn die Parametervariable eine Typvariable ist. Bei Signaturen wie f(T... params) hilft die Annotation @SafeVarargs, die Compilermeldung zu unterdrücken.

 

Zum Seitenanfang

11.1.5    Diamonds are forever Zur vorigen ÜberschriftZur nächsten Überschrift

Bei der Initialisierung einer Variablen, deren Typ generisch ist, fällt auf, dass das Typargument zweimal angegeben werden muss. Bei geschachtelten Generics ist die Mehrarbeit unangenehm. Nehmen wir eine Liste, die Maps enthält, wobei der Assoziativspeicher Datumswerte mit Strings verbindet:

List<Map<Date,String>> listOfMaps;

listOfMaps = new ArrayList<Map<Date,String>>();

Das Typargument Map<Date, String> steht einmal auf der Seite der Variablendeklaration und einmal hinter dem Schlüsselwort new.

Der Diamantoperator

Verfügt der Compiler über alle Typinformationen, so können hinter new die generischen Typargumente entfallen, und es bleibt lediglich ein Pärchen spitzer Klammern:

[zB]  Beispiel

Statt

List<Map<Date,String>> listOfMaps = new ArrayList<Map<Date,String>>();

ist Folgendes möglich:

List<Map<Date,String>> listOfMaps = new ArrayList<>();

Dass der Compiler die Typen aus dem Kontext ableiten kann, geht auf eine Compilereigenschaft zurück, die Typ-Inferenz (engl. type inference) heißt – sie wird uns noch einmal begegnen. Wegen des Aussehens der spitzen Klammern <> nennt sich der Typ, für den die spitzen Klammern stehen, auch Diamanttyp (engl. diamond type). Das Pärchen <> wird auch Diamantoperator (engl. diamond operator) genannt. Es ist ein Operator, weil er den Typ herausfindet, weshalb er auch Diamant-Typ-Inferenz-Operator genannt wird.

[»]  Randnotiz

Es ist ungewöhnlich, dass der Java-Compiler hier den Typ der linken Seite betrachtet – denn bei long val = 10000000000; macht er das auch nicht. Doch darüber müssen wir uns keine so großen Gedanken machen, denn dies ist nicht das einzige Problem in der Java-Grammatik …

Einsatzgebiete des Diamanten

Der Diamant in unserem Beispiel ersetzt das gesamte Typargument Map<Date,String>. Es ist nicht möglich, ihn nur zum Teil bei geschachtelten Generics einzusetzen. So schlägt new ArrayList<Map<>>() fehl. Auch ist nur bei new der neue Diamant-Operator erlaubt, und es wäre falsch, ihn auch auf der linken Seite bei der Variablendeklaration einzusetzen und ihn etwa auf der rechten Seite bei der Bildung des Exemplars zu nutzen. Eine Deklaration wie List<> listOfMaps; führt somit zum Compilerfehler, denn der Compiler würde nicht bei jeder folgenden Nutzung irgendwelche Typen ableiten können.

Da der Diamant bei new eingesetzt wird, kann er – bis auf einige Ausnahmen, die wir uns im folgenden Abschnitt anschauen – immer dort eingesetzt werden, wo Exemplare gebildet werden. Das nächste Nonsens-Beispiel zeigt vier Einsatzgebiete:

import java.util.*;



public class WhereToUseTheDiamond {



public static List<String> foo( List<String> list ) {

return new ArrayList<>();

}



public static void main( String[] args ) {

List<String> list = new ArrayList<>();

list = new ArrayList<>();

foo( new ArrayList<>( list ) );

}

}

Die Einsatzorte sind:

  • bei Deklarationen und der Initialisierung von Attributen und lokalen Variablen

  • bei der Initialisierung von Attributen, lokalen Variablen/Parametervariablen

  • als Argument bei Methoden-/Konstruktoraufrufen

  • bei Methodenrückgaben

Ohne Frage sind der erste und zweite Fall die sinnvollsten. Fast überall kann der Diamant die Schreibweise abkürzen. Besonders im ersten Fall spricht nichts Grundsätzliches gegen den Einsatz, bei den anderen drei Punkten muss berücksichtigt werden, ob nicht vielleicht die Lesbarkeit des Programmcodes leidet. Wenn zum Beispiel mitten in einer Methode eine Datenstruktur mit list = new ArrayList<>() initialisiert wird, aber die Variablendeklaration nicht auf der gleichen Bildschirmseite liegt, ist mitunter für den Leser nicht sofort sichtbar, was denn genau für Typen in der Liste sind.

Der Einsatz des Diamanten ist nicht immer möglich

Es gibt Situationen, in denen die Typableitung nicht so funktioniert wie erwartet. Oftmals hat das mit dem Einsatz des Diamanten bei Methodenaufrufen oder aufeinander aufbauenden Aufrufen zu tun, sodass anzuraten ist – auch schon aus Gründen der Programmverständlichkeit –, auf Diamanten zu verzichten.

[zB]  Beispiel

Für den Compiler ist das ein unlösbarer Fall:

List<String> list = new ArrayList<>().subList( 0, 1 ); 

Die Typ-Inferenz ist komplex,[ 212 ](Um den Diamanten zu testen, haben die Entwickler ein Tool geschrieben, das durch das JDK läuft und schaut, welche generisch genutzten news durch den Diamanten vereinfacht werden könnten. (Heraus kamen etwa 5.000 Stellen.) Nicht jedes Team hat jede erlaubte Konvertierung hin zum Diamanten akzeptiert. So wollte das Team, das die Java-Security-Bibliotheken pflegt, weiterhin die explizite Schreibweise der Generics bei Zuweisungen beibehalten. Als dieses Feature in Java 7 eingebaut wurde, standen zwei Algorithmen zur Typauswahl zur Auswahl: simpel und komplex. Der komplexe Ansatz bezieht neben den Typeninformationen, die eine Zuweisung liefert, noch den Argumenttyp ein. Zunächst verwendete das Team den einfachen Algorithmus, wechselte ihn jedoch später, da der komplexe Ansatz auf Algorithmen zurückgreift, die der Compiler auch an anderen Stellen einsetzt. ) und glücklicherweise muss sich ein Entwickler nicht um die interne Arbeitsweise kümmern. Wenn der Diamant wie im Beispiel nicht möglich ist, weil der Compiler ein »Type mismatch: cannot convert from List<Object> to List<String>« meldet, löst eine explizite Typangabe das Problem, also new ArrayList<String>().subList(0, 1).

Diamant vs. var

Diamant und var haben vergleichbare Aufgaben, unterscheiden sich aber durch die Quelle der Informationen. Beim Diamanten ist es zum Beispiel bei einer Zuweisung die linke Seite, die dem Compiler die Information gibt, was auf der rechten Seite der Zuweisung für ein Typ gemeint ist. Bei var wiederum ist das andersherum: Die rechte Seite hat den Kontext, und daher kann links der Variablentyp entfallen:

List<String> list1 = new ArrayList<>();  // List<String>

var list2 = new ArrayList<String>(); // ArrayList<String>

var list3 = new ArrayList<>(); // ArrayList<Object>

Im letzten Fall gibt es keinen Compilerfehler – nur ist eben nichts über das Typargument bekannt, und daher gilt Object.

Um Code abzukürzen, haben wir damit zwei Möglichkeiten: var oder Diamant.

 

Zum Seitenanfang

11.1.6    Generische Schnittstellen Zur vorigen ÜberschriftZur nächsten Überschrift

Eine Schnittstelle kann genauso als generischer Typ deklariert werden wie eine Klasse. Werfen wir einen Blick auf die Schnittstelle java.lang.Comparable und auf einen Ausschnitt von java.util.Set (das ist eine Schnittstelle, die Operationen für Mengenoperationen vorschreibt; mehr dazu folgt in Kapitel 17, »Einführung in Datenstrukturen und Algorithmen«).

Schnittstelle »Comparable«

Schnittstelle »Set«

public interface Comparable<T> { 

int compareTo(T o);

}
public interface Set<E> extends Collection<E> {

boolean add(E e);

int size();

boolean isEmpty();

boolean contains(Object o);

Iterator<E> iterator();

Object[] toArray();

<T> T[] toArray(T[] a);



}

Tabelle 11.3    Generische Deklaration der Schnittstellen »Comparable« und »Set«

Wie bekannt, greifen die Methoden auf die Typvariablen T und E zurück. Bei Set ist außerdem zu erkennen, dass sie selbst eine generisch deklarierte Schnittstelle erweitert.

Beim Einsatz von generischen Schnittstellen lassen sich die folgenden zwei Benutzungsmuster ableiten:

  • Ein nichtgenerischer Typ löst Generics bei der Implementierung auf.

  • Ein generischer Klassentyp implementiert eine generische Schnittstelle und gibt die Parametervariable weiter.

Nichtgenerischer Typ löst Generics bei der Implementierung auf

Im ersten Fall implementiert eine Klasse die generisch deklarierte Schnittstelle und gibt einen konkreten Typ an. Alle numerischen Wrapper-Klassen implementieren zum Beispiel Comparable, und das Typargument ist genau der Typ vom Wrapper:

Listing 11.8    java/lang/Integer.java, Ausschnitt

public final class Integer extends Number implements Comparable<Integer> {

public int compareTo( Integer anotherInteger ) { ... }

...

}

Durch diese Nutzung wird für den Anwender die Klasse Integer Generics-frei.

[+]  Tipp

Komplexe generische Typen lassen sich gut durch eigene Typdeklarationen vereinfachen. Anstatt zum Beispiel immer wieder HashMap<String,List<Integer>> zu schreiben, lässt sich eine Abkürzung nehmen:

class StringToIntListMap extends HashMap<String,List<Integer>> {}

Generischer Klassentyp implementiert generische Schnittstelle und gibt die Parametervariable weiter

Die Schnittstelle Set schreibt Operationen für Mengen vor. Eine Klasse, die Set implementiert, ist zum Beispiel HashSet. Der Kopf der Typdeklaration ist folgender:

public class HashSet<E>

extends AbstractSet<E>

implements Set<E>, Cloneable, java.io.Serializable

Es ist abzulesen, dass Set eine Typvariable E deklariert, die HashSet nicht konkretisiert. Der Grund ist, dass die Datenstruktur Set vom Anwender als parametrisierter Typ verwendet wird und nicht aufgelöst werden soll.

[»]  Hinweis

In manchen Situationen wird auch Void als Typargument eingesetzt. Wenn etwa interface I<T> { T foo(); } eine Typvariable T deklariert, ohne dass es bei der Implementierung von I etwas zurückzugeben gibt, dann kann das Typargument Void sein:

class C implements I<Void> {

@Override public Void foo() { return null; }

}

Allerdings sind void und Void unterschiedlich, denn bei Void muss es eine Rückgabe geben, was ein return null notwendig macht.

 

Zum Seitenanfang

11.1.7    Generische Methoden/Konstruktoren und Typ-Inferenz Zur vorigen ÜberschriftZur nächsten Überschrift

Die bisher genannten generischen Konstruktionen sahen im Kern wie folgt aus:

  • class Klassenname<T> { }

  • interface Schnittstellenname<T> { }

Eine an der Klassen- oder Schnittstellendeklaration angegebene Typvariable kann in allen nichtstatischen Eigenschaften des Typs angesprochen werden.

[zB]  Beispiel

Folgendes führt zu einem Fehler:

class Rocket<T> {

static void foo( T t ) { }; // inline image Compilerfehler

}

Der Eclipse-Compiler meldet: »Cannot make a static reference to the non-static type T«.

Doch was machen wir, wenn:

  • (statische) Methoden eine eigene Typvariable nutzen wollen?

  • unterschiedliche (statische) Methoden unterschiedliche Typvariablen nutzen möchten?

Eine Klasse kann auch ohne Generics deklariert werden, aber generische Methoden besitzen. Ganz allgemein kann jeder Konstruktor, jede Objektmethode und jede Klassenmethode einen oder mehrere Typparameter deklarieren. Die Typvariablen stehen dann nicht mehr an der Klasse, sondern an der Methoden-/Konstruktordeklaration und sind »lokal« für die Methode bzw. den Konstruktor. Das allgemeine Format ist:

Modifizierer <Typvariable(n)> Rückgabetyp Methodenname(Parameter) throws-Klausel

Ganz zufällig das eine oder andere Argument

Interessant sind generische Methoden insbesondere für Utility-Klassen, die nur statische Methoden anbieten, aber selbst nicht als Objekt vorliegen. Das folgende Beispiel zeigt das anhand einer Methode random():

Listing 11.9    src/main/java/com/tutego/insel/generic/GenericMethods.java, GenericMethods

public class GenericMethods {



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

return Math.random() > 0.5 ? m : n;

}



public static void main( String[] args ) {

String s = random( "Analogkäse", "Gel-Schinken" );

System.out.println( s );

}

}

Dabei deklariert <T> T random(T m, T n) eine generische Methode, wobei der Rückgabetyp und Parametertyp durch eine Typvariable T bestimmt werden. Die Angabe von <T> beim Klassennamen ist bei dieser Syntax entfallen und wurde auf die Deklaration der Methode verschoben.

[»]  Hinweis

Natürlich kann eine Klasse als generischer Typ und eine darin enthaltene Methode als generische Methode mit unterschiedlichem Typ deklariert werden. In diesem Fall sollten die Typvariablen unterschiedlich benannt sein, um den Leser nicht zu verwirren. So bezieht sich im Folgenden T bei sit(…) eben nicht auf die Parametervariable der Klasse Lupilu, sondern auf die der Methode:

interface Lupilu<T> { <T> void sit( T val ); }  // Verwirrend

interface Lupilu<T> { <V> void sit( V val ); } // Besser

Der Compiler auf der Suche nach Gemeinsamkeiten

Den Typ (der wichtig für die Rückgabe ist) leitet der Compiler automatisch aus dem Kontext, das heißt aus den Argumenten, ab. Diese Eigenschaft nennt sich Typ-Inferenz (engl. type inference). Das hat weitreichende Konsequenzen.

Bei der Deklaration <T> T random(T m, T n) sieht es vielleicht auf den ersten Blick so aus, als ob die Variablentypen m und n absolut gleich sein müssen. Das stimmt aber nicht, denn bei den Typen geht der Compiler in der Typhierarchie so weit nach oben, bis er einen gemeinsamen Typ findet.

Aufruf

Identifizierte Typen

Gemeinsame Basistypen

random("Essen", 1)

String, Integer

Object, Serializable, Comparable

random(1L, 1D)

Long, Double

Object, Number, Comparable

random(new Point(), new StringBuilder())

Point, StringBuilder

Object, Serializable, Cloneable

Tabelle 11.4    Gemeinsame Basistypen

Es fällt auf, aber überrascht nicht, dass Object immer in die Gruppe gehört.

Die Schnittmenge der Typen bilden im Fall von random(…) die gültigen Rückgabetypen. Erlaubt sind demnach für die Parametertypen String und Integer:

Object       s1 = random( "Essen", 1 );

Serializable s2 = random( "Essen", 1 );

Comparable s3 = random( "Essen", 1 );

Knappe Fabrikmethoden

Der Diamanttyp kürzt Variablendeklarationen mit Initialisierung einer Referenzvariablen angenehm ab. Muss bei

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

der generische Typ String zweimal angegeben werden, so erlaubt der Diamant (<>) Folgendes:

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

Mit der Typ-Inferenz gibt es eine alternative Lösung, falls wir selbst Erzeugermethoden bauen. Geben wir unserer Klasse Rocket eine Fabrikmethode

public static <T> Rocket<T> newInstance() {

return new Rocket<T>();

}

so ist folgende Alternative möglich:

Rocket<String> r = Rocket.newInstance();

Aus dem Ergebnistyp Rocket<String> leitet der Compiler das Typargument String für die Rakete ab. Und ist bei einem einfachen Typ wie String die Schreibersparnis noch gering, wird der Code bei verschachtelten Datenstrukturen kürzer. Soll die Rakete einen Assoziativspeicher aufnehmen, der eine Zeichenkette mit einer Liste von Zahlen assoziiert, so schreiben wir kompakt:

Rocket<Map<String,List<Integer>>> r = Rocket.newInstance();

Generische Methoden mit explizitem Typargument *

Es gibt Situationen, in denen der Compiler nicht aus dem Kontext über Typ-Inferenz den richtigen Typ ableiten kann. Zum Beispiel ist Folgendes nicht möglich:

boolean hasRocket = true;

Rocket<String> rocket = hasRocket ? Rocket.newInstance() : null;

Der Eclipse-Compiler meldet "Type mismatch: cannot convert from Rocket<Object> to Rocket <String>".

Die Lösung: Wir müssen bei Rocket.newInstance() das Typargument String explizit angeben:

Rocket<String> rocket = hasRocket ? Rocket.<String>newInstance() : null;

Die Syntax ist etwas gewöhnungsbedürftig, doch in der Praxis ist die explizite Angabe selten nötig.

Ein Beispiel: Ist das Argument der statischen Methode Arrays.asList(…) ein Array, dann ist das explizite Typargument nötig, da der Compiler nicht erkennen kann, ob das Array selbst das eine Element der Rückgabe-Liste ist oder ob das Array die Vararg-Umsetzung ist und alle Elemente des Arrays in die Rückgabeliste kommen:

List<String> list11 = Arrays.asList( new String[] { "A", "B" } );

List<String> list12 =

Arrays.asList( "A", "B" ); // Parameter ist als Vararg definiert

System.out.println( list11 ); // [A, B]

System.out.println( list12 ); // [A, B]

List<String> list21 = Arrays.<String>asList( new String[] { "A", "B" } );

List<String> list22 = Arrays.<String>asList( "A", "B" );

System.out.println( list21 ); // [A, B]

System.out.println( list22 ); // [A, B]

List<String[]> list31 = Arrays.<String[]>asList( new String[] { "A", "B" } );

// List<String[]> list32 = Arrays.<String[]>asList( "A", "B" );

System.out.println( list31 ); // [[Ljava.lang.String;@69b332]

Zunächst gilt es, festzuhalten, dass die Ergebnisse für list11, list12, list21 und list22 identisch sind. Der Compiler setzt ein Vararg automatisch als Array um und übergibt das Array der asList(…)-Methode. Im Bytecode sehen daher die Aufrufe gleich aus. Bei list21 und list22 ist das Typargument jeweils explizit angegeben, aber nicht wirklich nötig, da ja das Ergebnis wie list11 bzw. list12 ist. Doch das Typargument String macht deutlich, dass die Elemente im Array, also die Vararg-Argumente, Strings sind. Spannend wird es bei list31. Zunächst zum Problem: Ist new String[]{"A", "B"} das Argument einer Vararg-Methode, so ist das mehrdeutig, weil genau dieses Array das erste Element des vom Compiler automatisch aufgebauten Vararg-Arrays sein könnte (dann wäre es ein Array im Array) oder – und das ist die interne Standardumsetzung – der Java-Compiler das übergebene Array als die Vararg-Umsetzung interpretiert. Diese Doppeldeutigkeit löst <String[]>, da in dem Fall klar ist, dass das von uns aufgebaute String-Array das einzige Element eines neuen Vararg-Arrays sein muss. Und Arrays.<String[]> asList(…) stellt heraus, dass der Typ der Array-Elemente String[] ist. Daher funktioniert auch die letzte Variablendeklaration nicht, denn bei asList("A", "B") ist der Elementtyp String, aber nicht String[].

 


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