Generische Klassen & Methoden
Generische Klassen und Methoden sind ein wichtiger Baustein in der Programmierung mit Java. Dabei werden Typ-Variablen eingeführt, die dann bei der Instantiierung der generischen Klassen oder beim Aufruf von generischen Methoden mit existierenden Typen konkretisiert werden ("Typ-Parameter").
Syntaktisch definiert man die Typ-Variablen in spitzen Klammern hinter dem Klassennamen bzw.
vor dem Rückgabetyp einer Methode: public class Stack<E> { }
und public <T> T foo(T m) { }
.
- (K1) Begriffe generischer Typ, parametrisierter Typ, formaler Typ-Parameter, Typ-Parameter
- (K3) Erstellen und Nutzen von generischen Klassen und Interfaces
- (K3) Erstellen und Nutzen von generischen Methoden
Generische Strukturen
Vector speicher = new Vector();
speicher.add(1); speicher.add(2); speicher.add(3);
speicher.add("huhu");
int summe = 0;
for (Object i : speicher) { summe += (Integer)i; }
Problem: Nutzung des "raw" Typs Vector
ist nicht typsicher!
- Mögliche Fehler fallen erst zur Laufzeit und u.U. erst sehr spät auf: Offenbar werden im obigen
Beispiel
int
-Werte erwartet, d.h. das Hinzufügen von"huhu"
ist vermutlich ein Versehen (wird vom Compiler aber nicht bemerkt) - Die Iteration über
speicher
kann nur allgemein alsObject
erfolgen, d.h. in der Schleife muss auf den vermuteten/gewünschten Typ gecastet werden: Hier würde dann der String"huhu"
Probleme zur Laufzeit machen
Vector<Integer> speicher = new Vector<Integer>();
speicher.add(1); speicher.add(2); speicher.add(3);
speicher.add("huhu");
int summe = 0;
for (Integer i : speicher) { summe += i; }
Vorteile beim Einsatz von Generics:
- Datenstrukturen/Algorithmen nur einmal implementieren, aber für unterschiedliche Typen nutzen
- Keine Vererbungshierarchie nötig
- Nutzung ist typsicher, Casting unnötig
- Geht nur für Referenztypen
- Beispiel: Collections-API
Generische Klassen/Interfaces definieren
-
Definition: "
<Typ>
" hinter Klassennamenpublic class Stack<E> { public E push(E item) { addElement(item); return item; } }
Stack<E>
=> Generische (parametrisierte) Klasse (auch: "generischer Typ")E
=> Formaler Typ-Parameter (auch: "Typ-Variable")
-
Einsatz:
Stack<Integer> stack = new Stack<Integer>();
Integer
=> Typ-ParameterStack<Integer>
=> Parametrisierter Typ
Generische Klassen instantiieren
-
Typ-Parameter in spitzen Klammern hinter Klasse bzw. Interface
ArrayList<Integer> il = new ArrayList<Integer>(); ArrayList<Double> dl = new ArrayList<Double>();
Beispiel I: Einfache generische Klassen
class Tutor<T> {
// T kann in Tutor *fast* wie Klassenname verwendet werden
private T x;
public T foo(T t) { ... }
}
Tutor<String> a = new Tutor<String>();
Tutor<Integer> b = new Tutor<>(); // ab Java7: "Diamond Operator"
a.foo("wuppie");
b.foo(1);
b.foo("huhu"); // Fehlermeldung vom Compiler
Typ-Inferenz
Typ-Parameter kann bei new()
auf der rechten Seite oft weggelassen werden
=> Typ-Inferenz
Tutor<String> x = new Tutor<>(); // <>: "Diamantoperator"
(gilt seit Java 1.7)
Beispiel II: Vererbung mit Typparametern
interface Fach<T1, T2> {
public void machWas(T1 a, T2 b);
}
class SHK<T> extends Tutor<T> { ... }
class PM<X, Y, Z> implements Fach<X, Z> {
public void machWas(X a, Z b) { ... }
public Y getBla() { ... }
}
class Studi<A,B> extends Person { ... }
class Properties extends Hashtable<Object,Object> { ... }
Auch Interfaces und abstrakte Klassen können parametrisierbar sein.
Bei der Vererbung sind alle Varianten bzgl. der Typ-Variablen denkbar. Zu beachten ist dabei vor allem, dass die Typ-Variablen der Oberklasse (gilt analog für Interfaces) entweder durch Typ-Variablen der Unterklasse oder durch konkrete Typen spezifiziert sind. Die Typ-Variablen der Oberklasse dürfen nicht "in der Luft hängen" (siehe auch nächste Folie)!
Beispiel III: Überschreiben/Überladen von Methoden
class Mensch { ... }
class Studi<T extends Mensch> {
public void f(T t) { ... }
}
class Prof<T> extends Mensch { ... }
class Tutor extends Studi<Mensch> {
public void f(Mensch t) { ... } // Ueberschreiben
public void f(Tutor t) { ... } // Ueberladen
}
Vorsicht: So geht es nicht!
class Foo<T> extends T { ... }
class Fluppie<T> extends Wuppie<S> { ... }
- Generische Klasse
Foo<T>
kann nicht selbst vom Typ-ParameterT
ableiten (warum?) - Bei Ableiten von generischer Klasse
Wuppie<S>
muss deren Typ-ParameterS
bestimmt sein: etwa durch den Typ-Parameter der ableitenden Klasse, beispielsweiseFluppie<S>
(stattFluppie<T>
)
Generische Methoden definieren
-
"
<Typ>
" vor Rückgabetyppublic class Mensch { public <T> T myst(T m, T n) { return Math.random() > 0.5 ? m : n; } }
-
"Mischen possible":
public class Mensch<E> { public <T> T myst(T m, T n) { ... } public String myst(String m, String n) { ... } }
Aufruf generischer Methoden
Aufruf
- Aufruf mit Typ-Parameter vor Methodennamen, oder
- Inferenz durch Compiler
Finden der richtigen Methode durch den Compiler
- Zuerst Suche nach exakt passender Methode,
- danach passend mit Konvertierungen => Compiler sucht gemeinsame Oberklasse in Typhierarchie
Beispiel
class Mensch {
<T> T myst(T m, T n) { ... }
}
Mensch m = new Mensch();
m.<String>myst("Essen", "lecker"); // Angabe Typ-Parameter
m.myst("Essen", 1); // String, Integer => T: Object
m.myst("Essen", "lecker"); // String, String => T: String
m.myst(1.0, 1); // Double, Integer => T: Number
Reihenfolge der Suche nach passender Methode gilt auch für nicht-generisch überladene Methoden
class Mensch {
public <T> T myst(T m, T n) {
System.out.println("X#myst: T");
return m;
}
// NICHT gleichzeitig erlaubt wg. Typ-Löschung (s.u.):
/*
public <T1, T2> T1 myst(T1 m, T2 n) {
System.out.println("X#myst: T");
return m;
}
*/
public String myst(String m, String n) {
System.out.println("X#myst: String");
return m;
}
public int myst(int m, int n) {
System.out.println("X#myst: int");
return m;
}
}
public class GenericMethods {
public static void main(String[] args) {
Mensch m = new Mensch();
m.myst("Hello World", "m");
m.myst("Hello World", 1);
m.myst(3, 4);
m.myst(m, m);
m.<Mensch>myst(m, m);
m.myst(m, 1);
m.myst(3.0, 4);
m.<Double>myst(3, 4);
}
}
Wrap-Up
-
Begriffe:
- Generischer Typ:
Stack<T>
- Formaler Typ-Parameter:
T
- Parametrisierter Typ:
Stack<Long>
- Typ-Parameter:
Long
- Raw Type:
Stack
- Generischer Typ:
-
Generische Klassen:
public class Stack<E> { }
- "
<Typ>
" hinter Klassennamen
- "
-
Generische Methoden:
public <T> T foo(T m) { }
- "
<Typ>
" vor Rückgabewert
- "
- [Bloch2018] Effective Java
Bloch, J., Addison-Wesley, 2018. ISBN 978-0-13-468599-1. - [Java-SE-Tutorial] The Java Tutorials
Oracle Corporation, 2022.
Specialized Trails: Generics - [LernJava] Learn Java
Oracle Corporation, 2022.
Kapitel Generics - [Ullenboom2021] Java ist auch eine Insel
Ullenboom, C., Rheinwerk-Verlag, 2021. ISBN 978-3-8362-8745-6.
Kapitel 11.1: Einführung in Java Generics