Dalszy ciąg typów generycznych w języku Java! Ten artykuł będzie kontynuował zagadnienie typu wieloznacznego (więc najpierw zaproszę tutaj, aby przeczytać, a potem żebyś wrócił(a) do tego), a konkretniej nakładania na niego ograniczeń podtypu. W języku Java, nazywa się to "kowariancja" i właśnie to, Czytelniku, będziesz mieć okazję przejrzeć 📖!
KOWARIANCJA W JĘZYKU JAVA ODNOSI SIĘ DO TYPÓW WIELOZNACZNYCH
Kowariancja (ang. covariance) oznacza nakładanie ograniczenia podtypu dla typu wieloznacznego (ang. wildcard). Działa tak samo, jak ograniczanie typu generycznego T (opisane szerzej w osobnym materiale), lecz można je nałożyć na typ wieloznaczny. W praktyce pozwala to na podstawianie do metod generycznych struktur wykorzystujących typ ogólny nie tylko konkretnego (podanego) typu T, lecz także jego klas potomnych! Pokażę na przykładzie.
POSZERZANIE POLA DO MANEWRÓW
Pierwszą cechą kowariancji jest możliwość podstawiania typu T jako klasy potomnej. Weźmy pod lupę dwa zapisy, ten:
private void printList(List<BaseClass> list)
i ten:
private void printList(List< ? extends BaseClass> list)
Do nakładania ograniczeń stosujemy to samo słówko "extends". Różnica jest o wiele większa, niż możesz przypuszczać! W pierwszym zapisie, do metody będziesz mógł/mogła wstawić tylko te listy, które mają bezpośrednio podany typ "BaseClass" - to jest inwariancja, czyli standardowy rodzaj ograniczenia jakby "z obu stron". W drugim, typ "BaseClass" ORAZ wszystkie klasy potomne wychodzące od niej! Wobec tego, poniższe podstawienia:
ArrayList<BaseClass> baseClassesList = new ArrayList<>();
ArrayList<DerivedClass> derivedClassesList = new ArrayList<>();
printList(baseClassesList);
printList(derivedClassesList);
będą jak najbardziej na miejscu i kompilator nie będzie miał nic przeciwko temu 👍. Po co to wszystko? Dla zapewnienia bezpieczeństwa typów (jeszcze raz powtarzam 😜)!
OTWARTA NA ODCZYT, ZAMKNIĘTA NA ZAPIS
Kowariancja w języku Java charakteryzuje się jeszcze jedną rzeczą - pozwala na odczyt elementów z kolekcji, lecz zabrania ich zapisywania. Traktuj to jako operację "tylko do odczytu" (ang. read-only). Chodzi o to, że kompilator dysponuje wiedzą na temat typu tylko w ograniczonym zakresie. Wie, że chodzi o klasę "BaseClass" oraz jej klasy potomne, dlatego metoda typu "getter" będzie w stanie zwrócić element typu "BaseClass":
BaseClass firstElement = list.getFirst();
natomiast w kwestii zapisu jest całkowita blokada - nie wiadomo jaki konkretnie typ miałby zostać przekazany do listy (czy "BaseClass", czy klasa potomna A, czy klasa potomna B, czy jeszcze jakaś inna), dlatego też odmówi wykonania takiej operacji, bo znowu - kieruje się bezpieczeństwem typów.
To pozwala na odseparowanie od siebie kwestii bezpiecznego pobierania (akcesor) i niepewnego zapisywania (mutator). Z tym zagadnieniem wiąże się powstały mnemonik PECS: "Producer Extends, Consumer Super". On właśnie mówi o tym, żeby korzystać z kowariancji, gdy chcemy tylko pobierać elementy, a z kontrawariancji wtedy i tylko wtedy, gdy chcemy zapisywać elementy.
Kowariancja w języku Java oznacza ograniczenie podtypu dla typu wieloznacznego. Pozwala na przeprowadzanie operacji na elementach tylko do odczytu.
Na tym kończę ten artykuł mając nadzieję, że nie przeraziłem Cię tym wątkiem (choć istnieją dużo bardziej rozbudowane kombinacje typów wieloznacznych 😈) 😄.