Start / Seminare / Java SE Fortgeschrittene Sprachfeatures

Modul

Generics

7 Kapitel in diesem Modul-Video · Laufzeit

Vertonung mit synthetischer Stimme · Inhalte redaktionell verantwortet · © 2026 HECKER CONSULTING, alle Rechte vorbehalten

Transkript

Der gesprochene Text dieses Moduls zum Mitlesen, Überfliegen und Durchsuchen. Ein Klick auf einen Zeitstempel springt an die Stelle im Video.

Generics

0:00 Generics gehören zu den Themen, bei denen fast jeder Java-Entwickler sagt: Klar, kenne ich. Und bei denen fast jeder ins Stocken gerät, sobald es über List von String hinausgeht. Das ist kein Vorwurf, sondern eine Beobachtung — Generics sind das komplizierteste Sprachmittel, das Java kennt. Die gute Nachricht: Der Kerngedanke ist erstaunlich einfach, und wenn man ihn einmal sauber gefasst hat, ordnet sich der Rest von selbst.

0:25 In diesem Modul bauen wir das von Grund auf auf: eigene generische Klassen, eigene generische Methoden, und die Schranken, die daraus überhaupt erst etwas Brauchbares machen.

Generics

0:36 Sieben Kapitel liegen vor uns, und sie bauen strikt aufeinander auf. Wir beginnen mit den Begriffen, und ich bitte Sie, dieses erste Kapitel nicht als lästiges Vorgeplänkel abzutun — die saubere Trennung von Parameter und Argument trägt das gesamte Modul und macht später auch Fehlermeldungen lesbar. Danach schreiben wir eigene generische Typen, verwenden sie, erweitern sie. Dann kommen generische Methoden, deren Aufruf, und zum Schluss die Schranken.

1:01 Der Anspruch steigt von Kapitel zu Kapitel, aber jeder Schritt ist klein. Vier Ziele. Das erste ist begrifflich: Typparameter sind für Typen dasselbe wie Parameter für Werte — wenn Sie diese Analogie einmal verinnerlicht haben, ist vieles selbsterklärend. Das zweite und dritte sind handwerklich: eigene generische Typen und Methoden schreiben. Und das vierte ist das, was Generics überhaupt nützlich macht — die Schranken.

1:26 Ohne Schranke wissen Sie im Rumpf nämlich gar nichts über Ihren Typparameter und können entsprechend wenig damit anfangen. Das ist der Punkt, an dem viele hängen bleiben.

Überblick und Grundbegriffe

1:36 Fangen wir also mit den Begriffen an. Ich weiß, das ist der Teil, den man gern überspringt, um endlich zum Code zu kommen. Meine Erfahrung ist aber: Die meisten Verständnisprobleme mit Generics sind in Wahrheit Begriffsprobleme. Wer Parameter und Argument durcheinanderbringt, kann eine Fehlermeldung nicht mehr entschlüsseln — und die Meldungen des Compilers sind an dieser Stelle sehr genau.

1:59 Diese Grundlage kostet uns zehn Minuten und spart im Rest des Moduls deutlich mehr. Die Grundidee lässt sich in einem Bild fassen. Eine Variable ist ein Behälter, und ihr Typ bestimmt, was hineinpasst. Bisher war der Typ immer festgelegt — Sie schreiben String, und dann passt eben ein String hinein. Generics heben diese Idee eine Ebene höher: Was jetzt variiert, ist nicht mehr der Wert, sondern der Typ selbst. Man spricht deshalb von der Wertebene und der Typebene.

2:27 Auf der Typebene gibt es dieselben drei Rollen wie auf der Wertebene, nur eben für Typen — Variablen, Parameter und Argumente. Mehr steckt hinter dem Konzept nicht. Diese Tabelle stellt die beiden Ebenen nebeneinander, und die Symmetrie ist der ganze Punkt. Auf der Wertebene deklarieren Sie einen Parameter in der Signatur und übergeben beim Aufruf ein Argument.

