Tablice są kowariantne
Mówi się, że tablice są kowariantne, co w zasadzie oznacza, że biorąc pod uwagę reguły podtypów Java, tablica typu T[]
może zawierać elementy typu T
lub dowolnego podtypu T
. Na przykład
Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);
Ale nie tylko to, reguły Java dla podtypów również stwierdzają, że tablica S[]
jest podtypem tablicy, T[]
jeśli S
jest podtypem T
, dlatego też coś takiego jest również poprawne:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Ponieważ zgodnie z regułami podtypów w Javie tablica Integer[]
jest podtypem tablicy, Number[]
ponieważ liczba całkowita jest podtypem liczby.
Ale ta zasada podsieci może prowadzić do interesującego pytania: co by się stało, gdybyśmy spróbowali to zrobić?
myNumber[0] = 3.14; //attempt of heap pollution
Ta ostatnia linia byłaby dobrze skompilowana, ale gdybyśmy uruchomili ten kod, otrzymalibyśmy, ArrayStoreException
ponieważ próbujemy umieścić podwójny w tablicy liczb całkowitych. Fakt, że uzyskujemy dostęp do tablicy za pomocą odwołania do liczby, nie ma tutaj znaczenia, ważne jest to, że tablica jest tablicą liczb całkowitych.
Oznacza to, że możemy oszukać kompilator, ale nie możemy oszukać systemu typu wykonawczego. A to dlatego, że tablice są tym, co nazywamy typem możliwym do ponownego zdefiniowania. Oznacza to, że w czasie wykonywania Java wie, że ta tablica została faktycznie utworzona jako tablica liczb całkowitych, do których po prostu można uzyskać dostęp poprzez odwołanie typu Number[]
.
Tak więc, jak widzimy, jedna rzecz to faktyczny typ obiektu, a inna to rodzaj odwołania, którego używamy do uzyskania dostępu, prawda?
Problem z Generics Java
Problem z rodzajami rodzajowymi w Javie polega na tym, że kompilator porzuca kompilację informacji o typie dla parametrów typu; dlatego te informacje tego typu nie są dostępne w czasie wykonywania. Ten proces nazywa się kasowaniem typu . Istnieją dobre powody, aby implementować takie generyczne w Javie, ale to długa historia i ma ona związek z binarną zgodnością z istniejącym kodem.
Ważną kwestią jest to, że ponieważ w czasie wykonywania nie ma informacji o typie, nie ma sposobu, aby zapewnić, że nie popełniamy zanieczyszczenia hałdy.
Rozważmy teraz następujący niebezpieczny kod:
List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Jeśli kompilator Java nie powstrzyma nas przed zrobieniem tego, system typów wykonawczych też nas nie powstrzyma, ponieważ w czasie wykonywania nie ma możliwości ustalenia, że ta lista miała być tylko liczbą całkowitą. Środowisko wykonawcze Java pozwoliłoby nam umieścić na tej liście wszystko, co chcemy, gdy powinno ono zawierać tylko liczby całkowite, ponieważ kiedy zostało utworzone, zostało zadeklarowane jako lista liczb całkowitych. Dlatego kompilator odrzuca wiersz 4, ponieważ jest niebezpieczny i jeśli jest dozwolony, może złamać założenia systemu typów.
W związku z tym projektanci Java upewnili się, że nie możemy oszukać kompilatora. Jeśli nie możemy oszukać kompilatora (tak jak w przypadku tablic), nie możemy też oszukać systemu typu wykonawczego.
W związku z tym mówimy, że typy ogólne nie podlegają zwrotowi, ponieważ w czasie wykonywania nie możemy ustalić prawdziwej natury typu ogólnego.
Pominąłem niektóre części tych odpowiedzi, możesz przeczytać cały artykuł tutaj:
https://dzone.com/articles/covariance-and-contravariance