Start / Seminare / Java SE Fortgeschrittene Sprachfeatures

Modul

Generics im Detail

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 im Detail

0:00 In den letzten beiden Modulen haben wir Generics benutzt. Jetzt schauen wir unter die Oberfläche — und ich verspreche Ihnen, das lohnt sich. Denn fast alles, was an Generics als willkürliche Einschränkung erscheint, folgt aus einer einzigen Tatsache: Generics gelten beim Übersetzen und sind zur Laufzeit verschwunden. Wenn Sie diesen Satz wirklich verinnerlicht haben, können Sie sich jede der bekannten Einschränkungen selbst herleiten, statt sie auswendig zu lernen.

0:25 Und Sie erkennen die Stellen, an denen Typsicherheit still entweicht — davon gibt es einige, und sie sind unauffälliger, als man erwarten würde.

Generics im Detail

0:34 Sieben Kapitel, aber im Kern nur zwei Ursachen. Wir beginnen mit Rohtypen, die aus reiner Rücksicht auf alten Code existieren. Dann kommt die Typlöschung, aus der sich die meisten Einschränkungen ergeben. Danach klären wir die Invarianz, die uns im Wildcard-Modul begegnet ist, und sehen, warum sie gut begründet ist. Anschließend Heap Pollution — ein dramatischer Name für ein reales Problem. Und zum Schluss die beiden Stellen, an denen es unangenehm wird: Arrays und Varargs.

1:03 Vier Fragen. Warum gibt es Rohtypen und warum sind sie trotzdem nie die richtige Wahl? Was macht der Compiler tatsächlich mit Ihren Typparametern? Warum ist eine Liste von Zugängen keine Liste von Buchungen — die Frage aus dem letzten Modul, die wir dort vertagt haben? Und wie können Arrays und Varargs die Typsicherheit unterlaufen?

1:22 Der letzte Punkt ist der praktisch relevanteste, weil er zu Fehlern führt, die weder beim Übersetzen noch beim Auslösen auffallen, sondern erst viel später an ganz anderer Stelle.

Wie Generics arbeiten

1:33 Verschaffen wir uns zuerst die Landkarte. Ich zeige Ihnen die zentrale Tatsache, auf der dieses ganze Modul beruht, und was alles daraus folgt — die folgenden sechs Kapitel füllen diese Landkarte dann im Einzelnen aus. Wenn Ihnen dabei etwas zu knapp erscheint, keine Sorge, es kommt gleich ausführlich. Mir geht es hier vor allem darum, dass Sie den Zusammenhang sehen: Was wie sieben getrennte Themen aussieht, hat in Wahrheit nur zwei Ursachen.

1:59 Der zentrale Satz lautet: Generics sind ein reines Übersetzungsmerkmal. Der Compiler nutzt Typparameter und Typargumente für seine Prüfungen — und wirft sie danach weg. Zur Laufzeit gibt es keine Information mehr darüber. Ein Bild dazu: Denken Sie an ein Baugerüst. Es ist unverzichtbar, solange gebaut wird, und wenn das Haus steht, ist es fort.

2:20 Die Stabilität steckt danach im Bauwerk selbst, nicht im Gerüst. Genauso stecken die Typprüfungen im übersetzten Code, aber die Typinformation ist weg. Aus dieser einen Tatsache folgt fast alles Weitere. Rohtypen existieren, weil alter Code weiter übersetzen musste. Neue Instanzen über eine Typvariable sind unmöglich, weil zur Laufzeit niemand mehr weiß, welcher Typ gemeint war. instanceof kann Typargumente nicht prüfen, aus demselben Grund.

2:48 Und Arrays vertragen sich nicht mit Generics, weil sie genau umgekehrt gebaut sind. Was ich Ihnen mitgeben möchte: Das sind keine vier Regeln zum Auswendiglernen, das sind vier Folgerungen aus einem Satz. Wenn Sie den Satz haben, brauchen Sie die Regeln nicht. Diese Tabelle ordnet die Themen ihren Ursachen zu, und das Bemerkenswerte steht in der rechten Spalte: Es gibt nur zwei echte Ursachen.

