Dlaczego tablice są kowariantne, ale typy ogólne są niezmienne?

160

Z książki Effective Java Joshua Blocha,

  1. Tablice różnią się od typu ogólnego na dwa ważne sposoby. Pierwsze tablice są kowariantne. Typy generyczne są niezmienne.
  2. Kowariantny oznacza po prostu, że jeśli X jest podtypem Y, to X [] będzie również podtypem Y []. Tablice są kowariantne, ponieważ łańcuch jest podtypem obiektu So

    String[] is subtype of Object[]

    Niezmienny oznacza po prostu niezależnie od tego, czy X jest podtypem Y, czy nie,

     List<X> will not be subType of List<Y>.

Moje pytanie brzmi: dlaczego podjęto decyzję o kowariantności tablic w Javie? Istnieją inne posty SO, takie jak Dlaczego tablice są niezmienne, ale listy są kowariantne? , ale wydają się być skupieni na Scali i nie jestem w stanie nadążyć.

chętny do nauki
źródło
1
Czy to nie dlatego, że typy ogólne zostały dodane później?
Sotirios Delimanolis,
1
Myślę, że porównywanie tablic i kolekcji jest niesprawiedliwe, kolekcje używają tablic w tle !!
Ahmed Adel Ismail
4
@ EL-conteDe-monteTereBentikh Na przykład nie wszystkie kolekcje LinkedList.
Paul Bellora,
@PaulBellora Wiem, że Mapy różnią się od tych, które implementują Kolekcję, ale przeczytałem w SCPJ6, że Kolekcje zasadniczo polegały na tablicach !!
Ahmed Adel Ismail
Ponieważ nie ma wyjątku ArrayStoreException; podczas wstawiania niewłaściwego elementu w Collection, jeśli ma go tablica. Więc Collection może znaleźć to tylko w czasie odzyskiwania, a także z powodu rzucania. Tak więc leki generyczne zapewnią rozwiązanie tego problemu.
Kanagavelu Sugumar

Odpowiedzi:

150

Przez wikipedię :

Wczesne wersje Javy i C # nie zawierały typów ogólnych (czyli polimorfizmu parametrycznego).

W takim ustawieniu niezmienność tablic wyklucza użyteczne programy polimorficzne. Na przykład rozważ napisanie funkcji do tasowania tablicy lub funkcji, która testuje dwie tablice pod kątem równości przy użyciu Object.equalsmetody na elementach. Implementacja nie zależy od dokładnego typu elementu przechowywanego w tablicy, więc powinno być możliwe napisanie pojedynczej funkcji działającej na wszystkich typach tablic. Implementacja funkcji typu jest łatwa

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

Jeśli jednak typy tablicowe byłyby traktowane jako niezmienne, byłoby możliwe wywołanie tych funkcji tylko na tablicy dokładnie tego typu Object[]. Nie można na przykład przetasować tablicy ciągów.

Dlatego zarówno Java, jak i C # traktują typy tablicowe jako kowariantne. Na przykład w C # string[]jest podtypem object[], aw Javie String[]jest podtypem Object[].

Odpowiada to na pytanie „Dlaczego tablice są kowariantne?”, A dokładniej: „Dlaczego w tamtym czasie tablice były kowariantne ?”

Kiedy wprowadzono leki generyczne, celowo nie uczyniono ich kowariantnymi z powodów wskazanych w tej odpowiedzi przez Jona Skeeta :

Nie, a List<Dog>nie jest List<Animal>. Zastanów się, co możesz zrobić z List<Animal>- możesz dodać do niego dowolne zwierzę ... w tym kota. Czy możesz logicznie dodać kota do miotu szczeniąt? Absolutnie nie.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

Nagle masz bardzo zdezorientowanego kota.

