Powody tego są oparte na tym, jak Java implementuje typy generyczne.
Przykład tablic
Dzięki tablicom możesz to zrobić (tablice są kowariantne)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Ale co by się stało, gdybyś spróbował to zrobić?
myNumber[0] = 3.14; //attempt of heap pollution
Ta ostatnia linia skompilowałaby się dobrze, ale jeśli uruchomisz ten kod, możesz uzyskać plik ArrayStoreException. Ponieważ próbujesz wstawić podwójną do tablicy liczb całkowitych (niezależnie od tego, czy uzyskasz dostęp przez odwołanie liczbowe).
Oznacza to, że możesz oszukać kompilator, ale nie możesz oszukać systemu typów środowiska uruchomieniowego. Dzieje się tak, ponieważ tablice nazywamy typami reifiable . Oznacza to, że w czasie wykonywania Java wie, że ta tablica została faktycznie utworzona jako tablica liczb całkowitych, do której dostęp uzyskuje się przez odwołanie typu Number[].
Jak więc widać, jedną rzeczą jest rzeczywisty typ obiektu, a inną typ odniesienia, którego używasz, aby uzyskać do niego dostęp, prawda?
Problem z generycznymi językami Java
Problem z typami ogólnymi Java polega na tym, że kompilator odrzuca informacje o typie i nie są one dostępne w czasie wykonywania. Ten proces nazywa się wymazywaniem typu . Jest dobry powód do implementowania takich typów generycznych w Javie, ale to długa historia i musi to być związane, między innymi, z binarną zgodnością z wcześniej istniejącym kodem (zobacz Jak mamy dostępne typy generyczne ).
Ale ważne jest tutaj to, że ponieważ w czasie wykonywania nie ma informacji o typie, nie ma sposobu, aby upewnić się, że nie popełnimy zanieczyszczenia sterty.
Na przykład,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution
Jeśli kompilator Java nie powstrzyma Cię przed tym, system typów środowiska wykonawczego również nie może Cię powstrzymać, ponieważ w czasie wykonywania nie ma sposobu, aby określić, że ta lista miała być tylko listą liczb całkowitych. Środowisko wykonawcze Javy pozwoliłoby umieścić na tej liście cokolwiek zechcesz, kiedy powinna zawierać tylko liczby całkowite, ponieważ podczas tworzenia została zadeklarowana jako lista liczb całkowitych.
W związku z tym projektanci Java upewnili się, że nie można oszukać kompilatora. Jeśli nie możesz oszukać kompilatora (tak jak możemy to zrobić z tablicami), nie możesz również oszukać systemu typów wykonawczych.
W związku z tym mówimy, że typy generyczne nie podlegają ponownej wymianie .
Najwyraźniej utrudniłoby to polimorfizm. Rozważmy następujący przykład:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Teraz możesz go użyć w ten sposób:
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));
Ale jeśli spróbujesz zaimplementować ten sam kod w kolekcjach ogólnych, nie odniesiesz sukcesu:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Otrzymasz błędy kompilatora, jeśli spróbujesz ...
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
Rozwiązaniem jest nauczenie się korzystania z dwóch potężnych funkcji generycznych języka Java, znanych jako kowariancja i kontrawariancja.
Kowariancja
Dzięki kowariancji możesz czytać elementy ze struktury, ale nie możesz niczego do niej zapisać. To wszystko są ważne deklaracje.
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();
Możesz przeczytać myNums:
Number n = myNums.get(0);
Ponieważ możesz być pewien, że cokolwiek zawiera rzeczywista lista, można ją przesłać dalej do liczby (w końcu wszystko, co rozszerza liczbę, jest liczbą, prawda?)
Nie możesz jednak umieszczać niczego w kowariantnej strukturze.
myNumst.add(45L); //compiler error
Nie byłoby to dozwolone, ponieważ Java nie może zagwarantować, jaki jest rzeczywisty typ obiektu w strukturze ogólnej. Może to być wszystko, co rozszerza Number, ale kompilator nie może być tego pewien. Możesz więc czytać, ale nie pisać.
Sprzeczność
Z kontrawariancją możesz zrobić odwrotnie. Możesz umieścić rzeczy w ogólnej strukturze, ale nie możesz z niej odczytać.
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);
W tym przypadku rzeczywistą naturą obiektu jest lista obiektów, a przez kontrawariancję możesz wstawić do niej liczby, zasadniczo dlatego, że wszystkie liczby mają wspólnego przodka obiektu. Jako takie, wszystkie liczby są obiektami i dlatego jest to poprawne.
Jednak nie możesz bezpiecznie odczytać niczego z tej kontrawariantnej struktury, zakładając, że otrzymasz liczbę.
Number myNum = myNums.get(0); //compiler-error
Jak widać, gdyby kompilator zezwolił na napisanie tej linii, w czasie wykonywania wystąpiłby wyjątek ClassCastException.
Zasada Get / Put
W związku z tym użyj kowariancji, gdy zamierzasz tylko wyprowadzić wartości ogólne ze struktury, użyj kontrawariancji, gdy zamierzasz umieścić tylko wartości ogólne w strukturze i użyj dokładnego typu ogólnego, jeśli zamierzasz zrobić jedno i drugie.
Najlepszym przykładem, jaki mam, jest następujący, który kopiuje wszelkiego rodzaju liczby z jednej listy do innej listy. To tylko staje się przedmioty od źródła, a jedynie stawia pozycji w cel.
public static void copy(List<? extends Number> source, List<? super Number> target) {
for(Number number : source) {
target(number);
}
}
Dzięki możliwościom kowariancji i kontrawariancji działa to w takim przypadku:
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);