3:12 Die Abwärtskompatibilität zu Java vor Version fünf erklärt die Rohtypen. Die Typlöschung erklärt die Einschränkungen, die Heap Pollution und die Array-Probleme. Alles andere sind Folgen. Nehmen Sie diese Tabelle als Kompass für die nächsten Kapitel — wenn Sie sich irgendwo fragen, warum etwas so ist, wie es ist, steht die Antwort mit hoher Wahrscheinlichkeit in dieser rechten Spalte.

Rohtypen

3:34 Fangen wir mit dem einfachsten Kapitel an. Rohtypen sind schnell erklärt, und die Botschaft ist unmissverständlich: Sie haben in neuem Code nichts zu suchen. Damit könnte man es bewenden lassen — interessanter als die Regel ist aber der Grund, warum es sie überhaupt gibt. Er sagt nämlich etwas über die Java-Plattform insgesamt aus und darüber, welchen Preis Abwärtskompatibilität hat.

3:56 Und er erklärt, warum Ihnen Rohtypen im Bestandscode weiterhin begegnen werden. Ein Rohtyp ist ein generischer Typ ohne Typargumente — List statt List von String. Der Grund für ihre Existenz ist rein historisch: Als Generics mit Java fünf kamen, gab es bereits gewaltige Mengen an Code, der Listen ohne Typargumente verwendete.

4:16 Der musste weiter übersetzen. Abwärtskompatibilität war und ist eines der wichtigsten Versprechen der Java-Plattform, und dieses Versprechen hat einen Preis — hier ist er. Was Sie wissen müssen: Ein Rohtyp schaltet einen großen Teil der Typprüfungen ab, und das ist keine Kleinigkeit. Drei Zeilen, ein Unterschied. Oben der Rohtyp, darunter die parametrisierte Fassung.

4:39 Und dann die dritte Zeile: Dort landet eine Zeichenkette in einer Liste, die eigentlich Zugänge enthalten soll — ohne Fehler, ohne Warnung. Das ist der Punkt, den ich betonen möchte. Nicht dass es möglich ist, sondern dass es geräuschlos möglich ist. Der Fehler entsteht hier und fällt an einer ganz anderen Stelle auf, nämlich beim Lesen. Diesen Zeitversatz sehen wir uns im Kapitel über Heap Pollution genauer an.

5:04 Wenn Sie im Bestandscode auf Rohtypen stoßen, hilft dieses Vorgehen. Prüfen Sie zuerst, welche Typen die Stelle tatsächlich verarbeitet — meistens ist es genau einer, und dann ist die Sache schnell erledigt. Passen mehrere verwandte Typen, setzen Sie eine obere Schranke; das Wissen dafür haben Sie aus dem letzten Modul. Und wenn wirklich nichts über den Typ bekannt ist, nehmen Sie wenigstens die unbeschränkte Wildcard.

5:28 Schritt vier ist das Minimum: Ein Fragezeichen ist immer besser als gar nichts, weil es zumindest die stillschweigenden Einfügungen verhindert. Der erste Punkt ist ein Missverständnis: Ein Rohtyp ist keine kurze Schreibweise für „egal welcher Typ" — dafür gibt es das Fragezeichen. Der zweite passiert oft unabsichtlich beim Erweitern generischer Klassen; vergessen Sie dort das Typargument, haben Sie einen Rohtyp, ohne es zu merken.

5:53 Der dritte ist eine organisatorische Falle: Warnungen zu Rohtypen dauerhaft abzuschalten, weil es gerade zu viele sind. Und der vierte ist der folgenreichste — Rohtypen in neuen Schnittstellen, denn damit verlagern Sie das Problem zu allen Ihren Aufrufern.

Type Erasure

6:08 Und jetzt das Kernkapitel dieses Moduls. Type Erasure, die Typlöschung, ist der Mechanismus, aus dem sich fast alle Einschränkungen ergeben, über die man bei Generics stolpert. Ich zeige Ihnen zuerst ganz konkret, was der Compiler mit Ihrem Code anstellt — vorher und nachher nebeneinander. Und danach leiten wir die bekannten Einschränkungen gemeinsam daraus ab.

