Generische Datentypen
Notwendigkeit für generische Datentypen anhand eines Beispielprojekts
Eine der fundamentalen Idee der Informatik ist die Abstraktion zur Vermeidung von Duplikaten. Entsprechend gilt das Mantra:
„Wenn Du zwei Programmteile siehst, die sich nur an wenigen Stellen unterscheiden und die inhaltlich verwandt sind, abstrahiere!“
Werkzeuge der strukturieren, objektorientierten Programmierung sind hierfür beispielsweise Klassen, Methoden und Schleifen. Aber selbst unter weitestgehender Nutzung dieser Mittel kann es dazu kommen, dass es Methoden bzw. Klassen gibt, die sich nur in ihrer Signatur und der darin verwendeten Typen unterscheiden. Häufig findet man solche Doppelungen bei Sammlungen gleichartiger Objekte.

Abbildung 2: Klassendiagramm des Projektes „Filmsammlung“ von ZPG Informatik [CC BY-SA 4.0 DE], aus 01_hintergrund.pdf
Als Beispiel dient uns eine Filmsammlung
(betrachte das zugehörige Klassendiagramm in Abbildung 2), die sowohl Kinofilme
als auch Serien
erfasst. Eine Serie
ist dabei wiederum eine Sammlung von SerienEpisoden
, deren Anzahl bestimmt werden kann. Sowohl Filmen
als auch SerienEpisoden
können eine Bewertung bekommen. Die Bewertung einer Serie
bestimmt sich aber aus den Bewertungen der enthaltenen SerienEpisoden
.
Die Filmsammlung kann nun auf einfachste Weise als eine Reihung implementiert werden, in die Filme eingetragen werden können. Durch Polymorphie ist sowohl ein Kinofilm
, eine Serie
, als auch eine SerienEpisode
ein Film
. Instanzen dieser Klassen können demnach problemlos einer Filmsammlung hinzugefügt werden. Im Begleitmaterial ist eine solche einfache Implementierung mitgegeben.
Für die folgenden Erläuterungen werden als Beispielobjekte die Serien s1
und s2
mit Episoden e1
bis e4
genutzt. In Abbildung 3 sind diese in einem Objektdiagramm dargestellt.