2:49 Auf der Typebene deklarieren Sie einen Typparameter in spitzen Klammern und setzen bei der Verwendung ein Typargument ein. Merken Sie sich die Faustregel aus der Fußzeile: Parameter steht in der Definition, Argument steht beim Gebrauch. Wenn Sie das konsequent auseinanderhalten, werden Fehlermeldungen wie „cannot infer type arguments" plötzlich zu einer verständlichen Aussage statt zu einem Rätsel.

3:11 Ein kurzer Blick zurück, weil er das Warum erklärt. Vor Java fünf — das ist zwanzig Jahre her — konnte man einer Liste nicht ansehen, was sie enthält. Sie holten ein Objekt heraus und mussten es umwandeln, jedes Mal, überall. Und jede dieser Umwandlungen war ein Risiko: Wenn dort etwas anderes lag als erwartet, gab es zur Laufzeit eine ClassCastException.

3:33 Generics haben diese ganze Fehlerklasse abgeschafft, indem sie die Prüfung vom Betrieb in den Compiler verlagert haben. Das ist übrigens ein wiederkehrendes Muster in diesem Seminar — Fehler früher sichtbar machen. Noch zwei Begriffe, die im Alltag ständig durcheinandergehen. Ein generischer Typ ist ein Typ mit Typparametern — List ist generisch.

3:54 Ein parametrisierter Typ entsteht, wenn Sie Typargumente einsetzen — List von String ist parametrisiert. Das klingt nach Wortklauberei, hat aber einen praktischen Wert: Der Compiler unterscheidet die beiden sehr genau, und seine Meldungen tun das auch. Wenn dort von einem parametrisierten Typ die Rede ist und Sie den Unterschied kennen, wissen Sie sofort, wo Sie suchen müssen.

4:16 Das spart in der Fehlersuche mehr Zeit, als der Begriff Aufwand kostet. Diese vier Punkte sind alle begrifflicher Natur, und genau das macht sie tückisch. Wer Parameter und Argument synonym verwendet, kann Fehlermeldungen nicht mehr entschlüsseln — sie sprechen die genaue Sprache. Wer generisch und parametrisiert vermengt, sucht an der falschen Stelle.

4:37 Der dritte Punkt ist eine Vorwegnahme: Generics wirken nicht zur Laufzeit, und was das bedeutet, klären wir ausführlich in Modul acht. Und der vierte ist eine Haltungsfrage — ein Typparameter ist kein Schreibkomfort, sondern ein Vertrag über das, was hier hineinpasst.

Generische Klassen definieren

4:53 Genug Begriffe. Schreiben wir eine eigene generische Klasse — und zwar zuerst ohne Generics, so wie man es vor zwanzig Jahren getan hätte. Das ist kein Umweg, sondern der kürzeste Weg zum Verständnis: Der direkte Vergleich zeigt, was Typparameter tatsächlich leisten, und zwar deutlicher als jede Erklärung. Achten Sie dabei vor allem auf die Aufrufseite, denn dort entfaltet sich der Nutzen. Der Autor der Klasse hat kaum weniger Arbeit — die Nutzer haben deutlich weniger Ärger.

5:22 Die Mechanik ist schnell erzählt. Typparameter stehen in spitzen Klammern hinter dem Namen der Klasse, der Schnittstelle oder des Records. Innerhalb der Definition treten sie an die Stelle konkreter Typen — in Feldern, in Parametern, im Rückgabetyp. Der entscheidende Gedanke dabei: Innerhalb der Klasse ist gar nicht bekannt, um welchen Typ es sich handelt. Das ist keine Schwäche, sondern der Zweck.

5:46 Sie schreiben Code, der für jeden Typ funktioniert, weil er keine Annahmen macht. Was fehlt, ist die Möglichkeit, doch etwas anzunehmen — und dafür gibt es später die Schranken. So hätte man das vor zwanzig Jahren geschrieben: zwei Felder vom Typ Object. Funktioniert, und ist trotzdem eine schlechte Idee. Denn wer hier etwas herausholt, bekommt ein Object und muss umwandeln — bei jedem einzelnen Zugriff.

6:10 Diese Umwandlungen stehen dann überall im aufrufenden Code verstreut, und jede einzelne kann zur Laufzeit brechen. Der eigentliche Schaden ist aber nicht die Ausnahme, sondern die fehlende Information: Wer diese Klasse liest, weiß nicht, was darin liegen soll. Der Typ ist zur Kommentarsache geworden. Und hier dieselbe Sache mit Typparametern — als Record geschrieben sogar in einer Zeile.