6:29 Das ist der Teil, nach dem Sie diese Regeln nicht mehr auswendig lernen müssen, weil Sie sie herleiten können. Beim Übersetzen entfernt der Compiler die Typparameter. Typvariablen werden durch Object ersetzt — oder durch den Schrankentyp, falls eine Schranke da ist. Parametrisierte Typen werden zu ihrem Rohtyp. Und wo es nötig ist, ergänzt der Compiler Typumwandlungen, damit alles zusammenpasst.

6:53 Für Typen, deren Information dabei verloren geht, gibt es einen eigenen Begriff: nicht reifizierbar. Das Wort klingt sperrig, meint aber nur: Dieser Typ existiert zur Laufzeit nicht mehr in der Form, in der Sie ihn geschrieben haben. Diese Folie ist die wichtigste des Moduls. Oben steht, was Sie schreiben, unten, was die Laufzeit sieht.

7:14 Aus den Typparametern ist Object geworden, aus dem parametrisierten Typ ein Rohtyp. Und dann die letzte Zeile: Dort steht eine Umwandlung, die Sie nie geschrieben haben. Der Compiler hat sie ergänzt, weil er weiß, dass dort eine Zeichenkette liegt — die Laufzeit weiß es nicht mehr. Wenn Sie ein Bild dafür suchen: Sie schreiben in Farbe, ausgeliefert wird in Schwarzweiß, und der Compiler hat sich vorher notiert, wo welche Farbe war.

7:40 Noch eine Präzisierung zum Begriff. Nicht reifizierbar sind parametrisierte Typen mit mindestens einem konkreten Typargument oder einer beschränkten Wildcard — dort geht Information verloren. Parametrisierte Typen mit ausschließlich unbeschränkten Wildcards bleiben dagegen reifizierbar. Das leuchtet ein, wenn man kurz darüber nachdenkt: Ein Fragezeichen ohne Schranke enthält ja gar keine Typinformation, also kann auch nichts verloren gehen.

8:06 Diese Unterscheidung ist der Grund, warum manche instanceof-Prüfungen erlaubt sind und andere nicht. Und jetzt die Ernte. Diese fünf Einschränkungen kennen Sie vermutlich alle aus der Praxis — und die rechte Spalte zeigt, dass sie alle denselben Ursprung haben. new T geht nicht, weil zur Laufzeit niemand weiß, was T ist. instanceof gegen einen parametrisierten Typ geht nicht, weil die Typargumente fort sind.

8:31 Klassenliterale gibt es aus demselben Grund nicht. Zwei Methoden, die sich nur in den Typargumenten unterscheiden, hätten nach der Löschung dieselbe Signatur. Und ungeprüfte Umwandlungen sind eben ungeprüft, weil zur Laufzeit die Prüfgrundlage fehlt. Diese Aufgabe ist ungewöhnlich, weil sie kaum Tipparbeit erfordert. Nehmen Sie sich drei der Einschränkungen und begründen Sie sie selbst — ohne nachzuschlagen.

8:55 Schreiben Sie die verbotenen Zeilen bewusst hin und vergleichen Sie Ihre Begründung mit der Fehlermeldung. Der Wert liegt darin, dass Sie danach nicht fünf Regeln kennen, sondern ein Prinzip. Und ein Prinzip trägt auch bei der sechsten Einschränkung, die Ihnen irgendwann begegnet und die auf keiner Folie stand. Der erste Punkt ist die instanceof-Prüfung gegen einen parametrisierten Typ, die regelmäßig versucht wird.

9:19 Der zweite ist das Überladen, das nach der Löschung kollidiert — die Fehlermeldung spricht dann von gleicher Erasure, und wenn man den Begriff kennt, ist sie sofort klar. Der dritte und vierte gehören zusammen: Unchecked-Warnungen wegdrücken und SuppressWarnings setzen, ohne begründen zu können, warum die Umwandlung sicher ist.

9:37 Diese Annotation ist eine Zusicherung, die Sie geben — und für die Sie geradestehen sollten.

