|
||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | ||||||||||||||||||||||
|
Java Generics - Einführung
|
|||||||||||||||||||||
Java Generics - Parametrisierte Typen und Methoden
JavaMagazin, April 2004
Java Generics
1 Überblick über die Sprachmittel der Java Generics1.1 Wozu braucht man Generics?
Traditionell (in nicht-generischem Java) erreicht man das dadurch, dass die Collection-Klassen (siehe z.B. die Klassen aus dem J2SE Collection Framework) Referenzen vom Typ Object verwalten. Da Object die Superklasse aller Java-Klassen ist, kann eine Collection Elemente beliebigen Typs enthalten. Ausgenommen sind lediglich die primitiven Typen, weil sie nicht von Object abgeleitet sind. Aber das ist kein größeres Problem, weil es zu jedem primitiven Typ einen korrespondierenden Referenztyp gibt. Die Umwandlung eines primitiven Typs in den korrespondierenden Referenztyp bezeichnet man als Boxing . Das musste man bisher von Hand machen, aber in J2SE 1.5 wird das Boxing eine automatische Konvertierung (Autoboxing) sein, die man nicht mehr explizit hinschreiben muss. Eine Eigenschaft der so implementierten Collections ist, dass sie nicht notwendig homogen sind, in dem Sinne, dass sie nicht notwendig Elemente desselben Typs enthalten. Vielmehr kann eine Collection eine Mischung von Objekten unterschiedlichen Typs verwalten. Das führt dazu, dass man beim Herausholen eines Elements aus einer Collection niemals genau weiß, von welchem Typ das betreffende Element ist. Deshalb muss man einen Cast machen, ehe man das Element verwenden kann. Hier ein Beispiel:
LinkedList list = new LinkedList();
Dieser Cast ist ein Laufzeit-Cast vom Typ Object-Referenz auf den Typ Integer-Referenz. Sollte sich zur Laufzeit herausstellen, dass das Element kein Integer ist, dann wird eine ClassCastException ausgelöst. Die traditionellen Implementierungen von Collections erfordern also relativ viele Casts, die zur Laufzeit scheitern können. Java Generics erlauben nun eine alternative Implementierungstechnik, bei der die Collections mit einem Element-Typ parametrisiert werden und dann homogen sind, also Elemente desselben Typs enthalten. Das Einfügen eines Elements eines "fremden" Typs wird zur Compilezeit bereits abgewiesen. Infolgedessen muss beim Herausholen eines Elements aus der Collection nicht mehr geprüft werden, ob das gefundene Element vom gewünschten Typ ist. Der lästige Laufzeit-Cast entfällt deshalb. Hier ein Beispiel:
LinkedList<Integer> list = new LinkedList<Integer>();
Im Vergleich zu einer traditionell implementierten Collection, wo Laufzeit-Casts beim Herausholen der Elemente nötig sind, werden bei einer parametrisierten Collection Typprüfungen bereits zur Compilezeit beim Einfügen der Elemente gemacht. Traditionelle Collections sind ein Beispiel für Weak Typing mit Typprüfungen zur Laufzeit; parametrisierte Collections sind ein Beispiel für Strong Typing, bei dem Typprüfungen zur Compilezeit bereits gemacht werden. In J2SE 1.5 hat der Programmierer nun beide Techniken zur Verfügung. Die Collections des traditionellen Collection-Frameworks werden im übrigen in J2SE 1.5 durch entsprechende parametrisierte Collections ersetzt. Wie oben bereits gezeigt, kann die LinkedList in J2SE1.5 als LinkedList<String> oder LinkedList<Integer> oder LinkedList<AnyType> verwendet werden. Die herkömmliche Verwendung als LinkedList ist aber weiterhin möglich. Auf diesen Kompatibilitätsaspekt gehen wir später noch genauer ein.
Sehen wir uns aber zunächst einmal die Syntax der Java Generics
genauer an. Man kann nämlich nicht nur die vorgefertigten parametrisierten
Typen des Collection-Frameworks verwenden (wie im obigen Beispiel gezeigt),
sondern man kann selber parametrisierte Typen implementieren. Sehen
wir uns an, wie das im Detail aussieht.
1.2 Generische TypenListing 1 zeigt Beispiele verschiedener parametrisierter Typen, die angelehnt sind an die Typen, die im 1.5 Collection-Framework zur Verfügung stehen. Der Beispiel-Code zeigt eine parametrisierte Klasse LinkedList<A>, ihr Super-Interface Collection<A> und ihren Iterator vom Typ Iterator<A>.
interface Collection<A> {
class LinkedList<A> implements Collection<A>{
Parametrisierte Typen unterscheiden sich von regulären Typen dadurch,
dass sie Typparameter haben. (Typparameter werden auch als Typvariablen
bezeichnet; beide Begriffe sind synonym.) In unserem Beispiel haben
die parametrisierten Typen genau einen Parameter mit dem Namen A. A bezeichnet
dabei den Typ der Elemente, die in der Liste abgelegt werden können.
Den Typparameter A kann man sich als Platzhalter für einen Typ vorstellen,
der später durch einen konkreten Typ ersetzt wird. Beispielsweise
wäre in einer LinkedList<String>, der Typparameter A durch den
konkreten Typ String ersetzt. Die Sache mit dem Platzhalter stimmt
nicht ganz, wie wir später noch erläutern werden; aber in erster
Näherung ist das eine angemessene Vorstellung von einem Typparameter.
1.3 BoundsFür die Implementierung der LinkedList mussten wir eigentlich nichts über den unbekannten Elementtyp A wissen. Wir haben lediglich die Objekt-Referenzen verwaltet. Da wir keine Methode des Typs A aufgerufen haben, ist es völlig egal, welche Felder oder Methoden der konkrete Typ später hat.Das ist nicht bei allen Collections so. Nehmen wir zum Beispiel die Implementierung einer Baum-basierten Collection wie TreeMap. Eine TreeMap enthält Paare bestehend aus einem Schlüssel (Key) und einem assoziierten Wert (Data). Diese Einträge werden in sortierter Reihenfolge gehalten. Die Sortierreihenfolge wird mit Hilfe der compareTo()-Methode des Key-Typs bestimmt; deshalb wird verlangt, dass der Key-Typ das Comparable-Interface implementiert, welches eine compareTo()-Methode verlangt. Listing 2 zeigt einen Auszug aus einer denkbaren Implementierung einer parametrisierten TreeMap. Listing 2: Implementierung einer Baum-basierten Collection – ohne Bounds
public interface Comparable<T> {
Die Klasse TreeMap hat 2 Typparameter Key und Data, die für den Typ des Schlüssels und den Typ des assoziierten Werts stehen. Außerdem gibt es eine eingeschachtelte parametrisierte Klasse Entry, die ebenfalls 2 eigene Typparameter K und V mit analoger Bedeutung hat. Auch das Interface Comparable ist parametrisiert - mit einem Typparameter, der beschreibt, zu welchem Typ ein Comparable-Objekt vergleichbar ist. Interessant ist der Aufruf der compareTo()-Methode in der Implementierung von getEntry(). Da über den Key-Typ nichts bekannt ist, ist zunächst einmal ein Cast auf den Typ Comparable<Key> nötig, ehe die compareTo()-Methode aufgerufen werden kann. Sollte sich in der Collection ein Element befinden, dass nicht Comparable ist, dann würde dieser Cast zur Laufzeit eine ClassCastException auslösen. Man hat also wieder eine relativ späte Typprüfung zur Laufzeit. Um eine frühe Typprüfung zur Compilezeit zu ermöglichen, gibt es in Java Generics das Sprachmittel der Bounds. Ein Typparameter kann einen oder mehrere sogenannte Bounds haben. Bounds sind Klassen oder Interfaces, von denen der unbekannte Typ abgeleitet sein muss. Der Compiler prüft dann, ob der konkrete Typ, der für den Typparameter später eingesetzt wird, von den spezifizierten Bounds abgeleitet ist. In unserem Beispiel der TreeMap können wir Bounds benutzen, um sicher zu stellen, dass der Key-Typ immer das Interface Comparable<Key> implementiert. Das sieht dann so aus, wie in Listing 3 dargestellt. Listing 3: Implementierung einer Baum-basierten Collection - mit Bounds
public class TreeMap<Key extends Comparable<Key>,Data> {
Da nun sichergestellt ist, dass für den Typparameter Key nur Typen eingesetzt werden können, die das Interface Comparable<Key> implementieren, kann der Cast beim Aufruf von compareTo() entfallen. Der Hauptzweck der Bounds besteht darin, dem Compiler die Möglichkeit zu geben, gewisse Typeigenschaften der Typparameter bereits zur Compilezeit zu prüfen. Beispielsweise würde der Compiler eine Instanziierung als TreeMap<Number,String> abweisen, weil Number nicht Comparable<Number> ist. Eine TreeMap<String,Number> wäre hingegen in Ordnung, weil String (in J2SE 1.5) das Interface Comparable<String> implementiert. Desweiteren haben die Bounds die Funktion, dem Compiler den Aufruf von Methoden des unbekannten Typs zu ermöglichen. Über einen Typparameter ohne Bounds weiß der Compiler nichts; er kann also ohne Cast nur die Methoden aufrufen, die bereits in Object definiert sind. Wenn hingegen ein Typparameter Bounds hat, dann kann der Compiler alle Methoden, die in den Bounds definiert sind, ohne Cast aufrufen. Das gilt allerdings nur für nicht-statische Methoden; Konstruktoren oder statische Methoden können nicht durch Bounds zugänglich gemacht werden. Die meisten Bounds sind in der Praxis Interfaces, wie das Comparable-Interface in unserem Beispiel. Die Syntax für einen Typparameter mit mehreren Bounds sieht wie folgt aus: TypParameter implements SuperClass & Interface1 & Interface2 & ... & InterfaceNEs gibt eine Einschränkung: in den Bounds dürfen nicht mehrere Instanziierungen desselben parametrisierten Interfaces vorkommen. Die folgende Deklaration wäre beispielsweise illegal:
class SomeType<T implements Comparable<String> & Comparable<StringBuffer>>
Comparable<String> und Comparable<StringBuffer> sind Instanziierungen
des Comparable-Interfaces und dürfen deshalb nicht beide in den Bounds
des Typparameters T vorkommen. Woher diese Einschränkung kommt,
erläutern wir später noch.
1.4 Generische MethodenZusätzlich zu den generischen Typen lassen sich auch generische Methoden definieren. Die Syntax ist ein wenig anders als bei den generischen Typen, aber alles bisher Gesagte über Typparameter und Bounds gilt genauso auch für generische Methoden. Alle Arten von Methoden können parametrisiert werden: statische und nicht-statische Methoden sowie Konstruktoren. Listing 4 zeigt das Beispiel einer parametrisierten max()-Methode, die das größte Element in einer Collection findet:Listing 4: Beispiel einer parametrisierten Methode
interface Comparable<A> {
Parametrisierte Methoden werden wie ganz normale Methoden aufgerufen, wie man in der main()-Methode sehen kann. Der konkrete Typ, der den Typparameter ersetzt, muss nicht explizit angegeben werden. Er wird automatisch vom Compiler aus dem Typ des Arguments deduziert, das beim Aufruf an die parametrisierte Methode übergeben wird. In unserem Beispiel wird eine LinkedList<Byte> als Argument an die max()-Methode übergeben. Der Compiler ruft dann automatisch die Instanziierung max<Byte>() auf, ohne dass explizit spezifiziert werden muss, dass der Typparameter A durch Byte ersetzt werden soll. Das überlegt sich der Compiler von ganz alleine. Diesen Prozess bezeichnet man als Type Inference. 1.5 Wildcard-InstanziierungenDer Vollständigkeit halber wollen wir noch die sogenannten Wildcard-Instanziierungen parametrisierter Typen erwähnen. Bisher haben wir ausschließlich Instanziierungen von parametrisierten Typen gezeigt, bei denen der Typparameter durch einen konkreten Typ ersetzt war. Ein Beispiel ist List<String>. Nun kann man aber nicht nur konkrete Typen für den Typparameter einsetzen, sondern auch sogenannte Wildcards. Es gibt 3 Arten von Wildcards: "? extends Type", "? super Type" und "?". Wildcard-Instanziierungen sehen dann zum Beispiel so aus: List<? extends Number>, oder List<? super Long> oder List<?>.Ein Wildcard bezeichnet keinen konkreten Typ, sondern eine Familie von Typen. Das Wildcard "? extends Number" bezeichnet beispielsweise die Menge aller Typen, die vom Typ Number direkt oder indirekt abgeleitet sind, d.h. die Familie aller Subtypen von Number, also Long, Integer, usw., inklusive Number selbst. Die Wildcard-Instanziierung List<? extends Number> bezeichnet daher logischerweise die Familie aller Instanziierungen des parametrisierten Typs List, bei dem ein Typ aus der Familie der Subtypen von Number für den Typparameter eingesetzt wurde, also List<Long>, List<Integer>, usw. inklusive List<Number>. Ein Wildcard wie "? super Long" bezeichnet die Familie aller Supertypen von Long und das Wildcard "?" steht für die Menge aller Typen. Eine Wildcard-Instanziierung eines parametrisierten Typs kann verglichen mit einer konkreten Instanziierung nicht dazu verwendet werden, um Objekte zu erzeugen. Man kann zwar eine Variable vom Typ List<? extends Number> deklarieren, aber man kann kein Objekt vom Typ List<? extends Number> erzeugen. Eine Variable vom Typ List<? extends Number> kann aber auf Objekte von kompatiblen Typen verweisen. Die kompatiblen Typen sind genau die Typen aus der Familie von Typen, die die Wildcard-Instanziierung bezeichnet. Eine Referenzvariable vom Typ List<? extends Number> kann also auf ein Objekt vom Typ List<Long> oder List<Integer> usw. verweisen. Analog kann eine Variable vom Typ List<? super Long> auf Objekte vom Typ List<Long> oder List<Number> oder List<Comparable> usw. verweisen. Eine Variable vom Typ List<?> kann auf beliebige Instanziierungen von List verweisen. Der Zugriff auf ein Objekt, das über eine Referenzvariable vom Typ Wildcard-Instanziierung referenziert wird, ist eingeschränkt. Über eine Variable vom Typ List<? extends Number> beispielsweise dürfen keine Methoden des Typs List aufgerufen werden, die ein Argument vom dem Typ nehmen, für den das Wildcard steht. Hier ist ein Beispiel:
List<? extends Number> list = new LinkedList<Integer>();
Bei einem Wildcard mit "super" ist der Aufruf von Methoden unmöglich, deren Returntyp vom dem Typ ist, für den das Wildcard steht. Für das "?" Wildcard gelten beide Einschränkungen.
Warum diese Einschränkungen gelten, wollen wir an dieser Stelle
nicht erläutern, weil es den Rahmen des Artikels sprengen würde.
Auch auf die Benutzung der Wildcard-Instanziierungen können wir an
dieser Stelle nicht eingehen. In der Praxis wird man Wildcard-Instanziierungen
in der Regel als Argument- und Returntyp von Methoden finden, und eher
seltener für die Deklaration von Referenzvariablen. Dabei dürfte
das Wildcard mit "extends" am häufigsten Verwendung finden.
In den J2SE 1.5 Plattform-Bibliotheken findet man Beispiele dafür
(siehe z.B. die Methode boolean addAll(Collection<? extends E> c) der
Klasse java.util.List).
1.6 Nachsatz zum ÜberblickDamit wären die Grundbegriffe von Java Generics erklärt. Das neue Sprachmittel ist in der Tat ausgesprochen nützlich und erlaubt es, stärker selbsterklärenden Code zu schreiben. Der Software-Entwickler kann jetzt klar und unmissverständlich sagen, dass eine Methode beispielsweise eine Collection von Strings erwartet, und nicht eine Collection irgendeinen anderen Inhalts Es kann also mehr Information im Source-Code hinterlegt werden und damit dem Leser des Codes das Verständnis erleichtert, aber auch dem Compiler die Möglichkeit gegeben werden, viele Typprüfungen bereits zur Compilezeit zu machen, die traditionell erst zur Laufzeit gemacht wurden. Beides erweist sich in der praktischen Arbeit als nützlich.Die Praxis zeigt aber auch, dass Java Generics allerlei Überraschungen zu bieten haben, wobei mit "Überraschungen" unerwartete Einschränkungen, gewöhnungsbedürftige Semantik, und ähnliche Phänomene gemeint sind. Insbesondere Entwickler mit C++-Kenntnissen, die sich bei Java Generics an C++-Templates erinnert fühlen, werden überrascht sein zu sehen, dass Java Generics mit C++-Templates, abgesehen von der Syntax, nicht viel gemeinsam haben.
Um die Semantik von Java Generics im Detail verstehen zu können,
ist es hilfreich zu wissen, wie der Java Compiler parametrisierte Typen
und Methoden übersetzt. Die Implementierung des Sprachmittels
erklärt viele der eher überraschenden Seiten von Java Generics.
Deshalb sehen wir uns im folgenden die Implementierung des Sprachmittels
an.
2 Implementierung von Java Generics
2.1 Übersetzung von GenericsEin Compiler hat im Prinzip 2 Möglichkeiten, um einen parametrisierten Typ oder eine parametrisierte Methode zu übersetzen.
Das ist insbesondere dann reine Verschwendung, wenn beispielsweise eine Collection ausschließlich Pointer oder Referenzen auf Elemente verwaltet. Pointer und Referenzen sind alle gleich groß und ihre Verwendung ist völlig unabhängig vom Typ des referenzierten Objekts. Der generierte Binärcode für eine Liste von String-Referenzen ist nahezu identisch mit dem Binärcode für eine Liste von Integer-Referenzen. Der Unterschied liegt lediglich in einigen Typprüfungen und –konvertierungen, wenn Elemente in die Collection hineingegeben oder aus der Collection herausgeholt werden.. Da in Java fast alle Typen Referenztypen sind, ist es naheliegend, dass für die Übersetzung von parametrisierten Typen und Methoden in Java die Code-Sharing-Technik verwendet wird.
Code-Sharing hat den Vorteil, dass tendenziell weniger Binärcode
erzeugt wird. Es verhindert aber andererseits die Verwendung von
primitiven Typen als Typargumente einer Instanziierung. Und so kann man
in Java Generics beispielsweise keine LinkedList<int> verwenden; lediglich
die LinkedList<Integer> ist erlaubt.
2.2 Type ErasureJava übersetzt parametrisierte Typen und Methoden mit der Code-Sharing-Technik. Dabei müssen die verschiedenen Instanziierungen auf die eine gemeinsame Repräsentation des parametrisierten Typs/Methode abgebildet werden. Wie erfolgt diese Abbildung?In Java Generics erfolgt diese Abbildung über eine sogenannte Type Erasure . Die Übersetzung mittels Type Erasure kann man sich vorstellen wie eine Übersetzung von generischem Java in reguläres Java: von der Instanziierung eines parametrisierten Typs/Methode werden sämtliche Typparameter entfernt, und in der Definition eines parametrisierten Typs/Methode wird der Typparameter durch sein erstes Bound oder den Typ Object ersetzt, falls keine Bounds angegeben waren. Aus der parametrisierten LinkedList<A> wird eine LinkedList<Object> und aus ihren Instanziierungen wie LinkedList<String> und LinkedList<Integer> wird LinkedList. Parametrisierte Methoden wie max<A extends Comparable<A>>() und ihre Instanziierungen wie max<Integer>() und max<String>() werden übersetzt in eine Methode max<Comparable>() bzw. max(). Listing 5 und 6 zeigen die parametrisierten Typen aus Listing 1 und 4 nach der Type Erasure. Listing 5: Parametrisierte Typen nach der Type Erasure
interface Collection {
class LinkedList implements Collection{
Wie man sehen kann, wurde der Typparameter A überall durch den Typ Object ersetzt. Das Ergebnis dieser Transformation ist exakt die Implementierung einer LinkedList, die man ohne Java Generics gebaut hätte: die entstandene nicht-parametrisierte Liste verwaltet alle Elemente per Object-Referenz. Das ist ein beabsichtigter Effekt, weil auf dieser Weise die neuen parametrisierten Collections kompatibel zu den traditionellen nicht-parametrisierten Collections sind. Nach der Übersetzung per Type Erasure kann man die parametrisierte Collection von der nicht-parametrisierten traditionellen Collection-Implementierung nicht mehr unterscheiden. Listing 5 zeigt noch ein weiteres typisches Element der Übersetzung von generischem in nicht-generisches Java: beim Herausholen von Elementen aus der Collection wird automatisch ein Cast eingefügt. Das ist genau der Cast, den man bei den traditionellen Collections schon immer gebraucht hat. Wenn ein Element aus einer LinkedList<String> (nach der Übersetzung nur noch eine LinkedList) geholt wird, dann ist das Ergebnis eine Object-Referenz, die auf den Typ String gecastet wird. Den entsprechenden Cast hat der Compiler im Zuge der Übersetzung automatisch eingefügt. Ganz ähnlich funktioniert die Übersetzung einer generischen Methode. Listing 6 zeigt die Type Erasure der parametrisierten Methode max() aus Listing 4: Listing 6: Parameterisierte Methode nach der Type Erasure
interface Comparable {
Wieder wurden die Typparameter an allen Stellen durch ihr erstes Bound oder Object ersetzt. Man sieht in main() wieder den automatisch eingefügten Cast für das Ergebnis der max()-Methode. Das Beispiel demonstriert darüber hinaus ein weiteres Element der Übersetzung per Type Erasure: die sogenannte Bridge-Methode. Bridge-Methoden werden eingefügt, damit das Überschreiben von Methoden in Subklassen funktioniert, die von parametrisierten Superklassen oder –interfaces abgeleitet sind. In dem gewählten Beispiel implementiert die Klasse Byte das Interface Comparable<Byte> und muss die Methode int compareTo(Byte) implementieren. Nach der Type Erasure ist aus dem Interface Comparable<Byte> ein schlichtes Interface Comparable mit einer Methode int compareTo(Object) geworden. Die implementierende Klasse Byte hat aber keine Implementierung für diese Methode; statt dessen hat sie eine Methode int compareTo(Byte). Diese Methode ist aber keine überschreibende Variante der im Interface verlangten Methode, weil die Signatur anders ist. Die Klasse Byte, die vor der Type Erasure das Interface Comparable<Byte> implementiert hat, implementiert nach der Type Erasure das nun entstandene Interface Comparable nicht mehr. Der generierte Code würde sich also ohne weitere Maßnahmen nicht übersetzen lassen. Um dies zu vermeiden, fügt der Compiler nun die Bridge-Methode ein.
Eine Bridge-Methode wird in Subklassen benötigt, die von einem
parametrisierten Interface oder einer parametrisierten Superklasse abgeleitet
sind. Die Bridge-Methode hat genau die Signatur, die der Supertyp
nach der Type Erasure verlangt. In unserem Beispiel hat die Bridge-Methode
die Signatur int compareTo(Object). Bridge-Methoden sind immer so
implementiert, dass sie an die eigentliche Methode delegieren, in unserem
Beispiel an die Methode int compareTo(Byte). Der Compiler fügt
die Bridge-Methoden im Zuge der Übersetzung per Type Erasure automatisch
ein.
2.3 Repräsentation von Generics im LaufzeitsystemDie Übersetzung per Type Erasure haben wir zur Veranschaulichung als Übersetzung von generischem Java in reguläres Java erklärt. In Wahrheit erzeugt der Compiler selbstverständlich keine temporäres .java-Datei, die er dann in eine .class-Datei übersetzt, sondern die Übersetzung erfolgt direkt von generischem Java nach Java Bytecode. Der generierte Bytecode entspricht exakt dem nicht-generischen Javacode, den wir in den Beispielen gezeigt haben. Davon kann man sich leicht durch eine Decompilation der .class-Datei überzeugen.Es gibt einige wenige Spuren, die die Generics in einer .class-Datei dennoch hinterlassen: es werden sogenannte Signature-Attribute im Bytecode ablegen; sie enthalten statische Information über die Typparameter einer Klasse oder Methode. Diese Signature-Attribute werden von der virtuellen Maschine als Kommentar behandelt und nicht ausgewertet. Die Signature-Attribute werden lediglich vom Compiler ausgewertet, um beispielsweise zu prüfen, ob eine Instanziierung zulässig im Sinne der Bounds ist. Für die virtuelle Maschine ist hingegen keinerlei Unterschied zwischen einem parametrisierten Typ/Methode und einem regulären Typ/Methode zu erkennen. Nach der Type Erasure sind die Typparameter (bis auf die Signature-Attribute) verschwunden und damit sieht 1.5-Bytecode für die JVM so aus wie 1.4-Bytecode. Die Übersetzung per Type Erasure wurde bewusst von den Designern der Java Generics als Übersetzungstechnik gewählt. Ziel des Designs der Java Generics ist die hundertprozentige Kompatibilität des aus Generics generierten Bytecode mit herkömmlichem Bytecode. Es soll problemlos möglich sein, generischen mit nicht-generischem Code zu mischen und beides sowohl unter einer neuen, aber auch unter einer alten (d.h. 1.4) JVM ablaufen zu lassen. Mit Hilfe der Type Erasure wurde dieses Kompatibilitätsziel erreicht.
Die vollständige Eliminierung der Typparameter im Rahmen der Type
Erasure hat den Vorteil der Kompatibilität mit nicht-generischem Java.
Dies wird erkauft durch verschiedene Einschränkungen, die sich aus
der Type Erasure ergeben. Der wohl wesentlichste Nebeneffekt ist
das Fehler jeglicher Information über die Typparameter zur Laufzeit.
Beim Ablauf des Programm kann eine LinkedList<String> nicht von einer
LinkedList<Integer> unterschieden werden. Beide haben zur Laufzeit
nur noch den Type LinkedList. Das führt zu allerlei interessanten
Effekten und Überraschungen, deren Darstellung den Rahmen dieses Artikels
sprengen würden. Zu den Einschränkungen gehört u.a., dass
keine Arrays von parametrisierten Typen verwendet werden dürfen. Eine
Deklaration wie Comparable<String>[] ist beispielsweise nicht zulässig.
Das ist auf den ersten Blick ziemlich überraschend und in der Praxis
reichlich störend. Es gibt natürlich gute Gründe,
warum diese Einschränkung sinnvoll und notwendig ist. Insgesamt
aber stellt man bei näherem Hinsehen fest, dass diese und andere
Restriktionen wenig intuitiv sind und den Umgang mit Java Generics relativ
gewöhnungsbedürftig machen. Java Generics sind kein triviales
Sprachfeature, dass sich dem Programmierer quasi wie von selbst erschließt.
Im Gegenteil, ein gewisser Lernaufwand für einen sicheren Umgang mit
Java Generics wird sich nicht vermeiden lassen.
3 ZusammenfassungIn diesem Artikel haben wir die wesentlichen Sprachmittel für die Definition und Verwendung von parametrisierten Typen und Methoden in Java betrachtet. Mit Hilfe von parametrisierten Typen und Methoden können zahlreiche Laufzeit-Typprüfungen durch Compilezeit-Typprüfungen ersetzt werden. Der Compiler übersetzt Generics per Type Erasure in herkömmlichen Bytecode. Die Übersetzungstechnik per Type Erasure stellt sicher, dass generisches Java zu herkömmlichem Java kompatibel ist. Der Nachteil dieser Technik ist das Fehlen jeglicher Information über die Typparameter zur Laufzeit. Für weitere Informationen sei an dieser Stelle auf die nachfolgenden Links verwiesen.4 Weitere Informationen
|
||||||||||||||||||||||
© Copyright 1995-2007 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/JavaMagazin/Generics/GenericsPart1.html> last update: 10 Aug 2007 |