Advanced   Java   Services Generische Interfaces, Klassen und Methoden


Einleitung

In der API gibt es eine von Version zu Version zunehmende Zahl von Klassen und Interfaces, die generische Typen verwenden, etwa die Interfaces IComparable<T>, IEnumerable<T> , ISet<T>, ICollection<T> oder die generischen Containerklassen Klassen List<T>, HashSet<T> oder Collection<T>. Hier werden einfache Regeln über generische Interfaces und Klassen zusammengestellt.


Generische Interfaces

Wir geben uns das folgende einfache Interface vor

interface IFace<T>
{
   void method(T t);
   T    method();

}

Hier kann der generische Typ sowohl in der Parameterliste als auch als Returntyp auftauchen.

Nun soll ein Interface davon erben. Es gibt dann nur zwei Möglichkeiten, entweder das Interface realisiert den Typ oder es übernimmt den Typ


Typ wird realisiert

In diesem Fall setzt der Compiler in den beiden Methoden statt T den realen Typ ein.

interface IFace1 : IFace<String>
{
   // void method(String t);
   // String method();
}

Typ wird übernommen
interface IFace2<T> : IFace1<T>
{
}

In diesem Fall gibt es noch die Variante, daß ein neuer Typ hinzukommt

interface IFace3<T1, T2> : IFace1<T1>, IComparable<T2>
{
   void method(T1 t1, T2 t2);
}

Umgekehrt kann ein generisches Interface auch von einem nichtgenerischen Interface erben.

interface IFace<T> : ICloneable
{
   void method(T t);
}

Einschränkungen generischer Typen (Constraints)

Sowohl bei Interfaces als auch bei Klassen kann man Bedingungen an den generischen Typ stellen, so kann man z. Bsp. nur generische Typen zulassen, deren Realisierungen einen Defaultkonstruktor haben. Die Syntax arbeitet in diesem Fall mit einer Art where-Klausel. Im folgenden werden diese Constraints vorgestellt. Bei einer Implementierung muß die Klausel übernommen werden.


where T : class

In diesem Fall ist T eine Referenz, also eine Klasse, ein Interface oder ein Delegate.

interface IFace<T> where T : class
{}
Generischer Typ wird übernommen:
      class Impl<T> : IFace<T> where T : class
      {}
      
Generischer Typ wird eingesetzt:
      class Impl: IFace<String>
      {}
      

where T : struct

Hier ist T ein Werttyp, Ausnahme Nullable<T>

interface IFace<T> where T : struct
{}
Generischer Typ wird übernommen:
      class Impl<T> : IFace<T> where T : struct
      {}
      
Generischer Typ wird eingesetzt:
      class Impl : IFace<MyStructT>
      {}
      

where T : new()

Hier muß die Realisierung von T einen Defaultkonstruktor besitzen.

interface IFace<T> where T : new()
{}
Generischer Typ wird übernommen:
      class Impl<T> : IFace<T> where T : new()
      {}
      
Generischer Typ wird eingesetzt:
      class Impl : IFace<MyStructT>
      {}
      

where T : IrgendeineKlasse

Die Realisierung von T ist 'IrgendeineKlasse' oder eine Unterklasse davon, im Beispiel ArrayList

interface IFace<T> where T : IrgendeineKlasse
{}
Generischer Typ wird übernommen:
      class Impl<T> : IFace<T> where T : ArrayList
      {}
      
Generischer Typ wird eingesetzt:
      class Impl : IFace<ArrayListT>
      {}
      

where T : IrgendeinInterface

Die Realisierung ist 'IrgendeinInterface' oder ein Unterinterface/Unterklasse davon.

interface IFace<T> where T : IrgendeinInterface
{}
Generischer Typ wird übernommen:
      class Impl<T> : IFace<T> where T : ICloneable
      {}
      
Generischer Typ wird eingesetzt:
      class Impl : IFace<IMyInterfaceT>
      {}
      

Bei der Implementierung gibt insofern Varianten als eine Einschränkung durch eine stärkere ersetzt werden kann. So ist im vorigen Fall auch

class Impl<T> : IFace<T> where T : ArrayList
{}

richtig, weil ArrayList das Interface ICloneable implementiert.

Die folgende Variante ist jedoch falsch.

class Impl<T> : IFace<T> where T : new()
{}

class ImplB<T> : IFace<T> where T : ArrayList
{}

Die Forderung an T in der zweiten Klasse ist nämlich auch erfüllt, wenn T eine Unterklasse von ArrayList ist und es ist nicht garantiert, daß diese Unterklasse einen parameterlosen Konstruktor besitzt, also ist die zweite Bedingung an T keine verschärfung de ersten Bedingung.


Generische Klassen

Für generische Klassen ergeben sich die gleiche Möglichkeiten wir für Interfaces.


Zwei wichtige Einschränkungen

Ein generischer Typ kann nicht mit new instantiiert werden, es sei denn er wird über die new() Klausel eingeführt.

class Xyz<T>
{
   public X()
   {
      T t = new T();
      // Cannot create an instance of the variable type 'T' because it does not have the new() constraint
   }
}

Ein generischer Typ zu einer Klasse ist immer nicht-statisch.

Da ein generischer Typ immer über ein new Statement realisiert wird (Xy xy = new Xy();) bezieht sich der generische Typ immer auf eine nicht-statische Variable.


Beispiel 1: Eine generische Klasse mit einem generischen Datenfeld

Man kann hier den generischen Typ genauso verwenden wie einen realen Typ.

public class Member<T>
{
   private T id = null;

   public Member()
   {
   }

   public Member(T id)
   {
      this.id = id;
   }

   public T getId()
   {
      return id;
   }

   public void setId(T id)
   {
      this.id = id;
   }

   public String genericTypeToString()
   {
      return id.toString();
   }
}

Die Methode genericTypeToString() erscheint etwas ungewöhnlich, ist aber legal, da eine Konkretisierung des generischen Typs sich von Object ableiten muß


Beispiel 2: Implementierung von System.IComparable<T>

Wir erweitern Comparable um eine Vergleichsmethode, die case-insensitive ist.

public interface IComparableIgnoreCase<T> : IComparable<T>
{
   int CompareToIgnoreCase(T t);
}

Die Klasse Person implementiert nun das Interface IComparableIgnoreCase für ihren eigenen Typ

public class Person : IComparableIgnoreCase<Person>
{
   private String Vorname { get; set; }
   private String Nachname { get; set; }

   // TODO Constructors
   // TODO etc.

   public int CompareToIgnoreCase(Person other)
   {
      int erg = this.Nachname.ToLower().CompareTo(other.Nachname.ToLower());
      return erg == 0 ? this.Vorname.ToLower().CompareTo(other.Vorname.ToLower()) : erg;
   }

   public int CompareTo(Person other)
   {
      int erg = this.Nachname.CompareTo(other.Nachname);
      return erg == 0 ? this.Vorname.CompareTo(other.Vorname) : erg;
   }
}

Generische Methoden

Eine Methode kann generisch sein, ohne daß es die Klasse ist. In solch einem Fall muß man die spitzen Klammern, die den Platzhalter für den realen Typ beinhalten direkt nach dem Namen der Methode stellen und es gibt nur diesen Platz, jeder andere Ort ist falsch.

class Xyz
{
   public String genericTypeToString<T>(T t)
   {
      return t.ToString();
   }
}

Noch zwei Varianten

public void genericType<T>(T t)
{
    // do something with t
}

public T genericType2<T>(T t)
{
   // do something with t
   return t;
}

Auch statische Methode können generisch sein.