6:34 Das ist bemerkenswert: Die generische Fassung ist kürzer als die untypisierte und zugleich sicher. Achten Sie auf die letzte Zeile im Beispiel. Dort steht keine Umwandlung mehr, und zwar nicht, weil wir sie weggelassen hätten, sondern weil sie überflüssig ist. Der Compiler weiß, dass dort ein String liegt. Das ist der Gewinn in einem Satz: Sie schreiben die Typinformation einmal hin, statt sie an jeder Zugriffsstelle wiederholen zu müssen.

7:00 Ein paar Einschränkungen gibt es. Klassen, Schnittstellen und Records dürfen Typparameter tragen — Enums und Ausnahmeklassen nicht. Bei Enums leuchtet das ein: Die Instanzen stehen ja fest, ein variabler Typ hätte dort keinen Ort. Bei Ausnahmen hängt es mit dem Fangen zur Laufzeit zusammen, und warum das nicht funktioniert, verstehen Sie nach Modul acht.

7:23 Zur Benennung: Einzelne Großbuchstaben sind Konvention, keine Vorschrift. T, U, V für beliebige Typen, E für Element, K und V für Schlüssel und Wert. Halten Sie sich daran — jeder Java-Entwickler liest das sofort. Schreiben Sie einen eigenen Behältertyp, dessen Inhaltstyp erst bei der Verwendung feststeht. Der Hinweis in der Aufgabe ist der eigentliche Kern: Schreiben Sie die Klasse zweimal, einmal mit Object und einmal generisch.

7:49 Und dann schauen Sie nicht auf die Klasse, sondern auf die Aufrufseite. Dort zeigt sich der Unterschied — bei der einen Fassung stehen Umwandlungen, bei der anderen nicht. Diese Perspektive ist wichtig, weil Generics ihren Nutzen fast immer beim Aufrufer entfalten, nicht beim Autor. Der erste Punkt betrifft die Benennung: Sprechende Namen wie ElementTyp wirken gut gemeint, brechen aber mit einer Konvention, die jeder kennt — und Konventionen sind hier mehr wert als Ausführlichkeit.

8:17 Der zweite ist eine Einschränkung, die man einmal erlebt: Ein Enum lässt sich nicht generisch machen. Der dritte ist der wichtigste Entwurfsfehler — im Rumpf Annahmen über T treffen, die nirgends abgesichert sind; dafür gibt es Schranken. Und der vierte ist die Bequemlichkeitsfalle: Object beibehalten und die Typprüfung in Kommentare auslagern. Kommentare prüft niemand.

Generische Typen verwenden

8:39 Kommen wir zur Aufrufseite. Hier gibt es gleich vier Schreibweisen für ein und dieselbe Sache, und die Wahl dazwischen ist eine Frage der Lesbarkeit, nicht der Kürze — das möchte ich vorwegschicken, weil der Reflex meistens in Richtung kürzest möglich geht. Außerdem gibt es in diesem Zusammenhang eine Grenze, über die praktisch jeder einmal stolpert und deren Fehlermeldung nicht sofort verrät, worum es geht. Schauen wir uns beides an.

9:03 Aus einem generischen Typ wird ein parametrisierter, sobald Typargumente gesetzt sind. Java bietet Ihnen dabei zwei Abkürzungen an. Auf der linken Seite darf var stehen, dann leitet der Compiler den Typ der Variablen ab. Auf der rechten Seite darf der Diamant stehen — zwei leere spitze Klammern, die ihren Namen von ihrer Form haben.

9:22 Beide Abkürzungen sind sinnvoll, und beide lassen sich übertreiben. Die Frage, die Sie sich stellen sollten, lautet nicht „wie kurz geht es", sondern „erkennt der nächste Leser noch, womit er es zu tun hat". Hier stehen vier Varianten untereinander, von der ausführlichsten zur knappsten. Die erste wiederholt die Typargumente auf beiden Seiten — das ist redundant und war der Anlass für den Diamanten.

