Zur Hauptnavigation springen [Alt]+[0] Zum Seiteninhalt springen [Alt]+[1]

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.

Programmierparadigmen

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.

Programmierparadigmen

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 → Serie) 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 bei s1 tatsächlich um eine Serie 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 Objekt e1 ist eine SerienEpisode, 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.

Programmierparadigmen

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.

Formale Typparameter können, bis auf wenige Ausnahmen, überall dort verwendet werden, wo man normale Typen verwenden würde.

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>
Programmierparadigmen

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.

Programmierparadigmen

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).

Programmierparadigmen

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