Abbildung 3: Beispielobjekte in einer Filmsammlung von ZPG Informatik [CC BY-SA 4.0 DE], aus 01_hintergrund.pdf
Betrachte nun folgenden gekürzten Ausschnitt eines Programms
1| Filmsammlung fs = new Filmsammlung( new Film[]{s1, s2, e1} );
2| fs.get(1).setBewertung( 0.0 );
3| ...println( fs.get(0).getTitel()
4| ...println( fs.get(0).getAnzahlEpisoden() );
5| ...println( ( (Serie) fs.get(0) ).getAnzahlEpisoden() );
6| ...println( ( (Serie) fs.get(2) ).getAnzahlEpisoden() );
- InZeile2wirddurchPolymorphiedieüberschriebeneMethodederSerieaufgerufen. Dort kann sichergestellt werden, dass die Bewertung nicht verändert wird.
-
In Zeile 3 findet eine implizite erweiternde Typumwandlung (
Serie → Film
) statt. Es wird der Titel der Serie ausgegeben. -
Zeile 4 führt zu einem Kompilierungsfehler, da eine einschränkende Umwandlung (
Film → Seri
e) durchgeführt werden müsste, die in Java eine explizite Typkonvertierung erfordert. -
Damit die Anzahl der Episoden ausgegeben werden kann, muss in Zeile 5 die Typkonvertierung (
Film → Serie
) angegeben werden. Es findet dann eine explizite einschränkende Typumwandlung statt. Da es sich beis1
tatsächlich um eineSerie
handelt, wird die Anzahl Episoden wie gewünscht ausgegeben. -
WennallerdingswieinZeile6eineTypkonvertierung(
Film→Serie
)aufeinenfalschen Subtyp angewandt wird, kommt es zu einem Laufzeitfehler: Es wird eine ClassCastException geworfen. Das Objekte1
ist eineSerienEpisode
, welche nicht in direkter Verwandtschaft zu einer Serie steht.
Der Laufzeitfehler bei der Typkonvertierung kann vermieden werden, indem man für Serienepisoden eine eigene neue Klasse Episodensammlung
schreibt. Diese unterscheidet sich dann von der Filmsammlung
nur in der Signatur (siehe Abbildung 4). Die Implementierung wäre nahezu identisch.

Abbildung 4: Filmsammlung und Episodensammlung: beinahe identisch von ZPG Informatik [CC BY-SA 4.0 DE], aus 01_hintergrund.pdf
Mit Version 5 der JDK wurde Java um generische Datentypen erweitert. Diese ermöglichen es, bei der Programmierung einer Klasse von den enthaltenen Datentypen zu abstrahieren, sich bei der Nutzung aber trotzdem auf einen Typ zu beschränken.
Einfache generische Typen deklarieren
Zum Verständnis der Syntax folgt ein kleiner Auszug aus den Definitionen der Schnittstellen List
und Iterator
im Paket java.util:
public interface List <E> {
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E> {
E next();
boolean hasNext();
}
Neu in diesem Code sind die Angaben in den spitzen Klammern. Damit werden die formalen Typparameter der beiden Schnittstellen List
und Iterator
deklariert.
Wird die Filmsammlung
auf diese Weise parametrisiert, sieht das Klassendiagramm aus wie Abbildung 5. Vergleicht man mit der Signatur des generischen Typs ArrayList<E>
aus dem Paket java.util
, fällt auf, dass FilmsammlungGeneric<E>
lediglich andere Konstruktoren und wenige zusätzliche Methoden hat, ansonsten aber die selbe Funktionalität fordert. Bei der Implementierung erweitert man daher am besten einfach den generischen Typ.
public class FilmsammlungGeneric<E> extends ArrayList<E>

Abbildung 5: Klassendiagramm der generischen Typen FilmsammlungGeneric<E>
und ArrayList<E>
von
ZPG Informatik
[CC BY-SA 4.0 DE],
aus
01_hintergrund.pdf, bearbeitet
Es ist auch möglich mehrere formale Typparameter zu deklarieren, diese werden dann innerhalb der spitzen Klammern Komma-getrennt aufgelistet.
public interface Map<K,V>{…
Einfache generische Typen nutzen
Mit Hilfe selbst deklarierter oder bereits in Bibliotheken vorhandener generischer Typen kann bereits im Programmcode vorgegeben werden, welche konkreten Typparameter erlaubt sind. Fehler werden dann bereits bei der Kompilierung erkannt und es kommt nicht zu einem Laufzeitfehler, wie zuvor im Beispiel erläutert. Dadurch wird Typsicherheit gewährleistet.
Bei der Nutzung generischer Typen wird analog der Nutzung normaler Typen vorgegangen.
1| ArrayList l1 = new ArrayList();
2| ArrayList l2 = new ArrayList<>(); // Typinferenz
3| ArrayList l3 = new ArrayList(); // Laufzeitfehler möglich
4| ArrayList l4 = new ArrayList(); // Laufzeitfehler möglich
5| ArrayList l5 = new ArrayList(); //Kompilierungsfehler
Dabei sollte immer die Syntax wie in Zeile 1 oder 2 verwendet werden. Wird wie in Zeile 3 und 4 nur der Originaltyp (ohne spitze Klammern) notiert, dann wird Typsicherheit nicht bei der Kompilierung überprüft sondern zur Laufzeit verlagert. Das Verhalten ist dann identisch zum nicht parametrisierten Originaltyp. Es können Laufzeitzeitfehler an Stellen im Programmcode auftreten, an denen das jeweilige Objekt verwendet wird.
Zeile 2 ist erlaubt, da hier durch Typinferenz der konkrete Typparameter (in den spitzen Klammern) abgeleitet wird. Man nennt die zwei spitzen Klammern ohne konkreten Typparameter auch den Diamant-Operator <>. Dieser kann immer dann verwendet werden, wenn Typinferenz möglich ist.
Zeile 5 schlägt fehl, da in generischen Typen nur Referenztypen als konkrete Typparameter erlaubt sind. Primitive Datentypen wie int
müssen zunächst durch Boxing in einen Referenztyp gepackt werden.
Betrachte nun den kurzen Programmausschnitt, diesmal mit generischem Datentyp
1| FilmsammlungGeneric serien =
new FilmsammlungGeneric<>( new Serie[]{s1, s2} );
2| serien.get(1).setBewertung( 0.0 );
3| ...println( serien.get(0).getTitel() );
4| ...println( serien.get(0).getAnzahlEpisoden() );
Zeile 2 funktioniert nach wie vor mit dem selben Ergebnis.
Auch Zeile 3 liefert das gleiche Ergebnis, allerdings ist keine Typumwandlung notwendig. Die von der Superklasse geerbte Methode wird aufgerufen.
In Zeile 4 ist weder eine explizite Typkonvertierung, noch eine implizite Typumwandlung nötig, da sicher gestellt ist, dass es sich um ein Objekt des Typs Serie
handelt.
Für das Verständnis der generischen Typen und der Fehlermeldungen, die beim Programmieren auftreten können, ist es im Unterricht notwendig, Fachbegriffe klar zu definieren und durchgängig richtig anzuwenden. Leider wurden in der deutschen Literatur die englischen Begriffe vielfach unterschiedlich und teils falsch bzw. missverständlich übersetzt. Tabelle 3 fasst die in diesem Dokument verwendeten Begriffe zusammen. Ich empfehle ausschließlich diese deutschen Begriffe oder die englischen Originalbegriffe zu verwenden.

Tabelle 3: Fachbegriffe zu generischen Typen von ZPG Informatik [CC BY-SA 4.0 DE], aus 01_hintergrund.pdf, bearbeitet
Generische Typen und Vererbung10
Innerhalb einer Vererbungshierarchie kann ein Objekt aus unterschiedlichen Sichten betrachtet werden. So kann ein Objekt eines Typs dann einem Objekt eines anderen Typs zugewiesen werden, wenn diese kompatibel sind, also eine Typumwandlung möglich ist. Für Referenztypen ist dies i.A. dann der Fall, wenn eine Vererbungsbeziehung besteht.
Generische Typen können genauso wie normale Typen erweitert werden. Falls bei der Nutzung die konkreten Typparameter nicht variiert werden, bleibt die Subtyp-Beziehung bestehen.
Gehe von folgender Situation aus:
// Klassen Signaturen
final class Integer extends Number
final class Double extends Number
class BigBox extends Box
// Methoden Signaturen
public void numberTest( Number n );
public void boxTest( Box b );
Die folgenden Zeilen Code kompilieren bis auf die letzte ohne Fehler.
Object einObjekt;
Integer einInteger = new Integer(10);
einObjekt = einInteger; // OK: Integer ist Subtyp von Object
numberTest( new Integer(10)); // OK
Box box = new Box();
box.add( new Integer(10) ); // OK
Box bbox = new BigBox; // OK
boxTest( new Box() ); // OK
boxTest( new BigBox() ); //OK
boxTest( new Box() ); // FEHLER
Die letzte Zeile führt zu einem Kompilierungsfehler, da Box<Integer>
kein Subtyp von Box<Number>
ist, obwohl Integer
ein Subtyp von Number
ist (siehe Abbildung 6).

Abbildung 6: Beispiel möglicher Vererbungshierarchien bei generischen Typen von ZPG Informatik [CC BY-SA 4.0 DE], aus 01_hintergrund.pdf
Dies liegt daran, wie generische Typen vom Compiler verarbeitet werden (Type Erasure11 ). Der generisch deklarierte Typ wird in eine einzige class-Datei kompiliert, so wie jede andere Klasse oder Interface. Es gibt nicht mehrere Versionen des Codes für verschiedene konkrete Typparameter: weder im Code, noch zur Laufzeit. Jede Instanz einer generischen Klasse teilt sich die selbe Klasse unabhängig vom im Code verwendeten konkreten Typparameter.
Typparameter sind analog zu den normalen Parametern, die in Methoden oder Konstruktoren verwendet werden. Ähnlich wie eine Methode formale Wertparameter hat, die die Art von Werten beschreiben, mit denen sie arbeitet, hat eine generische Deklaration formale Typparameter.
Wenn eine Methode aufgerufen wird, werden die formalen Parameter durch konkrete Argumente ersetzt, und der Methodenkörper wird ausgewertet. Wenn eine generische Deklaration aufgerufen wird, werden die formalen Typparameter durch konkrete Argumente ersetzt. Formale Typparameter werden nach Aufruf weder bei der Kompilierung, noch zur Laufzeit durch konkrete Typparameter ersetzt, sondern durch die konkreten Typargumente.12
Einschränken der Typparameter über Bounds
Im Beispielprojekt ermöglicht der generische Typ FilmsammlungGeneric<E>
Typsicherheit und vermeidet doppelten Code. Die Methode getBewertung()
einer Filmsammlung soll den Durchschnitt der Bewertungen aller Elemente in der Sammlung zurück geben. Die Objekte in der Sammlung – deren Typ durch den konkreten Typparameter vorgegeben ist – müssen demnach selbst eine Methode getBewertung()
haben.
Allerdings kann bei der Instanziierung jeder beliebige konkrete Typparameter verwendet werden. Der Compiler kann beim Parsen der generischen Klasse deshalb nicht wissen, ob zur Laufzeit nur solche konkreten Typargumente vorliegen, die die Methode getBewertung()
haben. Damit das Programm ohne Fehler compiliert, muss über einen sogenannten Bound sichergestellt werden, dass nur solche konkreten Typparameter erlaubt sind, die die Methode getBewertung()
bereitstellen. Im Beispiel sind das all diejenigen Typen, die Subtyp von Film
sind. Film
stellt in der Vererbungshierarchie eine obere Schranke, den sogenannten upper Bound, dar. Im Programmcode wird dies durch das Schlüsselwort extends
erreicht, wobei dies sowohl erweitern (wie bei Klassen) oder implementieren (wie bei Schnittstellen) bedeutet. Der Typparameter mit Bound lautet dann:
FilmsammlungGeneric<E extends Film>
Nun sind für E noch die Typen Film, Kinofilm, Serie
und Serienepisode
erlaubt.
Als Bound dürfen sowohl Klassen als auch Schnittstellen verwendet werden. Bei der Verwendung von Schnittstellen ist es sogar möglich, mehrere Bounds anzugeben. Ist einer der Bounds eine Klasse, muss diese als erstes angegeben werden. Zum Beispiel:13
class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C>{ /* ... */ }
Generische Methoden
Auch Methoden können generisch definiert werden. Die Syntax lautet:
public <K, V> boolean compare( Pair<K, V> p1, Pair<K, V> p2 );
Dabei steht eine Liste der formalen Typparameter in spitzen Klammern vor dem Rückgabewert der Methode. Beim Aufruf der Methode müssen die konkreten Typparameter nicht angegeben werden, sondern der Compiler leitet sie automatisch ab (Typinferenz).
Nicht mit generischen Methoden zu verwechseln sind Methoden, die Typvariablen aus einer sie umgebenden generischen Klasse in ihrer Definition haben:
public E set (int index, E element)
Sie sind nicht generisch, weil der Typparameter E an anderer Stelle festgelegt wird und hier gar nicht mehr frei („generisch“) ist. Im Unterricht müssen generische Methoden nicht behandelt werden. Die Schülerinnen und Schüler sollten lediglich die Syntax der Signatur einer generischen Methode kennen. Dieser begegnen sie bei der Arbeit mit der offiziellen Java- Dokumentation.
Bei generischen Methoden spielen neben upper Bounds auch lower Bounds und Wildcards eine Rolle, auf diese wird hier aber nicht näher eingegangen. Es empfiehlt sich das Studium der entsprechenden Quellen.14
10https://docs.oracle.com/javase/tutorial/java/generics/inheritance.html (ausgewertet am 22.12.2020)
11https://docs.oracle.com/javase/tutorial/java/generics/erasure.html (ausgewertet am 22.12.2020)
12 https://docs.oracle.com/javase/tutorial/extra/generics/intro.html (Oracle, Gilad Bracha, The JavaTM Tutorials Lesson: Generics, ausgewertet am 22.12.2020)
13https://docs.oracle.com/javase/tutorial/java/generics/bounded.html (ausgewertet am 22.12.2020)
14https://docs.oracle.com/javase/tutorial/java/generics/methods.html (ausgewertet am 22.12.2020)
Hintergrundinformationen: Herunterladen [odt][370 KB]
Hintergrundinformationen: Herunterladen [pdf][480 KB]
Weiter zu Collections Framework