9:46 Die zweite ist der Alltagsfall und in den meisten Situationen die beste Wahl. Die dritte und vierte nutzen var. Bei der letzten Variante steht der Typ nirgends mehr im Code; der Compiler leitet ihn aus den übergebenen Werten ab. Das ist elegant, solange die Werte selbst sprechen. Bei einer Zeichenkette und einer Zahl geht das gerade noch — bei zwei Aufrufergebnissen wird es undurchsichtig.

10:10 Vier Dinge zum Merken. Die wichtigste Grenze: Primitive Typen sind als Typargument nicht erlaubt. Sie schreiben also Integer statt int, und der Compiler übernimmt das Ein- und Auspacken. Das ist keine Kleinigkeit, denn es bedeutet Objekterzeugung dort, wo vorher ein Zahlenwert stand — bei großen Datenmengen durchaus spürbar.

10:30 Der Diamant funktioniert nur, wo abgeleitet werden kann. Und var verlagert Information aus dem Code in die IDE, was beim Lesen im Versionsverlauf oder im Code-Review durchaus stört. Diese Aufgabe hat kein technisches Ziel, sondern ein handwerkliches. Suchen Sie sich drei Stellen im eigenen Code und legen Sie begründet fest, welche Schreibweise dort steht.

10:51 Der Prüfstein ist einfach: Verrät der Wert selbst schon etwas über seinen Typ? Bei new ArrayList von String ist alles klar. Bei einem Methodenaufruf, der irgendetwas zurückgibt, nicht mehr. Genau dort lohnt sich die ausgeschriebene Form. Diese Abwägung bewusst zu treffen, ist eine der unterschätzten Fähigkeiten beim Schreiben lesbaren Codes.

11:12 Der erste Punkt ist die Übertreibung, die wir eben besprochen haben — var und Diamant zusammen können den Typ vollständig unsichtbar machen. Der zweite ist die Sache mit den primitiven Typen; die Fehlermeldung nennt das Problem, aber der Zusammenhang mit Autoboxing erschließt sich nicht sofort. Der dritte ist reine Gewohnheit aus alten Zeiten. Und der vierte ist der ernsteste: Rohtypen, also generische Typen ganz ohne Typargumente.

11:36 Warum die eine schlechte Idee sind und was sie anrichten können, sehen wir uns in Modul acht im Detail an.

Generische Typen erweitern

11:43 Jetzt wird es einen Schritt anspruchsvoller. Was passiert, wenn Sie einen generischen Typ erweitern oder eine generische Schnittstelle umsetzen? Sie müssen dann ein Typargument angeben — und dafür gibt es zwei grundverschiedene Fälle, die man sauber auseinanderhalten sollte. In der Praxis vermischen sich die beiden gern, und dann entsteht entweder unnötige Starrheit oder versehentlich ein Rohtyp.

12:05 Die gute Nachricht: Es gibt eine Analogie, die die Unterscheidung sehr einfach macht. Die beiden Fälle lassen sich mit der Wertebene erklären, und diese Analogie finde ich besonders hilfreich. Stellen Sie sich vor, Sie rufen eine Methode auf: Sie können entweder eine Variable übergeben oder einen festen Wert. Genau dasselbe passiert auf der Typebene.

12:26 Entweder Ihr Untertyp ist selbst generisch und reicht seine Typvariable weiter — das entspricht der Variablen. Oder er ist nicht generisch und setzt einen konkreten Typ ein — das entspricht dem festen Wert. Und natürlich lässt sich beides mischen. Der erste Fall. Unsere Klasse ist selbst generisch und gibt ihre Typvariable an die Oberklasse weiter.

12:47 Das Beispiel nutzt AbstractList aus der Standardbibliothek — eine Klasse, die fast alle Listenmethoden bereits umsetzt und nur zwei von Ihnen verlangt. Das ist übrigens ein Muster, das sich zu merken lohnt: Wenn Sie eine Schnittstelle mit vielen Methoden umsetzen müssen, lohnt der Blick, ob es eine abstrakte Basisklasse dafür gibt.

13:06 Worauf es hier ankommt, ist die Weitergabe: Das E unserer Klasse landet als Typargument bei AbstractList. Und hier die beiden anderen Varianten. Links eine Klasse, die nicht generisch ist und einen festen Typ einsetzt — daraus wird eine Liste, die nur Zeichenketten aufnimmt. Rechts eine Mischung: Der Schlüsseltyp steht fest, der Wertetyp bleibt offen.

