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)
  }
}
itmapa.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())
  }
}
itmapa.de - X2H V 0.18

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)
}
itmapa.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)
}
itmapa.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
}
itmapa.de - X2H V 0.18

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
itmapa.de - X2H V 0.18

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
itmapa.de - X2H V 0.18

Im Unterschied zur Kovarianz ist nun die Zuweisung zu target1 kein Fehler mehr. Dem entgegengesetzt ist nun die Zuweisung zu target3 ein Fehler.

Bounds (dt. Grenzen)

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

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.

Upper Bounds (dt. obere Grenzen)

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
  }
}
itmapa.de - X2H V 0.18

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.

Lower Bounds (dt. untere Grenzen)

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
  }
}
itmapa.de - X2H V 0.18

Upper and Lower Bounds

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
  }
}
itmapa.de - X2H V 0.20

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
  }
}
itmapa.de - X2H V 0.20

View Bounds

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)
}
itmapa.de - X2H V 0.20

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)
}
itmapa.de - X2H V 0.20

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