Was ist eine generische Klasse und wie lässt sie sich erzeugen?v.5.0
Ein Ausgangsbeispiel ohne Generics
Java-Klassen besitzen Instanz- und/oder Klassenvariablen. Die
Typen dieser Variablen sind durch den Entwurf der Klasse
festgelegt. Beispielsweise sei eine Klasse Rechner
gegeben, die zwei Variablen vom Typ double
besitzt,
mit denen hier nicht näher betrachtete Berechnungen
durchgeführt werden können.
public class Rechner { private double num1; private double num2; public Rechner() { } public void setNum1(double d) { num1 = d; } public void setNum2(double d) { num2 = d; } //... }
Ein Objekt dieser Klasse könnte auf die folgende Weise erzeugt und seine Instanzvariablen dann über die Setter mit zwei double-Werten initialisiert werden.
Rechner rechner = new Rechner(); rechner.setNum1(3.14159d); rechner.setNum2(1.4d);
Die beiden Setter-Methoden verlangen zwingend double
als Parameter1). Soll jedoch
sichergestellt werden, dass die Klasse - aus welchem Grund auch
immer - typsicher mit von Fall zu Fall unterschiedlichen
Datentypen verwendet werden kann, bietet sich die Verwednung von
Generics an.
Erzeugung einer generische Klasse
Erweitert man nun die obige Klasse durch Generics, so besteht
die Möglichkeit, erst bei ihrer Verwendung festzulegen, ob
für die enthaltenen Variablen ein Typ Double
,
Integer
oder ein anderer passender numerischer Typ
Verwendung finden soll. Die Implementierung einer Klasse wird
auf diese Weise universell gehalten und jedes ihrer Objekte kann
mit einem beliebigen, natürlich zur Funktionalität der
Klasse passenden Datentypen instanziert werden.
Um dies zu erreichen wird sie statt mit konkreten Typangaben mit
sog. Typparametern ausgestattet. Sie werden als frei
wählbare einzelne Großbuchstaben in spitzen Klammern
hinter dem Klassenbezeichner notiert und dann an all den Stellen
verwendet, an denen sonst der jeweilige konkrete Datentyp
angegeben würde, wie im Beispiel als Parameter der
Accessor-Methoden.
Typparameter dürfen als Platzhalter
für beliebige Referenztypen, jedoch nicht für
primitive Datentypen Verwendung finden. Wird, wie hier, ein
primitiver Typ benötigt, so kann dies durch Referenzieren
der entsprechenen Wrapper-Klasse
gelöst werden. E steht hier im Beispiel für Element.
Es wird später bei der Bildung eines
Objektes der Klasse durch einen konkreten Datentyp
ersetzt.
Das obige Beispiel als generische Klasse sieht wie
folgt aus:
public class Rechner<E> { private E num1; private E num2; public Rechner() { } public void setNum1(E e) { num1 = e; } public void setNum2(E e) { num2 = e; } //... }
Wird für eine oder mehrere Variablen nur ein Datentyp verwendet, reicht wie oben die einfache Nennung eines einzigen Typparameters. Sollen in der Klasse jedoch verschiedene Typen Verwendung finden, so werden diese als einfache Komma-separierte Liste hinter dem Klassenbezeichner angegeben. In diesem Fall besteht dann die Möglichkeit, die beiden Variablen später mit Werten unterschiedlicher Datentypen zu belegen.
public class Rechner<E, F> { private E num1; private F num2; public Rechner() { } public void setNum1(E e) { num1 = e; } public void setNum2(F f) { num2 = f; } //... }
Selbstverständlich ist bei einer generischen Klasse auch eine Parametrisierung des Konstruktors möglich.
//... public Rechner(E e, F f) { this.num1 = e; this.num2 = f; } //...
Es ist nicht selten der Fall, dass die Typparameter einer generischen Klasse zwingend einer bestimmten Superklasse angehören müssen, um ungeeignete Datentypen zu vermeiden. So ist es sinnvoll, beispielsweise die Variablen eines Rechners nur mit numerischen Typen zu initialisieren. Dies kann durch Erweiterungsangaben der Typparameter erfolgen:
public class Rechner<E extends Number, F extends Number> { private E num1; private F num2; //... }
Auf diese Weise können bei der Objektbildung nur von Number
abgeleitete Typen für die Parametrisierung Verwendung
finden.
Verwendung einer generische Klasse
Wird das Objekt einer generischen Klasse (ein sog. generischer
Consumer) erzeugt, so legt der Entwickler bei der Objektbildung
an der Stelle der Typparameter i.a. konkret existierende
Datentypen fest, die selbstverständlich zur
Funktionalität der Klasse passen müssen (s.o.). Wie
bereits erwähnt, muss es sich hierbei grundsätzlich um
Referenztypen handeln, elementare Datentypen sind an dieser
Stelle nicht zulässig. Statt dieser können ggf. jedoch
deren Wrapper-Klassen
verwendet werden.
Ein Consumer muss nicht zwingend
parametrisiert werden. Wird hierauf verzichtet, so werden an
Stelle der Typparameter die Typen derer Elternklassen
eingesetzt, im vorliegenden Beispiel ohne erweiternde Angabe
also entweder der Typ Object
oder bei Erweiterung
durch extends Number
der Typ Number
.
Die Bildung eines Objektes des obigen Beispiels mit einem
Typparameter gestaltet sich bei der Verwendung von double-Werten
folgendermaßen. Man beachte die Verwendung der groß
geschriebenen Wrapper-Klasse Double
.
Rechner<Double> rechner = new Rechner<Double>(); rechner.setNum1(Double.valueOf(3.14d)); rechner.setNum2(0.5d);
Analog gestaltet sich die Verwendung der generischen Klasse mit zwei verschiedenen Datentypen, z.B. Integer und Float, folgendermaßen:
Rechner<Integer, Float> rechner = new Rechner<Integer, Float>(); rechner.setNum1(17); rechner.setNum2(0.5f);
Bei der Instanzierung kann die Typangabe rechts vom Zuweisungsoperator auch weggelassen werden. Der Compiler ermittelt sie dann durch die Angaben bei der Deklaration auf der linken Seite. Aufgrund des Aussehens der beiden spitzen Klammern, spricht man hier auch vom Diamond-Operator.
Rechner<Double> rechner = new Rechner<>();
Es soll nochmals erwähnt werden, dass die Verwendung von Generics dazu führt, dass der Compiler die beim Consumer aufgeführten Datentypen prüft und entsprechend ersetzt und somit die Sicherstellung der Typsicherheit vor der Laufzeit gewährleistet.
1) Selbstverständlich können an dieser
Stelle auch andere numerische Datentypen übergeben werden.
Sie werden intern jedoch durch implizites casting in double
konvertiert.
Wenn Ihnen javabeginners.de gefällt, freue ich mich über eine Spende an diese gemeinnützigen Organisationen.