13:28 Solche Mischformen begegnen Ihnen häufiger, als man denkt, etwa bei Zuordnungen mit festem Schlüsseltyp. Die Frage, die dahintersteht, ist immer dieselbe: Soll der Nutzer dieses Typs den jeweiligen Typ noch wählen dürfen? Wenn ja, bleibt die Variable. Wenn nein, setzen Sie ein. Die Entscheidungsregel ist damit einfach. Der Untertyp bleibt generisch, wenn seine Nutzer den Typ noch wählen sollen. Er legt fest, wenn er fachlich nur einen Typ meint.

13:55 Zwei praktische Hinweise noch: Weitergereichte Typvariablen behalten den Namen, den Ihr Untertyp vergibt — nicht den der Oberklasse; das verwirrt gelegentlich beim Lesen. Und alle geerbten Methoden arbeiten anschließend mit dem eingesetzten Typargument. Bei der festgelegten Variante heißt das: Die IDE zeigt Ihnen plötzlich konkrete Signaturen an, wo vorher Typvariablen standen.

14:18 Bauen Sie beide Varianten einmal selbst — eine generische Ableitung und eine festgelegte. Der lehrreiche Moment kommt bei der zweiten: Schauen Sie sich in der IDE an, welche Methodensignaturen dort angezeigt werden. Sie werden sehen, dass aus allgemeinen Signaturen konkrete geworden sind. Das macht sichtbar, was beim Einsetzen eines Typarguments tatsächlich passiert — es ist kein Etikett, sondern wirkt sich auf die gesamte geerbte Schnittstelle aus.

14:44 Achten Sie darauf, dass beide Fassungen ohne Warnung übersetzen. Der erste Punkt ist der häufigste Anfängerfehler: beim Erweitern das Typargument vergessen. Dann entsteht ein Rohtyp, und der Compiler warnt — aber die Warnung geht im Rauschen unter. Der zweite ist eine Frage der Sorgfalt beim Benennen. Der dritte ist eine Entwurfsfrage, die über Generics hinausgeht: Sammlungen zu erweitern ist selten die beste Idee; meist ist Kapselung die sauberere Wahl, weil Sie dann kontrollieren, welche Operationen überhaupt möglich sind.

15:15 Und der vierte ist die vorschnelle Festlegung auf einen Typ, wo Offenheit gefragt wäre.

Generische Methoden definieren

15:21 Bisher ging es ausschließlich um Typen. Jetzt kommen Methoden dazu — und dort wird es interessant, weil Methoden ja innerhalb generischer Typen leben können. Die Frage, welche Typparameter eine solche Methode sehen darf und welche sie selbst deklarieren muss, hat eine Antwort, die viele überrascht. Besonders bei statischen Methoden, wo der erste Versuch fast immer scheitert. Wir klären zunächst die Syntax und dann die drei Fälle, die man auseinanderhalten muss.

15:48 Die Syntax zuerst: Typparameter einer Methode stehen in spitzen Klammern nach den Modifikatoren und vor dem Rückgabetyp. Diese Position ist ungewöhnlich und deshalb ein gutes Erkennungsmerkmal — wenn Sie in einer Signatur spitze Klammern vor dem Rückgabetyp sehen, ist die Methode generisch. Verwendbar sind diese Typparameter danach überall: im Rückgabetyp, in der Parameterliste, im Rumpf.

16:10 Und sie funktionieren genauso wie die Typparameter eines Typs, nur eben mit kleinerer Reichweite. Ein typisches Beispiel: eine Hilfsmethode, die das erste Element liefert oder einen Vorgabewert, wenn die Sammlung leer ist. Worauf es ankommt, ist die Verkettung. Der Typparameter E taucht dreimal auf — im Rückgabetyp, im Typ der Sammlung und im Typ des Vorgabewerts.

