Generics

Einstieg
Eine 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)
  }
}
timpt.de - X2H V 0.14

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)
}
timpt.de - X2H V 0.17

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)
}
timpt.de - X2H V 0.17

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.

Wikipedia
Generischer Typ


Heiko Seeberger
Advanced Scala - Varianz
http://it-republik.de/jaxenter/artikel/Advanced-Scala-%96-Varianz-3475.html

______________________________
1 Siehe: Wikipedia - Generischer Typ