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) } }
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()) } }
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.
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) }
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.
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 }
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
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.
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
Im Unterschied zur Kovarianz ist nun die Zuweisung zu target1
kein Fehler mehr.
Dem entgegengesetzt ist nun die Zuweisung zu target3
ein Fehler.
Ohne Bounds können Typparameter beliebige Typen annehmen. Mithilfe von Bounds können wir die Typen, die für einen Typparameter zulässig sind einschränken. Mit Bounds können wir festlegen, dass ein Typparameter Sub- und/oder Supertyp eines anderen Typen sein muss.
Machen wir keine Einschränkung für den Typparameter, haben wir keine Informationen zum Typ und
können auch nicht (ohne Typecast)
auf spezielle Methoden/Funktionen des Typs zugreifen. zugreifen.
Dazu ein Beispiel, wobei die Klassen A1 B1 C1 A2 B2 C2
zwei verschiedene Vererbungshierachien
darstellen sollen.
class A1 { def doIt1() = println("A1") } class B1 extends A1 { override def doIt1() = println("B1") } class C1 extends B1 { override def doIt1() = println("C1") } class A2{ def doIt2() = println("A2") } class B2 extends A2 { override def doIt2() = println("B2") } class C2 extends B2 { override def doIt2() = println("C2") } class OhneBounds[T](t: T) { def doIt() = println("doIt") } object Main{ def main(args: Array[String]) { val a1 = new OhneBounds(new A1) val b1 = new OhneBounds(new B1) val c1 = new OhneBounds(new C1) val a2 = new OhneBounds(new A2) val b2 = new OhneBounds(new B2) val c2 = new OhneBounds(new C2) } }
Im obigen Beispiel wissen wir innerhalb der Methode doIt
von OhneBounds
nichts über den Typ von t
außer, dass dieser vom Typ Any
(Supertyp aller Klassen) abgeleitet sein muss. Wir können also jeden beliebigen Typ übergeben.
Mit Upper Bounds können wir festlegen, dass ein Typparameter einem anderen Typparameter entsprechen oder ein Subtyp des Parameters sein muss. Die Schreibweise für einen Upper Bound ist:
A <: B
und bedeutet, dass A
vom Typ B
oder ein Subtyp von B
sein muss.
Sehen wir uns nun eine Abwandlung des Beispiels an, wobei wir nun eine Klasse
UpperBounds
definieren und festlegen, dass der übergebene Typ
Subtyp von A1
sein muss.
class A1 { def doIt1() = println("A1") } class B1 extends A1 { override def doIt1() = println("B1") } class C1 extends B1 { override def doIt1() = println("C1") } class A2{ def doIt2() = println("A2") } class B2 extends A2 { override def doIt2() = println("B2") } class C2 extends B2 { override def doIt2() = println("C2") } class UpperBounds[T <: A1](t: T) { def doIt() = println(t.doIt1()) } object Main{ def main(args: Array[String]) { val a1 = new UpperBounds(new A1) val b1 = new UpperBounds(new B1) val c1 = new UpperBounds(new C1) // val a2 = new UpperBounds(new A2) // Fehler // val b2 = new UpperBounds(new B2) // Fehler // val c2 = new UpperBounds(new C2) // Fehler } }
Die upper Bunds definieren wir im Konstruktor der Klasse UpperBounds:
class UpperBounds[T <: A1](t: T)
Dadurch, dass wir nun wissen, dass der übergebene Typ Subtyp von A1
sein muss, können wir nun auch auf die Methode doIt1
zugreifen, die in A1
definiert wurde.
Die Variablen a2
, b2
und c2
im unteren Teil des Objektes Main
können wir nicht mehr definieren,
da diese kein Subtyp von A1
sind. Die Definition würde zu einem Fehler während
der Complilierung führen.
Mit Lower Bounds können wir festlegen, dass ein Typparameter einem anderen Typparameter entsprechen oder ein Supertyp des Parameters sein muss. Die Schreibweise füf ein Lower Bound ist:
A >: B
und bedeutet, dass A
vom Typ B
oder ein
Supertyp von B
sein muss.
Im nachfolgenden Beispiel definieren wir eine Klasse LowerBounds
mit dem Typparameter T
.
Diese Klasse definiert eine Methode doIt22
, welche einen Typparameter A
definiert,
welcher Supertyp von T
sein muss. Jeder Versuch, die Methode mit einem Typparameter aufzurufen,
der nicht Supertyp von T
ist, führt zu einem Fehler.
class A1 { def doIt1() = println("A1") } class B1 extends A1 { override def doIt1() = println("B1") } class C1 extends B1 { override def doIt1() = println("C1") } class A2{ def doIt2() = println("A2") } class B2 extends A2 { override def doIt2() = println("B2") } class C2 extends B2 { override def doIt2() = println("C2") } class LowerBounds[T]() { def doIt22[A >: T](a: A) = println("Kuck:"+ a.getClass) } object Main{ def main(args: Array[String]) { val lb = new LowerBounds[B2]() // lb.doIt22[A1](new A1) // Fehler // lb.doIt22[B1](new B1) // Fehler // lb.doIt22[C1](new C1) // Fehler lb.doIt22[A2](new A2) lb.doIt22[B2](new B2) lb.doIt22[A2](new C2) // lb.doIt22[C2](new C2) // Fehler } }
Upper und Lower Bounds schließen sich gegenseitig nicht aus. Wir können für einen Typparameter festlegen, dass er Supertyp eines Typen und Subtyp eines anderen Typen (inklusive der Grenzen) sein muss. Nachfolgend die Schreibweise zur Angabe von Upper und Lower Bounds:
A >: B <: C
Dieses Beispiel besagt, dass A
Supertyp von B
und Subtyp von C
sein
mnuss.
Nachfolgend ein Beispiel zur Angabe von Upper und Lower Bounds. Die Methode doIt
erwartet als Typparameter einen Wert der Supertyp von D1 und Subtyp von B1 ist.
Eine Übergabe von E1 bzw. A1 oder Typen aus anderen Vererbungshirarchien führt
zu einem Compilerfehler.
class A1 class B1 extends A1 class C1 extends B1 class D1 extends C1 class E1 extends D1 class A2 class LowerAndUpperBounds { def doIt[T >: D1 <: B1](t: T) = println("Kuck:"+ t.getClass) } object Main{ def main(args: Array[String]) { val myVal = new LowerAndUpperBounds // myVal.doIt[A1](new A1) // Fehler myVal.doIt[B1](new B1) myVal.doIt[C1](new C1) myVal.doIt[D1](new D1) // myVal.doIt[E1](new E1) // Fehler // myVal.doIt[A2](new A2) // Fehler } }
Upper und Lower Bounds müssen nicht fest vorgegeben werden, sondern können auch über Typparameter der Klasse festgelegt werden. Das nachfolgende Beispiel zeigt diese Vorgehensweise.
class A1 class B1 extends A1 class C1 extends B1 class D1 extends C1 class E1 extends D1 class A2 class LowerAndUpperBounds[X,Y] { def doIt[T >: X <: Y](t: T) = println("Kuck:"+ t.getClass) } object Main{ def main(args: Array[String]) { val myVal = new LowerAndUpperBounds[D1,B1] // myVal.doIt[A1](new A1) // Fehler myVal.doIt[B1](new B1) myVal.doIt[C1](new C1) myVal.doIt[D1](new D1) // myVal.doIt[E1](new E1) // Fehler // myVal.doIt[A2](new A2) // Fehler } }
Bei der Verwendung von Upper Bounds in Kombination mit impliziter Typkonvertierung stehen wir vor dem Problem. Wir können keine Typen übergeben, welche mit Hilfe der impliziten Typkonvertierung in einen akzeptablen Typen umgewandelt werden können (vorausgesetzt Sie sind nicht Bestandteil der gewünschten Typhirarchie). Abhilfe schaffen hier View Bounds. View Bounds besagen, dass der übergebene Typ in der entsprechenden Hirarchie steht oder als ein Typ der Hirarchie angesehen werden kann. Also sind hier auch implizite Typumwandlungen möglich.
Ein View Bound wird folgender maßen definiert:
A <% B
Die Aussage dieser Definition ist, dass der Typ A als B angesehen werden können muss.
Nachfolgend ein Beispiel mit Upper Bounds. Obwohl der Typ C2
implizit nach C1
umgewandelt werden kann, kann er nicht für den Konstruktor von MyViewBounds
verwendet werden.
class A1 class B1 extends A1 class C1 extends B1 class A2 class B2 extends A2 class C2 extends B2 object ViewBoundDemo { def main(args: Array[String]) { val myVal1 = new MyViewBounds(new C1) // OK val myVal2 = new MyViewBounds(new C2) // Fehler } implicit def convert(c2: C2) : C1 = new C1 } class MyViewBounds[T <: A1](t: T){ println(t) }
Im nächsten Beispiel werden View Bounds statt Upper Bounds verwendet.
Hier ist es möglich C2
für den Konstruktor zu verwenden,
da C2
implizit nach C1
umgewandelt werden kann.
class A1 class B1 extends A1 class C1 extends B1 class A2 class B2 extends A2 class C2 extends B2 object ViewBoundDemo { def main(args: Array[String]) { val myVal1 = new MyViewBounds(new C1) // OK val myVal2 = new MyViewBounds(new C2) // OK } implicit def convert(c2: C2) : C1 = new C1 } class MyViewBounds[T <% A1](t: T){ println(t) }
Heiko Seeberger
Advanced Scala - Varianz
http://it-republik.de/jaxenter/artikel/Advanced-Scala-%96-Varianz-3475.html