Generics
EinstiegEine erste eigene generische Klasse
Eine generische Methode / Funktion
Varianz
Invarianz
Kovarianz
Kontravarianz
Links
Einstieg
Der auffälligste Unterschied bei der Verwendung generischer Datentypen zwischen Java und Scala ist,
die Art der Angabe des generischen Typs. In Java wird dieser zwischen einem Größer- und einem Kleinerzeichen (<Datentyp>) angegeben, wobei
in Scala der generische Typ innerhalb eckiger Klammern ([Datentyp]) angegeben wird.
Generische Datentypen sind Typen, die mit einem Typparameter parametrisiert werden können1.
Die nachfolgenden generischen Typangaben in Java und Scala geben als Typparameter die Klasse Integer an.
Java: ArrayList<Integer>
Scala: List[Integer]
Nachfolgend ein Beispiel, wo die Klasse List mit dem Typparameter String
verwendet wird. Der Variablen list können nur Elemente vom Typ String
zugeordnet werden.
object GT {
def main(args: Array[String]) {
val list : List[String] = List("abc","bcd","cde")
println(list)
}
}
Eine erste eigene generische Klasse
In diesem Abschnitt wollen wir eine erste eigene generische Klasse schreiben. Am einfachsten sieht man sich zunächst das folgende Beispiel an:
class MyFirstGenericClass[A](a: A, b: Double) {
def doOutput(){
println(a.toString()+" "+b.toString())
}
}
timpt.de - X2H V 0.10
Der auffälligste Unterschied zu nicht generischen Klassen stellt
die Klassendefinition dar. Zwischen dem Namen der Klasse und der
in runden Klammern gegebenen Konstruktorparameterliste ist ein
A in eckigen Klammern angegeben [A].
Hier definieren wir, dass unsere Klasse einen generischen Typ
enthält, den wir innerhalb der Klasse mit dem Typ A
"ansprechen". Da wir keine weiteren Informationen über
den generischen Typ haben, haben wir nur die Möglichkeit Objekte
des Typs A als Subtyp von Any anzunehmen, vor
der jede Klasse erbt. Der Name A ist frei wählbar.
In unserem Beispiel wird der generische Typ direkt im Konstruktor der
Klasse verwendet. In der Methode doOutput() verwenden
wir den generischen Typ und rufen dessen toString() Methode
auf, welche in Any definiert ist und somit auch in A
definiert sein muss. A muss von Any abgeleitet sein,
da Any die Oberklasse aller Klassen ist.
Eine generische Methode / Funktion
Nicht nur ganze Klassen können generische Typparameter haben, sondern auch
einzelne Methoden, wobei die zugehörige Klasse selber keine Typparameter haben
muss. Möchten wir einen Typparameter für eine Methode / Funktion definieren,
geben wir diesen einfach nach dem Methodennamen und vor der Parameterliste in
geschweiften Klammern an. Im nachfolgenden Beispiel definieren wir eine generische
Methode myGeneric in der Klasse AGenericMethodClass mit
der Typparameterbezeichnung B. Diese Methode rufen wir zwei Mal
aus dem Objekt GenericMethod auf. Der erste Aufruf erfolgt mit einer
Double Variablen und der zweite Aufruf erfolgt mit einer
String Variablen.
object GenericMethod {
def main(args: Array[String]): Unit = {
val myB1: Double = 123.4
val myB2: String = "abc"
val aGenericMethodClass = new AGenericMethodClass
aGenericMethodClass.myGeneric[Double](myB1)
aGenericMethodClass.myGeneric[String](myB2)
}
}
class AGenericMethodClass {
def myGeneric[B](b: B): Unit = println(b)
}
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
123.4
abc
Wenn wir uns der Typinference von Scala bedienen, können wir die Angabe des Types beim Aufruf der generischen Methode auch weglassen. Demnach führt folgendes leicht modifiziertes Programm, zum gleichen Ergebnis wie das obige.
object GenericMethod {
def main(args: Array[String]): Unit = {
val myB1: Double = 123.4
val myB2: String = "abc"
val aGenericMethodClass = new AGenericMethodClass
aGenericMethodClass.myGeneric(myB1)
aGenericMethodClass.myGeneric(myB2)
}
}
class AGenericMethodClass {
def myGeneric[B](b: B): Unit = println(b)
}
Varianz
Das Thema Varianz, im Bezug zu generischen Datentypen, behandelt die Frage, in welcher Vererbungs-Beziehung die generischen Typen zueinanderstehen dürfen, sodass Elemente, mit generischen Typen, zuweisungskompatibel sind. Wir unterscheiden in dieser Thematik folgende drei Arten der Varianz:
Invarianz
Es muss genau der angegebene Typparameter sein. Super- und Subklassen sind nicht erlaubt.
Kovarianz
Es sind auch Subklassen erlaubt.
Kontravarianz
Es sind auch Superklassen erlaubt.
Invarianz
Ein generischer Typ ist invariant (oder nonvariant), wenn nur Objekte zugewiesen werden können, deren Typparameter identisch sind. Auch wenn der Typparameter Subklasse des geforderten Typparameter ist, sind die generischen Typen nicht zuweisungskompatibel. Die Invarianz ist in Scala der Standard für generische Typen. Um einen invarianten generischen Typ zu definieren, geben wir lediglich einen Typparameter, ohne irgendwelche Zusätze an. Nachfolgend ein Beispiel zur Invarianz:
class A
class B extends A
class C extends B
class MyGenericClass[T]
object InVarianz {
val myGenericClassA = new MyGenericClass[A]
val myGenericClassB = new MyGenericClass[B]
val myGenericClassC = new MyGenericClass[C]
val target1 : MyGenericClass[B] = myGenericClassA // Fehler
val target2 : MyGenericClass[B] = myGenericClassB // OK
val target3 : MyGenericClass[B] = myGenericClassC // Fehler<
}
timpt.de - X2H V 0.11
Kovarianz
Bei einem kovarianten Typparameter sind auch Subklassen des Typparameters zulässig. Die Kovarianz des Typparameters wird durch Voranstellen eines "+" am Typparameter definiert. Das "+" Zeichen wird auch als Varianz-Annotation bezeichnet. Nachfolgend noch mal das Beispiel aus dem Absatz Invarianz, mit dem Unterschied, dass der Typparameter kovariant ist.
class A
class B extends A
class C extends B
class MyGenericClass[+T]
object KoVarianz {
val myGenericClassA = new MyGenericClass[A]
val myGenericClassB = new MyGenericClass[B]
val myGenericClassC = new MyGenericClass[C]
val target1 : MyGenericClass[B] = myGenericClassA // Fehler
val target2 : MyGenericClass[B] = myGenericClassB // OK
val target3 : MyGenericClass[B] = myGenericClassC // OK
}
timpt.de - X2H V 0.11
Neben der Angabe einer Varianz-Annotation ist der Hauptunterschied im obigen Beispiel darin zu sehen, dass in der letzten Zeile kein Compiler-Fehler mehr gemeldet wird. Die Zuweisung ist dementsprechend bei einem kovarianten Typparameter OK.
Kontravarianz
Superklassen (Vaterklassen) sind bei einem kontravarianten Typparameter zulässig. Die Kontravarianz wird durch eine weitere Varianzannotation, dem Minuszeichen "-" definiert. Zur Verdeutlichung folgt nun nochmals das bekannte Beispiel, mit dem Unterschied, dass ein kontravarianter Typparameter Verwendung findet.
class A
class B extends A
class C extends B
class MyGenericClass[-T]
object KontraVarianz {
val myGenericClassA = new MyGenericClass[A]
val myGenericClassB = new MyGenericClass[B]
val myGenericClassC = new MyGenericClass[C]
val target1 : MyGenericClass[B] = myGenericClassA // OK
val target2 : MyGenericClass[B] = myGenericClassB // OK
val target3 : MyGenericClass[B] = myGenericClassC // Fehler
}
timpt.de - X2H V 0.11
Im Unterschied zur Kovarianz ist nun die Zuweisung zu target1 kein Fehler mehr.
Dem entgegengesetzt ist nun die Zuweisung zu target3 ein Fehler.
Links
Heiko Seeberger
Advanced Scala - Varianz
http://it-republik.de/jaxenter/artikel/Advanced-Scala-%96-Varianz-3475.html
1 Siehe: Wikipedia - Generischer Typ