16:32 Genau das ist der Grund, warum hier ein Typparameter nötig ist und keine Wildcard reicht: Er stellt eine Beziehung zwischen mehreren Stellen der Signatur her. Diesen Gedanken werden wir im nächsten Modul noch ausführlich brauchen, dort ist er die zentrale Entscheidungsregel. Und jetzt die Unterscheidung, die in der Praxis am häufigsten für Verwirrung sorgt.

16:53 Eine Methode innerhalb eines generischen Typs darf dessen Typparameter verwenden, ohne selbst generisch zu sein. Generisch ist sie erst, wenn sie eigene Typparameter mitbringt. Und dann der Sonderfall: Statische Methoden sehen die Typparameter des umgebenden Typs überhaupt nicht. Der Grund dafür ist logisch, sobald man ihn kennt — die Typparameter eines Typs gehören zur Instanz, nicht zur Klasse. Eine statische Methode existiert aber ohne Instanz.

17:21 Hier stehen die drei Fälle direkt untereinander. Oben eine Methode, die T und U verwendet, ohne selbst generisch zu sein — sie borgt sich die Typparameter des Records. In der Mitte dieselbe Methode mit eigenem Typparameter V; jetzt darf der erste Wert einen anderen Typ haben als vorher, die Methode ist flexibler geworden.

17:39 Und unten die statische Fabrikmethode, die eigene Typparameter deklarieren muss, weil ihr T und U nicht zur Verfügung stehen. Diese dritte Zeile ist die, an der man in der Praxis hängen bleibt. Warum ist das so? Weil ein Typparameter erst dann einen konkreten Wert bekommt, wenn Sie eine Instanz erzeugen. Bei einer Zuordnung von String und Integer steht T für String — aber eben nur für dieses eine Objekt.

18:04 Eine statische Methode gehört zur Klasse und existiert unabhängig von jeder Instanz; für sie gäbe es also gar keinen Wert, den T annehmen könnte. Deshalb muss sie eigene Typparameter deklarieren. Und ein Rat dazu: Nennen Sie sie anders als die des Typs. Gleiche Namen sind erlaubt, aber sie führen beim Lesen garantiert in die Irre.

18:25 Der Hinweis in dieser Aufgabe ist die eigentliche Übung: Versuchen Sie zuerst, T und U in der statischen Methode zu verwenden. Die Fehlermeldung, die Sie dann bekommen, ist die beste Erklärung des Prinzips — sie sagt Ihnen, dass ein nichtstatischer Kontext hier nicht referenziert werden kann. Wenn Sie diesen Satz einmal in freier Wildbahn gelesen haben, verstehen Sie ihn beim nächsten Mal sofort. Achten Sie danach darauf, dass Ihre Typparameternamen nichts verdecken.

18:51 Der erste Punkt ist die Verdeckung durch gleiche Namen — technisch erlaubt, praktisch eine Fehlerquelle. Der zweite ist ein Denkfehler: Eine Methode ist nicht schon deshalb generisch, weil in ihrer Signatur ein T vorkommt; sie muss es selbst deklarieren. Der dritte ist die statische Methode, an der wir eben gearbeitet haben.

19:10 Und der vierte ist ein Entwurfshinweis, der oft übersehen wird: Ein Typparameter, der in der Signatur nur ein einziges Mal vorkommt, stellt keine Beziehung her und bringt deshalb nichts. Dort ist eine Wildcard die bessere Wahl — dazu mehr im nächsten Modul.

Generische Methoden aufrufen

19:25 Die gute Nachricht dieses Kapitels vorweg: Beim Aufrufen einer generischen Methode müssen Sie meistens gar nichts tun. Der Compiler leitet die Typargumente aus den übergebenen Werten ab, und er tut das erstaunlich zuverlässig. Die weniger gute Nachricht: In den Fällen, in denen er es nicht kann, sollten Sie wissen, wie die Syntax aussieht — und die ist gewöhnungsbedürftig, weil sie an einer Stelle steht, an der sonst nichts steht.

19:49 Schauen wir uns beides an. Typargumente einer Methode stehen in spitzen Klammern nach dem Punkt und vor dem Methodennamen. Das ist eine Position, an die man sich gewöhnen muss, weil sie sonst nirgends vorkommt. In der Praxis werden Sie das selten schreiben — der Compiler leitet die Typargumente aus den übergebenen Werten ab, und zwar erstaunlich zuverlässig.