Generics und Vererbung

9:43 Jetzt lösen wir die Frage ein, die im Wildcard-Modul offen geblieben ist: Warum ist eine Liste von Zugängen keine Liste von Buchungen, obwohl jeder Zugang eine Buchung ist? Ich habe damals gesagt, dass es dafür gute Gründe gibt — die sehen wir uns jetzt an. Die Antwort ist kein Zufall und keine Nachlässigkeit im Sprachdesign.

10:02 Sie ist schlicht notwendig, und ein einziges Gegenbeispiel genügt, um das zu zeigen. Generics sind invariant. Obwohl Zugang ein Untertyp von Buchung ist, ist eine Liste von Zugang kein Untertyp einer Liste von Buchung. Der Grund liegt beim Einfügen. Eine Liste von Buchung darf alles aufnehmen, was eine Buchung ist — auch einen Abgang.

10:22 Wenn nun eine Liste von Zugang zugleich eine Liste von Buchung wäre, dann könnten Sie über diesen Umweg einen Abgang in die Zugangsliste legen. Die Invarianz ist also keine Einschränkung um der Strenge willen, sondern der Preis dafür, dass Sammlungen typsicher bleiben. Diese drei Zeilen zeigen das Gedankenexperiment. Die zweite Zeile wird vom Compiler abgelehnt — und genau das ist die Rettung.

10:46 Denn wäre sie erlaubt, dann wäre auch die dritte erlaubt, und in Ihrer Zugangsliste läge ein Abgang. Der Fehler würde erst auffallen, wenn jemand die Liste durchläuft und die Elemente als Zugänge behandelt. Dann gäbe es eine ClassCastException an einer Stelle, die mit der Ursache nichts zu tun hat. Die Invarianz verhindert diese ganze Fehlerklasse.

11:07 Für die drei Formen gibt es Fachbegriffe, die man kennen sollte, weil sie in jeder Diskussion über Typsysteme auftauchen. Invarianz ist der Regelfall bei Generics — keine Beziehung zwischen den Listen. Kovarianz erhält die Untertypbeziehung, und Java erlaubt sie gezielt über extends. Kontravarianz dreht sie um und wird über super erlaubt.

11:27 Was daran wichtig ist: Wildcards sind nicht irgendeine Syntaxspielerei, sondern der Schalter, mit dem Sie kontrolliert von der Invarianz abweichen — genau dort, wo Sie es brauchen, und nur dort. Und jetzt schließt sich der Kreis zum letzten Modul. Eine Liste von Buchungen mit extends nimmt die Liste der Zugänge an — die Kovarianz ist hergestellt.

11:48 Aber und das ist der entscheidende Punkt: Das Einfügen bleibt dort verboten. Genau dieses Verbot verhindert den Abgang in der Zugangsliste. Sie bekommen also die Flexibilität beim Übergeben und behalten die Sicherheit beim Inhalt. Wenn Sie im letzten Modul das Einfügeverbot als lästige Einschränkung empfunden haben — hier ist der Grund. Es ist keine Einschränkung, es ist der Sinn der Sache.

12:11 Der erste Punkt ist die Fehleinschätzung, Invarianz sei ein Mangel — sie ist eine Notwendigkeit. Der zweite ist die teure Umgehung: die Liste umkopieren, statt die Signatur zu öffnen. Der dritte ist ein Denkfehler, der besonders leicht passiert: Arrays sind kovariant, und man überträgt das gedanklich auf Generics. Warum Arrays das dürfen, sehen wir gleich.

12:32 Und der vierte ist die schon besprochene Erwartung, in eine kovariante Liste einfügen zu dürfen.

Heap Pollution

12:39 Der Name klingt nach Katastrophe, und in gewisser Weise ist es auch eine — nur eine sehr leise. Heap Pollution beschreibt eine Situation, in der Ihr Typsystem lügt: Eine Variable behauptet einen bestimmten Typ, und im Speicher liegt etwas ganz anderes. Das Unangenehme daran ist nicht der Absturz an sich, sondern der zeitliche und räumliche Abstand zwischen Ursache und Wirkung.