Oryginalna motywacja do tworzenia kowariantnych tablic opisana w artykule na Wikipedii nie dotyczyła typów ogólnych, ponieważ symbole wieloznaczne umożliwiały wyrażenie kowariancji (i kontrawariancji), na przykład:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
Paul Bellora
źródło
3
tak, Arrays pozwala na zachowanie polimorficzne, jednak wprowadza wyjątki w czasie wykonywania (w przeciwieństwie do wyjątków czasu kompilacji z typami ogólnymi). np .:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagertoLearn
21
To jest poprawne. Tablice mają typy reifable i rzucają ArrayStoreExceptionw razie potrzeby. Najwyraźniej uznano to wówczas za godny kompromis. Porównajmy to ze współczesnością: z perspektywy czasu wielu uważa kowariancję tablic za błąd.
Paul Bellora,
1
Dlaczego „wielu” uważa to za błąd? Jest to o wiele bardziej przydatne niż brak kowariancji tablic. Jak często widzieliście wyjątek ArrayStoreException; są dość rzadkie. Ironia polega na tym, że imo ... jednym z najgorszych błędów, jakie kiedykolwiek popełniono w Javie, jest wariancja użytkowa witryny, czyli symbole wieloznaczne.
Scott
3
@ScottMcKinney: "Dlaczego" wielu "uważa to za błąd?" AIUI, dzieje się tak, ponieważ kowariancja tablic wymaga dynamicznych testów typu dla wszystkich operacji przypisywania tablic (chociaż optymalizacje kompilatora mogą pomóc?), Co może powodować znaczne obciążenie środowiska uruchomieniowego.
Dominique Devriese
Dzięki, Dominique, ale z moich obserwacji wynika, że ​​powodem, dla którego „wielu” uważa to za błąd, jest raczej papugowanie tego, co powiedziało kilku innych. Ponownie, biorąc świeże spojrzenie na kowariancję tablic, jest ona znacznie bardziej przydatna niż niszczenie. Ponownie, faktycznym WIELKIM błędem popełnionym przez Javę była ogólna wariancja typu użycie-witryna za pomocą symboli wieloznacznych. To spowodowało więcej problemów, niż myślę, że „wielu” chce przyznać.
Scott
30

Powodem jest to, że każda tablica zna swój typ elementu w czasie wykonywania, a kolekcja ogólna nie z powodu wymazywania typów.

Na przykład:

String[] strings = new String[2];
Object[] objects = strings;  // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime

Jeśli było to dozwolone w przypadku kolekcji ogólnych:

List<String> strings = new ArrayList<String>();
List<Object> objects = strings;  // let's say it is valid
objects.add(12);  // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this

Ale spowodowałoby to problemy później, gdy ktoś próbowałby uzyskać dostęp do listy:

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
Katona
źródło
Myślę, że odpowiedź Paula Bellory jest bardziej odpowiednia, gdy komentuje DLACZEGO tablice są kowariantne. Jeśli tablice stały się niezmienne, to jest w porządku. miałbyś z tym wymazywanie typu. Głównym powodem wystąpienia właściwości Erasure typu jest zgodność z poprzednimi wersjami, prawda?
eagertoLearn
@ user2708477, tak, wymazywanie typu zostało wprowadzone z powodu wstecznej kompatybilności. I tak, moja odpowiedź próbuje odpowiedzieć na pytanie w tytule, dlaczego leki generyczne są niezmienne.
Katona,
Fakt, że tablice znają swój typ, oznacza, że ​​podczas gdy kowariancja pozwala kodowi poprosić o zapisanie czegoś w tablicy, w której nie będzie pasować - nie oznacza to, że taki magazyn będzie mógł mieć miejsce. W związku z tym poziom zagrożenia wynikający z kowariantności tablic jest znacznie mniejszy niż byłby, gdyby nie znali swoich typów.
supercat
@supercat, zgadza się, chciałem zwrócić uwagę na to, że w przypadku typów generycznych z wymazywaniem typu kowariancja nie mogła zostać zaimplementowana przy minimalnym bezpieczeństwie sprawdzeń w czasie wykonywania
Katona
1
Osobiście uważam, że ta odpowiedź dostarcza właściwego wyjaśnienia, dlaczego tablice są kowariantne, kiedy kolekcje nie mogą być. Dzięki!
asgs
22

Może ta pomoc: -

Leki generyczne nie są kowariantne

