Subsections of Generics: Umgang mit parametrisierten Typen
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
Bounds & Wildcards
Typ-Variablen können weiter eingeschränkt werden, in dem man einen verpflichtenden
Ober- oder Untertyp angibt mit extends
bzw. super
. Damit muss der später
bei der Instantiierung verwendete Typ-Parameter entweder die Oberklasse selbst
sein oder davon ableiten (bei extends
) bzw. der Typ-Parameter muss eine Oberklasse
der angegebenen Schranke sein (super
).
Durch die Einschränkung mit extends
können in der Klasse/Methode auf der Typ-Variablen
alle Methoden des angegebenen Obertyps verwendet werden.
Ein Wildcard (?
) als Typ-Parameter steht für einen beliebigen Typ, wobei die
Typ-Variable keinen Namen bekommt und damit innerhalb der Klasse/Methode nicht
zugreifbar ist.
- (K3) Umgang mit Wildcards und Bounds bei generischen Klassen/Methoden
Bounds: Einschränken der generischen Typen
public class Cps<E extends Number> {
// Obere Schranke: E muss Number oder Subklasse sein
// => Zugriff auf Methoden aus Number moeglich
}
Cps<Double> a;
Cps<Number> b;
Cps<String> c; // Fehler!!!
-
Schlüsselwort
extends
gilt hier auch für Interfaces -
Mehrere Interfaces: nach
extends
Klasse oder Interface, danach mit "&
" getrennt die restlichen Interfaces:class Cps<E extends KlasseOderInterface & I1 & I2 & I3> {}
Anmerkung: Der Typ-Parameter ist analog auch mit super
(nach unten) einschränkbar
Wildcards: Dieser Typ ist mir nicht so wichtig
Wildcard mit "?
" => steht für unbestimmten Typ
public class Wuppie {
public void m1(List<?> a) { ... }
public void m2(List<? extends Number> b) { ... }
}
-
m1
:List
beliebig parametrisierbar => Inm1
für Objekte in Listea
nur Methoden vonObject
nutzbar! -
m2
:List
muss mitNumber
oder Subklasse parametrisiert werden. => Dadurch für Objekte in Listeb
alle Methoden vonNumber
nutzbar ...
Weitere Eigenschaften:
- Durch Wildcard kein Zugriff auf den Typ
- Wildcard kann durch upper bound eingeschränkt werden
- Geht nicht bei Klassen-/Interface-Definitionen
[Bloch2018]: Nur für Parameter und nicht für Rückgabewerte nutzen!
Hands-On: Ausgabe für generische Listen
Ausgabe für Listen gesucht, die sowohl Elemente der Klasse A
als auch
Elemente der Klasse B
enthalten können
class A { void printInfo() { System.out.println("A"); } }
class B extends A { void printInfo() { System.out.println("B"); } }
public class X {
public static void main(String[] args) {
List<A> x = new ArrayList<A>();
x.add(new A()); x.add(new B());
printInfo(x); // Klassenmethode in X, gesucht
List<B> y = new ArrayList<B>();
y.add(new B()); y.add(new B());
printInfo(y); // Klassenmethode in X, gesucht
}
}
Hinweis: Dieses Beispiel beinhaltet auch Polymorphie bei/mit generischen Datentypen, bitte vorher auch das Video zum vierten Teil "Generics und Polymorphie" anschauen
Erster Versuch (A und B und main() wie oben)
public class X {
public static void printInfo(List<A> list) {
for (A a : list) { a.printInfo(); }
}
}
=> So gehts nicht! Eine List<B>
ist keine List<A>
(auch wenn ein B
ein A
ist, vgl. spätere Sitzung zu Generics und
Vererbung ...)!
Zweiter Versuch mit Wildcards (A und B und main() wie oben)
public class X {
public static void printInfo(List<?> list) {
for (Object a : list) { a.printInfo(); }
}
}
=> So gehts auch nicht! Im Prinzip passt das jetzt
für List<A>
und List<B>
. Dummerweise hat man durch das Wildcard
keinen Zugriff mehr auf den Typ-Parameter und muss für den Typ der
Laufvariablen in der for
-Schleife dann Object
nehmen. Aber
Object
kennt unser printInfo
nicht ... Außerdem könnte man die
Methode X#printInfo
dank des Wildcards auch mit allen anderen
Typen aufrufen ...
Dritter Versuch (Lösung) mit Wildcards und Bounds (A und B und main() wie oben)
public class X {
public static void printInfo(List<? extends A> list) {
for (A a : list) { a.printInfo(); }
}
}
Das ist die Lösung. Man erlaubt als Argument nur List
-Objekte und fordert,
dass sie mit A
oder einer Unterklasse von A
parametrisiert sind. D.h.
in der Schleife kann man sich auf den gemeinsamen Obertyp A
abstützen
und hat dann auch wieder die printInfo
-Methode zur Verfügung ...
Wrap-Up
-
Ein Wildcard (
?
) als Typ-Parameter steht für einen beliebigen Typ- Ist in Klasse oder Methode dann aber nicht mehr zugreifbar
-
Mit Bounds kann man Typ-Parameter nach oben oder nach unten einschränken (im Sinne einer Vererbungshierarchie)
extends
: Der Typ-Parameter muss eine Unterklasse eines bestimmten Typen seinsuper
: Der Typ-Parameter muss eine Oberklasse eines bestimmten Typen sein
Spieler, Mannschaften und Ligen Modellieren Sie in Java verschiedene Spielertypen sowie generische Mannschaften und Ligen, die jeweils bestimmte Spieler (-typen) bzw. Mannschaften aufnehmen können.
-
Implementieren Sie die Klasse
Spieler
, die das InterfaceISpieler
erfüllt.public interface ISpieler { String getName(); }
-
Implementieren Sie die beiden Klassen
FussballSpieler
undBasketballSpieler
und sorgen Sie dafür, dass beide Klassen vom Compiler als Spieler betrachtet werden (geeignete Vererbungshierarchie). -
Betrachten Sie das nicht-generische Interface
IMannschaft
. Erstellen Sie daraus ein generisches InterfaceIMannschaft
mit einer Typ-Variablen. Stellen Sie durch geeignete Beschränkung der Typ-Variablen sicher, dass nur Mannschaften mit vonISpieler
abgeleiteten Spielern gebildet werden können.public interface IMannschaft { boolean aufnehmen(ISpieler spieler); boolean rauswerfen(ISpieler spieler); }
-
Betrachten Sie das nicht-generische Interface
ILiga
. Erstellen Sie daraus ein generisches InterfaceILiga
mit einer Typvariablen. Stellen Sie durch geeignete Beschränkung der Typvariablen sicher, dass nur Ligen mit vonIMannschaft
abgeleiteten Mannschaften angelegt werden können.public interface ILiga { boolean aufnehmen(IMannschaft mannschaft); boolean rauswerfen(IMannschaft mannschaft); }
-
Leiten Sie von
ILiga
das generische InterfaceIBundesLiga
ab. Stellen Sie durch geeignete Formulierung der Typvariablen sicher, dass nur Ligen mit Mannschaften angelegt werden können, deren Spieler vom TypFussballSpieler
(oder abgeleitet) sind.Realisieren Sie nun noch die Funktionalität von
IBundesLiga
als nicht-generisches InterfaceIBundesLiga2
.
- [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.3
Type Erasure
Generics existieren eigentlich nur auf Quellcode-Ebene. Nach der Typ-Prüfung etc.
entfernt der Compiler alle generischen Typ-Parameter und alle <...>
(=>
"Type-Erasure"), d.h. im Byte-Code stehen nur noch Raw-Typen bzw. die oberen
Typ-Schranken der Typ-Parameter, in der Regel Object
. Zusätzlich baut der Compiler
die nötigen Casts ein. Als Anwender merkt man davon nichts, muss das "Type-Erasure"
wegen der Auswirkungen aber auf dem Radar haben!
- (K2) Typ-Löschung und Auswirkungen
Typ-Löschung (Type-Erasure)
Der Compiler ersetzt nach Prüfung der Typen und ihrer Verwendung alle Typ-Parameter durch
- deren obere (Typ-)Schranke und
- passende explizite Cast-Operationen (im Byte-Code).
Die obere Typ-Schranke ist in der Regel der Typ der ersten Bounds-Klausel
oder Object
, wenn keine Einschränkungen formuliert sind.
Bei parametrisierten Typen wie List<T>
wird der Typ-Parameter entfernt,
es entsteht ein sogenannter Raw-Typ (List
, quasi implizit mit Object
parametrisiert).
=> Ergebnis: Nur eine (untypisierte) Klasse! Zur Laufzeit gibt es keine Generics mehr!
Hinweis: In C++ ist man den anderen möglichen Weg gegangen und erzeugt für jede Instantiierung die passende Klasse. Siehe Modul "Systemprogrammierung" :)
Beispiel: Aus dem folgenden harmlosen Code-Fragment:
class Studi<T> {
T myst(T m, T n) { return n; }
public static void main(String[] args) {
Studi<Integer> a = new Studi<>();
int i = a.myst(1, 3);
}
}
wird nach der Typ-Löschung durch Compiler (das steht dann quasi im Byte-Code):
class Studi {
Object myst(Object m, Object n) { return n; }
public static void main(String[] args) {
Studi a = new Studi();
int i = (Integer) a.myst(1, 3);
}
}
Die obere Schranke meist Object
=> new T()
verboten/sinnfrei (s.u.)!
Type-Erasure bei Nutzung von Bounds
vor der Typ-Löschung durch Compiler:
class Cps<T extends Number> {
T myst(T m, T n) {
return n;
}
public static void main(String[] args) {
Cps<Integer> a = new Cps<>();
int i = a.myst(1, 3);
}
}
nach der Typ-Löschung durch Compiler:
class Cps {
Number myst(Number m, Number n) {
return n;
}
public static void main(String[] args) {
Cps a = new Cps();
int i = (Integer) a.myst(1, 3);
}
}
Raw-Types: Ich mag meine Generics "well done" :-)
Raw-Types: Instanziierung ohne Typ-Parameter => Object
Stack s = new Stack(); // Stack von Object-Objekten
- Wegen Abwärtskompatibilität zu früheren Java-Versionen noch erlaubt.
- Nutzung wird nicht empfohlen! (Warum?)
Anmerkung
Raw-Types darf man zwar selbst im Quellcode verwenden (so wie im Beispiel
hier), sollte die Verwendung aber vermeiden wegen der Typ-Unsicherheit:
Der Compiler sieht im Beispiel nur noch einen Stack für Object
, d.h. dort
dürfen Objekte aller Typen abgelegt werden - es kann keine Typprüfung
durch den Compiler stattfinden. Auf einem Stack<String>
kann der Compiler
prüfen, ob dort wirklich nur String
-Objekte abgelegt werden und ggf.
entsprechend Fehler melden.
Etwas anderes ist es, dass der Compiler im Zuge von Type-Erasure selbst Raw-Types in den Byte-Code schreibt. Da hat er vorher bereits die Typsicherheit geprüft und er baut auch die passenden Casts ein.
Das Thema ist eigentlich nur noch aus Kompatibilität zu Java5 oder früher da, weil es dort noch keine Generics gab (wurden erst mit Java6 eingeführt).
Folgen der Typ-Löschung: new
new
mit parametrisierten Klassen ist nicht erlaubt!
class Fach<T> {
public T foo() {
return new T(); // nicht erlaubt!!!
}
}
Grund: Zur Laufzeit keine Klasseninformationen über T
mehr
Im Code steht return (CAST) new Object();
. Das neue Object
kann man anlegen, aber ein Cast nach irgendeinem anderen Typ
ist sinnfrei: Jede Klasse ist ein Untertyp von Object
, aber
eben nicht andersherum. Außerdem fehlt dem Objekt vom Typ
Object
auch sämtliche Information und Verhalten, die der
Cast-Typ eigentlich mitbringt ...
Folgen der Typ-Löschung: static
static
mit generischen Typen ist nicht erlaubt!
class Fach<T> {
static T t; // nicht erlaubt!!!
static Fach<T> c; // nicht erlaubt!!!
static void foo(T t) { ... }; // nicht erlaubt!!!
}
Fach<String> a;
Fach<Integer> b;
Grund: Compiler generiert nur eine Klasse! Beide Objekte würden sich die statischen Attribute teilen (Typ zur Laufzeit unklar!).
Hinweis: Generische (statische) Methoden sind erlaubt.
Folgen der Typ-Löschung: instanceof
instanceof
mit parametrisierten Klassen ist nicht erlaubt!
class Fach<T> {
void printType(Fach<?> p) {
if (p instanceof Fach<Number>)
...
else if (p instanceof Fach<String>)
...
}
}
Grund: Unsinniger Code nach Typ-Löschung:
class Fach {
void printType(Fach p) {
if (p instanceof Fach)
...
else if (p instanceof Fach)
...
}
}
Folgen der Typ-Löschung: .class
.class
mit parametrisierten Klassen ist nicht erlaubt!
boolean x;
List<String> a = new ArrayList<String>();
List<Integer> b = new ArrayList<Integer>();
x = (List<String>.class == List<Integer>.class); // Compiler-Fehler
x = (a.getClass() == b.getClass()); // true
Grund: Es gibt nur List.class
(und kein List<String>.class
bzw. List<Integer>.class
)!
Wrap-Up
- Generics existieren eigentlich nur auf Quellcode-Ebene
- "Type-Erasure":
- Compiler entfernt nach Typ-Prüfungen etc.
generische Typ-Parameter etc. => im Byte-Code nur noch Raw-Typen
bzw. die oberen Typ-Schranken der Typ-Parameter, in der Regel
Object
- Compiler baut passende Casts in Byte-Code ein
- Transparent für User; Auswirkungen beachten!
- Compiler entfernt nach Typ-Prüfungen etc.
generische Typ-Parameter etc. => im Byte-Code nur noch Raw-Typen
bzw. die oberen Typ-Schranken der Typ-Parameter, in der Regel
- [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.2 und 11.6
Generics und Polymorphie
Auch mit generischen Klassen stehen die Mechanismen Vererbung und Überladen zur Verfügung. Dabei muss
aber beachtet werden, dass generische Klassen sich "invariant" verhalten: Der Typ selbst folgt der
Vererbungsbeziehung, eine Vererbung des Typ-Parameters begründet keine Vererbungsbeziehung! D.h.
aus U extends O
folgt nicht A<U> extends A<O>
.
Bei Arrays ist es genau anders herum: Wenn U extends O
dann gilt auch U[] extends O[]
... (Dies
nennt man "kovariantes" Verhalten.)
- (K3) Vererbungsbeziehungen mit generischen Klassen
- (K3) Umgang mit Arrays und generischen Typen
Generische Polymorphie
B<E> extends A<E>
class A<E> { ... }
class B<E> extends A<E> { ... }
A<Double> ad = new B<Double>();
A<String> as = new B<String>();
class Vector<E> { ... }
class Stack<E> extends Vector<E> { ... }
Vector<Double> vd = new Stack<Double>();
Vector<String> vs = new Stack<String>();
=> Polymorphie bei Generics bezieht sich auf Typ (nicht Typ-Parameter)
Invarianz: Generics sind invariant, d.h. ein HashSet<String>
ist ein
Untertyp von Set<String>
. Bei der Vererbung muss der Typ-Parameter identisch
sein.
Polymorphie bei Generics bezieht sich nur auf Typ!
"B extends A
" bedeutet nicht "C<B> extends C<A>
"
Stack<Number> s = new Stack<Integer>(); // DAS GEHT SO NICHT!
// Folgen (wenn obiges gehen wuerde):
s.push(new Integer(3)); // das ginge sowieso ...
// Folgen (wenn obiges gehen wuerde):
// Stack<Number> waere Oberklasse auch von Stack<Double>
s.push(new Double(2.0)); // waere dann auch erlaubt ...
// Das Objekt (Stack<Integer>) kann aber keine Double speichern!
// Zur Laufzeit keine Typ-Informationen mehr!
- Typ-Löschung => zur Laufzeit keine Typinformationen vorhanden
- Compiler muss Typen prüfen (können)!
Abgrenzung: Polymorphie bei Arrays
Wenn "B extends A
" dann "B[] extends A[]
"
Object[] x = new String[] {"Hello", "World", ":-)"};
x[0] = "Hallo";
x[0] = new Double(2.0); // Laufzeitfehler
String[] y = x; // String[] ist KEIN Object[]!!!
- Arrays besitzen Typinformationen über gespeicherte Elemente
- Prüfung auf Typ-Kompatibilität zur Laufzeit (nicht Kompilierzeit!)
Arrays gab es sehr früh, Generics erst relativ spät (ab Java6) => bei Arrays fand man das Verhalten natürlich und pragmatisch (trotz der Laufzeit-Überprüfung).
Bei der Einführung von Generics musste man Kompatibilität sicherstellen (alter Code soll auch mit neuen Compilern übersetzt werden können - obwohl im alten Code Raw-Types verwendet werden). Außerdem wollte man von Laufzeit-Prüfung hin zu Compiler-Prüfung. Da würde das von Arrays bekannte Verhalten Probleme machen ...
Kovarianz: Arrays sind kovariant, d.h. ein Array vom Typ String[]
ist wegen
String extends Object
ein Untertyp von Object[]
.
Arrays vs. parametrisierte Klassen
=> Keine Arrays mit parametrisierten Klassen!
Foo<String>[] x = new Foo<String>[2]; // Compilerfehler
Foo<String[]> y = new Foo<String[]>(); // OK :)
Arrays mit parametrisierten Klassen sind nicht erlaubt! Arrays brauchen zur Laufzeit Typinformationen, die aber durch die Typ-Löschung entfernt werden.
Diskussion Vererbung vs. Generics
Vererbung:
- IS-A-Beziehung
- Anwendung: Vererbungsbeziehung vorliegend, Eigenschaften verfeinern
- Beispiel: Ein Student ist eine Person
Generics:
- Schablone (Template) für viele Datentypen
- Anwendung: Identischer Code für unterschiedliche Typen
- Beispiel: Datenstrukturen, Algorithmen generisch realisieren
Wrap-Up
-
Generics: Vererbung und Überladen möglich, aber: Aus "
U extends O
" folgt nicht "A<U> extends A<O>
" -
Achtung: Bei Arrays gilt aber: Wenn "
U extends O
" dann gilt auch "U[] extends O[]
" ...
- [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.5