13:01 Wir sehen uns zwei Wege an, wie so etwas entsteht — und beide sehen im Code völlig harmlos aus. Heap Pollution liegt vor, wenn eine Variable eines parametrisierten Typs auf ein Objekt eines anderen Typs verweist. Die beiden Hauptursachen kennen Sie inzwischen: Rohtypen und Typlöschung. Und das Charakteristische daran ist der Zeitversatz.

13:22 Der Fehler entsteht an einer Stelle und zeigt sich an einer ganz anderen — als ClassCastException beim Lesen. Wer dann anfängt zu suchen, sucht am Absturzort. Dort ist aber alles in Ordnung. Diese Entkopplung von Ursache und Wirkung macht solche Fehler so teuer. Sehen wir uns den Weg über den Rohtyp an. Eine Liste von Zugängen wird einer Rohtyp-Variablen zugewiesen — erlaubt. In diese wird ein Abgang eingefügt — erlaubt, ohne Warnung.

13:49 Und dann, drei Zeilen später, bricht der Schleifendurchlauf mit einer ClassCastException. In diesem Beispiel liegen drei Zeilen zwischen Ursache und Wirkung. In echtem Code liegen dazwischen oft mehrere Klassen und im schlimmsten Fall Wochen. Deshalb ist das hier kein akademisches Beispiel, sondern das stärkste Argument gegen Rohtypen überhaupt.

14:11 Der zweite Weg ist subtiler. Java erlaubt es, einen wildcard-parametrisierten Typ auf einen konkreten parametrisierten Typ umzuwandeln. Das wirkt merkwürdig — und ist es auch ein Stück weit. Vermutlich ist es erlaubt, weil es legitime Anwendungsfälle gibt und ein Verbot das Typsystem zu starr machen würde. Immerhin warnt der Compiler hier. Und diese Warnung ist die einzige Stelle, an der das Problem überhaupt sichtbar wird — deshalb sollte man sie nicht ignorieren.

14:39 Warum kann der Compiler hier nicht einfach prüfen? Aus zwei Gründen, die beide schon gefallen sind. Beim Übersetzen ist der wirkliche Typ hinter der Wildcard unbekannt — das ist ja der Sinn einer Wildcard. Und zur Laufzeit sind die Typargumente entfernt, also fehlt auch dort die Grundlage. Es gibt schlicht keinen Zeitpunkt, an dem diese Prüfung stattfinden könnte.

15:00 Die Warnung ist damit die einzige Rückmeldung, die Sie überhaupt bekommen. Und SuppressWarnings beseitigt die Warnung, nicht die Verunreinigung — das ist ein Unterschied ums Ganze. Bauen Sie das einmal selbst nach. Der Hinweis nennt den eigentlichen Auftrag: Notieren Sie, wie viele Zeilen zwischen Ursache und Absturz liegen.

15:20 Und dann stellen Sie sich vor, diese Zeilen wären über drei Klassen und zwei Wochen verteilt. Das ist die Erfahrung, die aus einer abstrakten Regel — verwende keine Rohtypen — eine Überzeugung macht. Solche Fehler zu erleben, wenn sie noch klein und überschaubar sind, ist deutlich angenehmer als die Alternative. Der erste Punkt ist eine begriffliche Klarstellung, die mir wichtig ist: SuppressWarnings ist keine Lösung, sondern eine Behauptung — Sie behaupten damit, die Umwandlung sei sicher.

15:49 Der zweite ist ein Prozessproblem: Warnungen im Bauprozess ausblenden, weil es zu viele sind. Dann sind alle Warnungen wertlos, auch die wichtigen. Der dritte ist die unbegründete Zuversicht. Und der vierte ist der Diagnosefehler, den wir eben besprochen haben — den Absturzort für den Fehlerort zu halten.

Generics und Arrays

16:08 Zwei Kapitel bleiben, und beide handeln davon, dass Arrays und Generics nicht zusammenpassen. Der Grund dafür ist erfreulich klar und schnell erzählt: Sie sind in beiden relevanten Eigenschaften genau gegensätzlich gebaut. Das ist keine Nachlässigkeit, sondern die Folge zweier Entwurfsentscheidungen, die zehn Jahre auseinanderliegen und jede für sich stimmig ist.

16:28 Wenn Sie diese Gegenüberstellung einmal im Kopf haben, erklären sich die kryptischen Fehlermeldungen dieses Bereichs von selbst. Arrays kennen ihren Elementtyp zur Laufzeit — sie sind reifizierbar. Und sie sind kovariant, ein Array von Zugängen gilt also als Array von Buchungen. Generics sind in beiden Punkten das Gegenteil: nicht reifizierbar und invariant.

16:49 Das ist keine Nachlässigkeit, sondern eine bewusste Entwurfsentscheidung zu unterschiedlichen Zeiten — Arrays gab es von Anfang an, Generics kamen zehn Jahre später. Und weil beide Entscheidungen in sich stimmig sind, aber gegenläufig, kann man sie nicht sauber kombinieren. Hier sehen Sie, wie Arrays das lösen. Die Zuweisung in der zweiten Zeile ist erlaubt, weil Arrays kovariant sind.

17:13 Die dritte Zeile übersetzt ebenfalls anstandslos — und wirft zur Laufzeit eine ArrayStoreException. Die Java-Autoren haben hier also entschieden, die Prüfung in die Laufzeit zu verlegen. Damit das funktioniert, muss ein Array seinen Elementtyp kennen; deshalb ist es reifizierbar. Generics gehen den anderen Weg und prüfen beim Übersetzen. Beide Ansätze sind vertretbar, nur eben nicht miteinander vereinbar.

17:38 Diese Gegenüberstellung erklärt in vier Zeilen, warum eine bestimmte Zeile Code nicht übersetzt. Ein Array braucht seinen Elementtyp zur Laufzeit. Ein parametrisierter Typ hat ihn dort nicht. Also kann es kein Array eines parametrisierten Typs geben — die Laufzeitprüfung des Arrays hätte keine Grundlage. Die Fehlermeldung dazu lautet knapp „generic array creation" und erklärt gar nichts.

18:01 Wenn Sie die beiden ersten Zeilen dieser Tabelle im Kopf haben, ist die Meldung trotzdem sofort verständlich. Praktisch heißt das: Ein Array eines nicht reifizierbaren Typs lässt sich nicht erzeugen. Der Ausweg ist in neunundneunzig von hundert Fällen erfreulich einfach — nehmen Sie eine Liste. Listen sind ohnehin die flexiblere Wahl, und der Grund, warum man überhaupt zu einem Array greift, ist meist Gewohnheit oder eine alte Schnittstelle.

18:27 Wo ein Array wirklich unvermeidlich ist, führt der Weg über ein übergebenes Klassenobjekt und Reflection. Das ist umständlich, funktioniert aber — und Sie wissen jetzt, warum es umständlich sein muss. Der erste Punkt ist die kryptische Meldung, die man ohne Hintergrundwissen nicht deuten kann. Der zweite ist der übliche Ausweg, der ins Verderben führt: über ein Object-Array umwegen und dabei genau die Heap Pollution erzeugen, die wir eben besprochen haben.

18:54 Der dritte ist eine Einordnungsfrage — die Kovarianz von Arrays ist keine Stärke, sondern eine frühe Entscheidung, die man heute anders treffen würde. Und der vierte ist die falsche Reaktion auf eine ArrayStoreException: im Betrieb nach der Ursache suchen, statt die Struktur zu ändern.

Generics und Varargs

19:10 Das letzte Kapitel, und es fügt alles zusammen, was wir heute besprochen haben. Varargs sind Arrays. Arrays vertragen sich nicht mit Generics. Und trotzdem erlaubt Java generische Varargs — was zunächst wie ein Widerspruch aussieht und auch einer ist. Was daraus folgen kann, sehen wir uns jetzt an. Es ist der unangenehmste Fall in diesem ganzen Modul, weil dabei jede einzelne Zeile für sich korrekt ist und trotzdem am Ende etwas Falsches im Speicher liegt.