Tablice w języku Java są kowariantne - co oznacza, że ​​jeśli Integer rozszerza liczbę (co robi), to nie tylko liczba całkowita jest również liczbą, ale liczba całkowita [] jest również a Number[], a Ty możesz podać lub przypisać Integer[]gdzie Number[]jest wezwany. (Bardziej formalnie, jeśli liczba jest nadtypem liczby całkowitej, to Number[]jest nadtypem Integer[]). Można by pomyśleć, że to samo dotyczy typów ogólnych - to List<Number>jest nadtyp List<Integer>i że można przekazać a List<Integer>tam, gdzie List<Number>oczekuje się a. Niestety to tak nie działa.

Okazuje się, że jest dobry powód, dla którego nie działa to w ten sposób: złamałoby to typowe zabezpieczenia, które miały zapewniać. Wyobraź sobie, że możesz przypisać a List<Integer>do List<Number>. Następnie poniższy kod pozwoliłby ci wstawić coś, co nie było liczbą całkowitą, do List<Integer>:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

Ponieważ ln to a List<Number>, dodanie do niego Float wydaje się całkowicie legalne. Ale gdyby ln był aliasowany z li, to złamałoby to obietnicę bezpieczeństwa typu zawartą w definicji li - że jest to lista liczb całkowitych, dlatego typy rodzajowe nie mogą być kowariantne.

Rahul Tripathi
źródło
3
W przypadku tablic otrzymujesz ArrayStoreExceptionat runtime.
Sotirios Delimanolis
4
moje pytanie brzmi: WHYtablice wykonane jako kowariantne. jak wspomniał Sotirios, z Arraysami można by uzyskać ArrayStoreException w czasie wykonywania, gdyby Arrays były niezmienne, to moglibyśmy wykryć ten błąd w czasie kompilacji, prawda?
eagertoLearn
@eagertoLearn: Jedną z głównych słabości semantycznych Javy jest to, że nic w jej systemie typów nie jest w stanie odróżnić „tablicy, która zawiera tylko pochodne Animal, która nie musi akceptować żadnych elementów otrzymanych z innego miejsca” z „tablicy, która nie może zawierać nic oprócz Animal, i musi być skłonny zaakceptować zewnętrzne odniesienia do Animal. Kod, który wymaga tego pierwszego, powinien akceptować tablicę Cat, ale kod, który potrzebuje drugiego, nie powinien. Jeśli kompilator mógłby rozróżnić te dwa typy, mógłby zapewnić sprawdzanie podczas kompilacji. Niestety jedyne, co je wyróżnia ...
supercat
... jest to, czy kod faktycznie próbuje coś w nich zapisać, i nie ma sposobu, aby to wiedzieć przed uruchomieniem.
supercat
3

Tablice są kowariantne z co najmniej dwóch powodów:

  • Jest to przydatne w przypadku zbiorów zawierających informacje, które nigdy nie ulegną zmianie na kowariantne. Aby zbiór T był kowariantny, jego magazyn zapasowy również musi być kowariantny. Chociaż można by zaprojektować niezmienną Tkolekcję, która nie używałaby T[]jako magazynu zapasowego (np. Używając drzewa lub połączonej listy), jest mało prawdopodobne, aby taka kolekcja działała tak dobrze, jak ta obsługiwana przez tablicę. Ktoś mógłby argumentować, że lepszym sposobem zapewnienia kowariantnych niezmiennych kolekcji byłoby zdefiniowanie typu „kowariantnej niezmiennej tablicy”, z której mogliby korzystać, ale po prostu zezwolenie na kowariancję tablic było prawdopodobnie łatwiejsze.

  • Tablice są często modyfikowane przez kod, który nie wie, jaki typ rzeczy się w nich znajdzie, ale nie umieści w tablicy niczego, co nie zostało odczytane z tej samej tablicy. Doskonałym tego przykładem jest kod sortujący. Koncepcyjnie możliwe, że typy tablicowe zawierałyby metody do zamiany lub permutacji elementów (takie metody mogą być w równym stopniu stosowane do dowolnego typu tablicy) lub zdefiniowanie obiektu „manipulatora tablicami”, który zawiera odniesienie do tablicy i co najmniej jedną rzecz które zostały z niego odczytane i mogą zawierać metody do przechowywania wcześniej odczytanych elementów w tablicy, z której pochodzą. Gdyby tablice nie były kowariantne, kod użytkownika nie byłby w stanie zdefiniować takiego typu, ale środowisko wykonawcze mogłoby zawierać pewne wyspecjalizowane metody.

Fakt, że tablice są kowariantne, może być postrzegany jako brzydki hack, ale w większości przypadków ułatwia to tworzenie działającego kodu.

superkat
źródło
1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.- słuszna uwaga
eagertoLearn
3

Ważną cechą typów parametrycznych jest możliwość pisania algorytmów polimorficznych, czyli algorytmów, które operują na strukturze danych niezależnie od wartości jej parametru, np Arrays.sort().

W przypadku typów ogólnych odbywa się to za pomocą typów symboli wieloznacznych:

<E extends Comparable<E>> void sort(E[]);

Aby były naprawdę użyteczne, typy symboli wieloznacznych wymagają przechwytywania symboli wieloznacznych, a to wymaga pojęcia parametru typu. Żadna z tych opcji nie była dostępna w czasie dodawania tablic do języka Java, a tworzenie tablic o kowariantach typu referencyjnego pozwoliło na znacznie prostszy sposób dopuszczenia algorytmów polimorficznych:

void sort(Comparable[]);

Jednak ta prostota otworzyła lukę w systemie typów statycznych:

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

wymagające sprawdzenia w czasie wykonywania każdego dostępu do zapisu w tablicy typu referencyjnego.

Krótko mówiąc, nowsze podejście zawarte w rodzajach generycznych sprawia, że ​​system typów jest bardziej złożony, ale także statycznie bezpieczniejszy dla typów, podczas gdy starsze podejście było prostsze i mniej statycznie bezpieczne. Projektanci języka zdecydowali się na prostsze podejście, mając do zrobienia ważniejsze rzeczy niż zamknięcie małej luki w systemie czcionek, która rzadko powoduje problemy. Później, kiedy powstała Java i zadbano o pilne potrzeby, mieli zasoby, aby zrobić to dobrze dla generycznych (ale zmiana dla tablic zepsułaby istniejące programy Java).

meriton
źródło
2

Typy generyczne są niezmienne : od JSL 4.10 :

... Podtytuł nie obejmuje typów ogólnych: T <: U nie oznacza, że C<T><: C<U>...

i kilka wierszy dalej, JLS wyjaśnia również, że
tablice są kowariantne (pierwszy punktor):

4.10.3 Podpisywanie między typami tablic

wprowadź opis obrazu tutaj

alfasin
źródło
2

Myślę, że na początku podjęli złą decyzję, która spowodowała, że ​​tablica stała się kowariantna. Łamie to bezpieczeństwo typów, jak opisano tutaj, i utknęli z tym z powodu kompatybilności wstecznej, a potem próbowali nie popełnić tego samego błędu dla generycznego. I to jest jeden z powodów, dla których Joshua Bloch woli listy od tablic w pozycji 25 książki „Efektywna Java (drugie wydanie)”

Arnold
źródło
Josh Block był autorem frameworka kolekcji Java (1.2) i autorem generyków Java (1.5). Więc facet, który stworzył leki generyczne, na które wszyscy narzekają, jest również przypadkiem facetem, który napisał książkę, mówiąc, że są lepszym rozwiązaniem? Nie jest to wielka niespodzianka!
cpurdy,
1

Moje podejście: Kiedy kod oczekuje tablicy A [] i dajesz jej B [], gdzie B jest podklasą A, są tylko dwie rzeczy, o które musisz się martwić: co się dzieje, gdy czytasz element tablicy i co się dzieje, gdy piszesz to. Dlatego nie jest trudno napisać reguły języka, aby zapewnić, że bezpieczeństwo typów jest zachowane we wszystkich przypadkach (główna zasada jest taka, że ArrayStoreExceptionjeśli spróbujesz włożyć A do B []), może zostać wyrzucone. Jednak w przypadku klasy ogólnej, kiedy deklarujesz klasę SomeClass<T>, może istnieć wiele sposobów Tużycia w treści klasy i myślę, że jest to zbyt skomplikowane, aby wypracować wszystkie możliwe kombinacje, aby napisać reguły o tym, kiedy rzeczy są dozwolone, a kiedy nie.

ajb
źródło