Advanced Java Services | Generische Interfaces, Klassen und Methoden |
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.
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
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(); }
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); }
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.
In diesem Fall ist T eine Referenz, also eine Klasse, ein Interface oder ein Delegate.
interface IFace<T> where T : class {}
class Impl<T> : IFace<T> where T : class {}
class Impl: IFace<String> {}
Hier ist T ein Werttyp, Ausnahme Nullable<T>
interface IFace<T> where T : struct {}
class Impl<T> : IFace<T> where T : struct {}
class Impl : IFace<MyStructT> {}
Hier muß die Realisierung von T einen Defaultkonstruktor besitzen.
interface IFace<T> where T : new() {}
class Impl<T> : IFace<T> where T : new() {}
class Impl : IFace<MyStructT> {}
Die Realisierung von T ist 'IrgendeineKlasse' oder eine Unterklasse davon, im Beispiel ArrayList
interface IFace<T> where T : IrgendeineKlasse {}
class Impl<T> : IFace<T> where T : ArrayList {}
class Impl : IFace<ArrayListT> {}
Die Realisierung ist 'IrgendeinInterface' oder ein Unterinterface/Unterklasse davon.
interface IFace<T> where T : IrgendeinInterface {}
class Impl<T> : IFace<T> where T : ICloneable {}
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.
Für generische Klassen ergeben sich die gleiche Möglichkeiten wir für Interfaces.
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
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ß
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; } }
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.