19:37 Varargs sind syntaktischer Zucker für ein Array. Ein Parameter mit drei Punkten ist im Rumpf schlicht ein Array. Und wenn der Elementtyp generisch ist, dann haben Sie dort ein Array eines nicht reifizierbaren Typs — also genau das, was Sie vorhin nicht erzeugen durften. Varargs sind damit eine Hintertür. Erzeugen dürfen Sie so ein Array nicht, haben dürfen Sie es.

19:58 Java lässt das zu, weil generische Varargs praktisch sind und ein Verbot viele nützliche Methoden unmöglich machen würde. Aber es warnt Sie — und zwar zu Recht. Diese vier Zeilen sind der eigentliche Grund für dieses Kapitel. Jede einzelne ist für sich völlig zulässig. Das Varargs-Array einer Object-Variablen zuweisen — erlaubt, Arrays sind kovariant. Etwas hineinlegen — erlaubt, in ein Object-Array passt alles. Und dann, beim Lesen, bricht es.

20:27 Was diesen Fall so unangenehm macht: Es gibt an keiner Stelle einen Fehler und keine Warnung. Jeder Schritt für sich ist korrekt, nur die Kombination ist es nicht. Und jetzt kommt der Punkt, der alles zusammenbringt. Arrays prüfen beim Speichern normalerweise den Elementtyp — das ist die ArrayStoreException von vorhin. Warum greift sie hier nicht? Weil durch die Typlöschung der Elementtyp nur noch List ist, ohne Typargument. Und eine Liste von Zeichenketten ist eine List.

20:56 Die Prüfung findet also statt, sie kann nur nicht mehr genau genug hinschauen. Der Schutz, auf den man sich bei Arrays verlässt, ist hier stumpf geworden — und genau deshalb warnt der Compiler an dieser Stelle. Wie geht man damit um? Erst lesen und verstehen, dann prüfen, ob eine Liste als Parameter die Varargs ersetzen kann — das ist oft die beste Lösung.

21:18 Wenn nicht, stellen Sie sicher, dass die Methode das Array nur liest und nicht weitergibt. Erst dann setzen Sie SafeVarargs, und zwar mit einer Begründung im Kommentar. Ein technisches Detail zum Schluss: Die Methode muss final, statisch oder ein Konstruktor sein, sonst greift die Annotation nicht. Und noch einmal deutlich: SafeVarargs ist eine Zusicherung des Autors. Der Compiler prüft sie nicht nach.

21:43 Die Aufgabe verlangt eine schriftliche Begründung, und das ist Absicht. Die Frage lautet immer: Verlässt das Array die Methode, oder wird darin geschrieben? Wenn beides nein ist, dann ist SafeVarargs vertretbar. Wenn eines davon ja ist, dann nicht — dann müssen Sie die Signatur ändern. Diese Frage schriftlich zu beantworten, zwingt zur Klarheit.

22:03 Und der Text, den Sie dabei schreiben, gehört als Kommentar an die Annotation, damit der nächste Leser die Zusicherung nachvollziehen kann. Der erste Punkt ist die häufigste Verwendung von SafeVarargs überhaupt: um die Warnung loszuwerden. Das ist genau die falsche Motivation. Der zweite und dritte sind die beiden Bedingungen, die dabei verletzt werden — das Array weitergeben oder hineinschreiben.

22:27 Und der vierte ist ein praktisches Problem: Diese Warnungen sieht man vor allem bei der Übersetzung auf der Kommandozeile. In der IDE gehen sie leicht unter, und im Bauprozess auch, wenn niemand hinschaut. Vier Sätze. Generics wirken beim Übersetzen; zur Laufzeit sind die Typargumente entfernt — daraus folgt fast alles andere.

22:47 Rohtypen existieren nur für Altcode und brechen die Typsicherheit geräuschlos. Invarianz ist keine Schikane, sondern der Schutz davor, den falschen Typ in eine Sammlung zu legen. Und Arrays und Varargs sind die beiden Stellen, an denen Typsicherheit still entweicht. Im letzten Modul wird es wieder handfester: Wir schauen uns Annotationen an — und wie Sie eigene schreiben und auswerten.

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