20:10 Nötig wird die ausgeschriebene Form nur, wenn die Ableitung nicht eindeutig ist. Lesen können sollten Sie sie trotzdem, denn in Bibliothekscode taucht sie durchaus auf. Hier sehen Sie dieselben Aufrufe jeweils zweimal — einmal ausgeschrieben, einmal abgeleitet. Beachten Sie die Position der spitzen Klammern in der ersten und dritten Zeile: hinter dem Punkt, vor dem Methodennamen.

20:32 Das gilt für statische und für Instanzmethoden gleichermaßen. Und dann vergleichen Sie mit den abgeleiteten Fassungen. Der Unterschied in der Lesbarkeit ist erheblich, und deshalb ist die kurze Form der Normalfall. Die lange ist ein Werkzeug für Sonderfälle, kein Stil. Diese Folie zeigt, wie weit die Ableitung tatsächlich trägt.

20:52 In der letzten Zeile arbeitet der Compiler an drei Stellen gleichzeitig: Er leitet die Typargumente der Methode bilde ab, dann die Typen der beiden Lambda-Parameter, und schließlich die Typargumente des Aufrufs von. Ausgeschrieben wäre diese Zeile mehr als doppelt so lang und deutlich schwerer zu lesen. Das ist der Grund, warum Java diese Ableitung über die Jahre immer weiter ausgebaut hat — ohne sie wäre generischer Code im Alltag kaum erträglich.

21:17 Vier Situationen, in denen Sie zur ausgeschriebenen Form greifen. Erstens: Die übergebenen Werte lassen mehrere Typargumente zu. Zweitens: Eine leere Sammlung oder null gibt der Ableitung nichts an die Hand — der Klassiker ist ein Aufruf mit List.of ohne Elemente. Drittens: Der abgeleitete Typ ist zwar korrekt, aber weiter oben in der Hierarchie als gewünscht.

21:39 Und viertens ein Erkennungszeichen aus der Praxis: Wenn die Fehlermeldung einen Typ nennt, den im Quelltext niemand geschrieben hat, dann hat der Compiler abgeleitet — und Sie sollten nachhelfen. Suchen Sie einen Aufruf, der ohne explizite Typargumente nicht übersetzt. Der Hinweis nennt einen guten Kandidaten: List.of ohne Elemente als Argument.

22:00 Der Wert dieser Übung liegt weniger im Ergebnis als in der Erfahrung — Sie lernen zu erkennen, wann eine kryptische Fehlermeldung in Wahrheit ein Ableitungsproblem ist. Das ist eine Diagnosefähigkeit, die Ihnen später viel Zeit spart, weil der erste Reflex bei solchen Meldungen sonst meistens in die falsche Richtung geht.

22:20 Der erste Punkt ist die ungewohnte Position der spitzen Klammern — vor den Punkt gehört sie nicht. Der zweite ist ein Ratschlag zur Fehlersuche: Wenn die Meldung unklar ist, schreiben Sie die Typargumente einmal aus. Meist wird der Fehler dann sofort sichtbar, weil der Compiler eine viel konkretere Meldung liefern kann. Der dritte ist die Übertreibung in die andere Richtung.

22:41 Und der vierte betrifft Lambdas: Deren Parameter zusätzlich zu typisieren, wo die Ableitung längst greift, macht den Code nur länger.

Schranken für Typparameter

22:49 Das letzte Kapitel dieses Moduls ist zugleich das wichtigste für die tägliche Arbeit. Denn bisher war unser Typparameter zwar schön flexibel, aber im Grunde nutzlos: Im Rumpf ließ sich mit ihm nichts anfangen, weil der Compiler nichts über ihn wusste. Sie konnten Werte durchreichen, mehr nicht. Schranken lösen genau dieses Problem — sie geben dem Compiler eine Zusage, mit der er arbeiten kann.

23:12 Und sie wirken dabei in zwei Richtungen zugleich, was man beim Entwerfen im Kopf haben sollte. Ohne Schranke darf ein Typparameter jeder Referenztyp sein. Klingt gut, hat aber eine unangenehme Kehrseite: Innerhalb des generischen Codes wissen Sie dann nichts über ihn und können außer den Object-Methoden nichts aufrufen. Eine Schranke schreiben Sie mit extends — etwa T extends Comparable von T. Danach stehen im Rumpf die Methoden des Schrankentyps zur Verfügung.

