Die Gründe dafür hängen davon ab, wie Java Generika implementiert.
Ein Arrays-Beispiel
Mit Arrays können Sie dies tun (Arrays sind kovariant)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Aber was würde passieren, wenn Sie dies versuchen?
myNumber[0] = 3.14; //attempt of heap pollution
Diese letzte Zeile würde gut kompiliert werden, aber wenn Sie diesen Code ausführen, könnten Sie eine bekommen ArrayStoreException
. Weil Sie versuchen, ein Double in ein Integer-Array einzufügen (unabhängig davon, ob Sie über eine Zahlenreferenz darauf zugreifen).
Dies bedeutet, dass Sie den Compiler täuschen können, aber Sie können das Laufzeitsystem nicht täuschen. Und das ist so, weil Arrays das sind, was wir als reifizierbare Typen bezeichnen . Dies bedeutet, dass Java zur Laufzeit weiß, dass dieses Array tatsächlich als Array von Ganzzahlen instanziiert wurde, auf die einfach über eine Referenz vom Typ zugegriffen wird Number[]
.
Wie Sie sehen, ist eine Sache der tatsächliche Typ des Objekts und eine andere die Art der Referenz, mit der Sie darauf zugreifen, oder?
Das Problem mit Java Generics
Das Problem bei generischen Java-Typen besteht nun darin, dass die Typinformationen vom Compiler verworfen werden und zur Laufzeit nicht verfügbar sind. Dieser Vorgang wird als Typlöschung bezeichnet . Es gibt gute Gründe, solche Generika in Java zu implementieren, aber das ist eine lange Geschichte, und sie hat unter anderem mit der binären Kompatibilität mit bereits vorhandenem Code zu tun (siehe Wie wir die Generika erhalten haben, die wir haben ).
Der wichtige Punkt hierbei ist jedoch, dass es zur Laufzeit keine Typinformationen gibt und es keine Möglichkeit gibt, sicherzustellen, dass wir keine Haufenverschmutzung begehen.
Zum Beispiel,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution
Wenn der Java-Compiler Sie nicht daran hindert, kann Sie das Laufzeitsystem auch nicht daran hindern, da zur Laufzeit nicht festgestellt werden kann, dass diese Liste nur eine Liste von Ganzzahlen sein sollte. Mit der Java-Laufzeit können Sie alles, was Sie wollen, in diese Liste einfügen, wenn sie nur Ganzzahlen enthalten soll, da sie beim Erstellen als Ganzzahlliste deklariert wurde.
Daher haben die Entwickler von Java sichergestellt, dass Sie den Compiler nicht täuschen können. Wenn Sie den Compiler nicht täuschen können (wie wir es mit Arrays tun können), können Sie auch das Laufzeitsystem nicht täuschen.
Daher sagen wir, dass generische Typen nicht reifizierbar sind .
Offensichtlich würde dies den Polymorphismus behindern. Betrachten Sie das folgende Beispiel:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Jetzt können Sie es so verwenden:
Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));
Wenn Sie jedoch versuchen, denselben Code mit generischen Sammlungen zu implementieren, ist dies nicht erfolgreich:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Sie würden Compiler-Fehler bekommen, wenn Sie versuchen ...
List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error
Die Lösung besteht darin, zu lernen, zwei leistungsstarke Funktionen von Java-Generika zu verwenden, die als Kovarianz und Kontravarianz bekannt sind.
Kovarianz
Mit Kovarianz können Sie Elemente aus einer Struktur lesen, aber nichts hineinschreiben. All dies sind gültige Erklärungen.
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();
Und Sie können lesen aus myNums
:
Number n = myNums.get(0);
Da Sie sicher sein können, dass die aktuelle Liste in eine Nummer hochgesendet werden kann (schließlich ist alles, was die Nummer erweitert, eine Nummer, oder?)
Sie dürfen jedoch nichts in eine kovariante Struktur einfügen.
myNumst.add(45L); //compiler error
Dies wäre nicht zulässig, da Java nicht garantieren kann, welcher Objekttyp in der generischen Struktur tatsächlich vorhanden ist. Es kann alles sein, was Number erweitert, aber der Compiler kann nicht sicher sein. Sie können also lesen, aber nicht schreiben.
Kontravarianz
Mit Kontravarianz können Sie das Gegenteil tun. Sie können Dinge in eine generische Struktur einfügen, aber Sie können nicht daraus vorlesen.
List<Object> myObjs = new List<Object>();
myObjs.add("Luke");
myObjs.add("Obi-wan");
List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);
In diesem Fall ist die tatsächliche Natur des Objekts eine Liste von Objekten, und durch Kontravarianz können Sie Zahlen einfügen, im Grunde genommen, weil alle Zahlen Objekt als gemeinsamen Vorfahren haben. Als solche sind alle Zahlen Objekte, und daher ist dies gültig.
Sie können jedoch nichts aus dieser kontravarianten Struktur sicher lesen, vorausgesetzt, Sie erhalten eine Zahl.
Number myNum = myNums.get(0); //compiler-error
Wie Sie sehen können, erhalten Sie zur Laufzeit eine ClassCastException, wenn der Compiler Ihnen das Schreiben dieser Zeile erlaubt.
Get / Put-Prinzip
Verwenden Sie daher die Kovarianz, wenn Sie nur generische Werte aus einer Struktur entfernen möchten, verwenden Sie die Kontravarianz, wenn Sie nur generische Werte in eine Struktur einfügen möchten, und verwenden Sie den genauen generischen Typ, wenn Sie beides ausführen möchten.
Das beste Beispiel, das ich habe, ist das folgende, das jede Art von Zahlen von einer Liste in eine andere Liste kopiert. Es nur bekommt Elemente aus der Quelle, und es nur bringt Elemente im Ziel.
public static void copy(List<? extends Number> source, List<? super Number> target) {
for(Number number : source) {
target(number);
}
}
Dank der Kraft der Kovarianz und Kontravarianz funktioniert dies für einen Fall wie diesen:
List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);