Mam dla Ciebie do zaprezentowania kolejny materiał z języka Java! Otóż istnieje takie hasło jak "wildcard" tłumaczone jako "znak wieloznaczny" albo "typ wieloznaczny". Typ wieloznaczny w języku Java jest fragmentem wiedzy o typach generycznych, więc jeżeli nie wiesz na czym to polega, lepiej się nie dobijaj tym tematem 😅.
TYP WIELOZNACZNY W JĘZYKU JAVA STOI POD ZNAKIEM ZAPYTANIA (DOSŁOWNIE!)
Korzystając z generycznych typów T w nagłówku metody (jak dobrze wiesz z odpowiedniego artykułu 😉), możemy nałożyć ograniczenia dla typu w celu zachowania reguł bezpieczeństwa i tym samym, możliwości skorzystania z konkretnych danych składowych i metod:
private <T extends BaseClass> void doSomething(List<T> list)
Natomiast ten zapis pozwala ograniczać tylko w kierunku klas potomnych. Gdybyśmy chcieli ograniczać w drugą stronę, musimy sięgnąć po inny element składniowy ℹ️.
"Wildcard", czyli typ wieloznaczny w języku Java, to rodzaj parametru typu, który reprezentuje typ nieznany ❓. On, tak jak typ T, polega na "przeistaczaniu się" w konkretny typ, który jednak funkcjonuje inaczej (zaraz się dowiesz jak bardzo 😀). Dzięki niemu możemy pójść w obie strony: albo ograniczając od podanej klasy "w górę" (klasy bazowe), albo "w dół" (klasy potomne). Wybór ma swoje konsekwencje i nadaje inne przyzwolenia w kontekście obsługi!
"Wildcard" rozpoznasz po znaku zapytania wewnątrz nawiasów kątowych, tam gdzie umieszczamy typ T:
ArrayList< ?>
Taki sposób zapisu oznaczać będzie brak ograniczeń dla typu "ArrayList", jednak przeważnie gdy to się stosuje, to właśnie po to, żeby ograniczyć pole przekazywanych typów 😉.
CHOOSE YOUR WILDCARD 😄
Typ wieloznaczny z ograniczeniami można zdefiniować na dwa sposoby, a jeden z nich "wchodzi w życie" wraz z użyciem słowa kluczowego "super" albo "extends". Niezależnie od wyboru, wpisujemy nazwę klasy, która ma być "punktem startowym" tego ograniczenia.
SUPER
Kiedy użyjemy typu wieloznacznego ze słowem "super", czyli "w górę" (nazywane też ograniczeniem nadtypu lub kontrawariancją):
ArrayList< ? super BaseClass>
będzie to oznaczać, że w miejsce typu możemy wstawić klasę "BaseClass" LUB dowolną jej klasę bazową, która "ciągnie się" aż do klasy "Object" (ponieważ każda klasa w języku Java dziedziczy od "Object"). Gdyby to przedstawić na przedziale, to zakres dopuszczalnych klas wygląda tak:
Zakres klas dopuszczalnych po nałożonym ograniczeniu nadtypu przez typ wieloznaczny.
Na tym się nie kończą różnice, jednak więcej szczegółów przedstawię jak dojdziemy do przykładu kodu.
EXTENDS
Drugą z opcji jest wstawienie "extends", czyli "w dół" (ograniczenie podtypu/kowariancja):
ArrayList< ? extends BaseClass>
i to zmieni postać rzeczy tak, że w miejsce typu wolno nam wstawić klasę "BaseClass" LUB dowolną jej klasę potomną. Przedział dopuszczalnych klas wyglądałby tak:
Zakres klas dopuszczalnych po nałożonym ograniczeniu podtypu przez typ wieloznaczny.
Uprzedzam: "wildcard" nie obsługuje typów skrzyżowanych, czyli nie możemy bezpośrednio nakładać ograniczeń w postaci interfejsów:
ArrayList< ? extends BaseClass & Countable>
natomiast możemy połączyć typ T z typem wieloznacznym nakładając ograniczenia tak samo, jak poprzednio ✅:
private <T extends BaseClass & Countable> void doSomething(ArrayList< ? extends T> list)
TO JESZCZE NIE KONIEC
Skonfrontujmy sobie teraz oba zapisy prostym kodem źródłowym, aby pokazać jak typ wieloznaczny w języku Java działa w obie strony. Mamy takie listy typu "ArrayList":
ArrayList<BaseClass> baseClasses = new ArrayList<>();
ArrayList<DerivedClass> derivedClasses = new ArrayList<>();
i takie metody:
private void iterateBySuper(List< ? super BaseClass> list) {
for (Object element : list) {
System.out.println(element);
}
}
private void iterateByExtends(List< ? extends BaseClass> list) {
for (BaseClass element : list) {
System.out.println(element);
}
}
Co łączy obie metody? Ważna rzecz: nie wolno dodawać ani usuwać elementów z listy w środku! Dlaczego? To jest typ wieloznaczny, czyli nieznany - jak na podstawie samego znaku zapytania możesz wywnioskować (i zagwarantować) jaki typ elementu zostanie wprowadzony 🙂?
Możesz natomiast skorzystać z metod wpływających na listę, które nie wymagają podania konkretnych informacji o elemencie np. metoda "removeFirst" - ona ma za zadanie usunąć pierwszy z brzegu i kropka:
list.removeFirst();
A teraz co je dzieli. Dla "super", elementy są typu "Object"! Pamiętasz przedział narysowany u góry? Ponieważ jest to ograniczenie nadtypu, typ "sięga" najdalej tam, gdzie tylko może, czyli do klasy wszystkich klas! Co innego jest w przypadku "extends" - tutaj typem elementu jest klasa podana po słowie kluczowym! A to dlatego, że ograniczenie podtypu "wie", że może sobie pozwolić na konkretyzację typu, bo granica idzie właśnie od tej klasy, która stanowi minimum.
Z tego powodu, gdybyśmy mieli taką oto przykładową metodę w "BaseClass":
public void setCounter(int counter) {
this.counter = counter;
}
mielibyśmy prawo z niej skorzystać tylko w tej drugiej metodzie ("iterateByExtends").
Oto podsumowanie. Typ wieloznaczny w języku Java charakteryzuje się następującymi cechami:
- pozwala nakładać ograniczenia w drugą stronę tzn. w kierunku klas bazowych (ograniczenia nadtypu),
- blokuje możliwość dodawania/usuwania elementów poprzez metody wymagające podania konkretnego typu np. konstruktor z typem, pobranie elementu X itd. (to jest typ nieznany, nie wiemy co się za nim kryje),
- gdy ograniczamy "w górę" (używając słowa "super"), typem elementu jest "Object" przez co nie wolno nam korzystać z danych składowych i metod zapewnianych przez podaną klasę,
- gdy ograniczamy "w dół" (używając słowa "extends"), typem elementu jest podana klasa przez co jak najbardziej możemy korzystać z danych składowych i metod zapewnianych przez nią.
Typ wieloznaczny (ang. wildcard) pozwala na nakładanie ograniczeń zarówno w kierunku klas potomnych (jak przy typach generycznych), jak w kierunku klas bazowych.
To z pewnością nie są wszystkie różnice jakie zawiera w sobie typ wieloznaczny w języku Java, czyli "wildcard", niemniej jednak da Ci szybki podgląd na to, dlaczego użycie słowa "super" czy "extends" MA znaczenie i jak wpływa na przebieg metody wykorzystującej znaczek zapytania.