23:39 Merken Sie sich das Schlüsselwort: Es heißt immer extends, auch bei Schnittstellen. Ein implements gibt es an dieser Stelle nicht, obwohl man es intuitiv erwarten würde. Ein klassisches Beispiel: das größte Element einer Sammlung finden. Der Rumpf ist unspektakulär — durchlaufen und vergleichen. Aber ohne die Schranke wäre die Vergleichszeile gar nicht schreibbar, denn der Compiler wüsste nicht, dass sich Objekte vom Typ T überhaupt vergleichen lassen.

24:06 Die Schranke sagt ihm genau das. Und ein Nebeneffekt kommt gratis dazu: An der Aufrufstelle sind Typen, die sich nicht vergleichen lassen, jetzt ausgeschlossen. Die Schranke wirkt also in beide Richtungen — nach innen ermöglichend, nach außen einschränkend. Es geht auch mit mehreren Bedingungen. Hier verlangen wir, dass T sowohl benannt als auch vergleichbar ist, verbunden mit dem kaufmännischen Und.

24:30 Im Rumpf stehen dann die Methoden beider Typen zur Verfügung. Eine Regel dazu, die man kennen sollte: Eine Klasse darf unter den Schranken nur an erster Stelle stehen, Schnittstellen danach. Merken Sie sich diese Möglichkeit gut — sie ist einer der Gründe, warum man manchmal einen Typparameter braucht, wo eine Wildcard naheliegender wäre.

24:50 Wildcards können nämlich nur eine einzige Schranke tragen. Vier Punkte zusammengefasst. Schranken machen den Typparameter im Rumpf überhaupt erst verwendbar — das ist ihr Hauptzweck. Sie schließen an der Aufrufstelle ungeeignete Typargumente aus. Sie ersetzen Prüfungen zur Laufzeit durch eine Prüfung beim Übersetzen, und das ist wieder das Muster, das sich durch dieses ganze Seminar zieht.

25:12 Und noch einmal zur Sicherheit, weil es die häufigste Fehlerquelle beim Schreiben ist: extends gilt für Klassen und Schnittstellen gleichermaßen. Der Hinweis in dieser Aufgabe beschreibt ein Vorgehen, das ich Ihnen generell empfehlen möchte: Schreiben Sie den Rumpf zuerst ohne Schranke. Der Compiler sagt Ihnen dann, welche Fähigkeit ihm fehlt — und daraus ergibt sich die Schranke fast von selbst.

25:35 Das ist deutlich verlässlicher, als vorab zu raten, was gebraucht werden könnte. Der Erfolg zeigt sich doppelt: Die Methode übersetzt, und ein Aufruf mit einem ungeeigneten Typ scheitert jetzt beim Übersetzen statt im Betrieb. Der erste Punkt ist der, den man genau einmal macht: implements statt extends schreiben. Der zweite ist die Reihenfolgeregel bei mehreren Schranken — höchstens eine Klasse, und die zuerst.

25:59 Der dritte ist subtil: Comparable ohne Typargument zu schreiben, führt einen Rohtyp ein, mit allen Folgen aus Modul acht. Und der vierte ist eine Entwurfsfrage: Eine zu enge Schranke schließt Aufrufer aus, die Ihre Methode eigentlich gut gebrauchen könnten. Fragen Sie sich immer, ob Sie wirklich so viel verlangen müssen, wie Sie verlangen.

26:20 Vier Punkte zum Mitnehmen. Typparameter stehen in der Definition, Typargumente bei der Verwendung — diese Trennung ist der Schlüssel zu allem Weiteren. Der Diamant und var kürzen den Aufruf, solange der Typ noch erkennbar bleibt. Statische Methoden brauchen eigene Typparameter, weil die des Typs zur Instanz gehören. Und erst eine Schranke macht einen Typparameter im Rumpf benutzbar.

26:42 Morgen geht es weiter mit dem Thema, bei dem Generics ihren Ruf als kompliziert wirklich verdienen — den Wildcards.

Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →