Ostatnim tematem związanym z typami generycznymi, jest używanie tak zwanego typu wieloznacznego (ang. wildcard). Oznacza się go za pomocą znaku zapytania (czyli np. ArrayList<?>). Powszechnym wykorzystaniem tego mechanizmu jest sytuacja, gdy wiedza o tym, jaki typ finalnie będzie użyty jest kompletnie nie znana. Świetnym przykładem, jak użyć typ wieloznaczny, jest refactoring* starego kodu. Pewnie tego nie wiesz, ale dawno dawno temu w Javie nie istniały typy generyczne i za każdym razem, gdy programista chciał użyć jakiegoś kontenera, to musiał go wpierw zrzutować na poprawny typ. Taka praktyka była bardzo podatna na błędy, stąd też wprowadzenie typów generycznych znacznie ułatwiło programistom życie. Mimo wszystko wciąż możesz używać kontenerów, takich jakich lista, nie używając generyków (tzw. raw type).
Raw type warto unikać
Spójrz na przykład poniżej.
List list = new ArrayList(); list.add("1"); System.out.println(list.get(0));
Taki kod skompiluje się i zadziała poprawnie. Java 'domyśli’ się, że lista, którą zaimplementowano ma dotyczyć typu tekstowego, ale zastanów się, do jakich paradoksów może prowadzić pisanie takiego kodu. Zobacz kolejny przykład.
public class Employee { private String name; private int age; public Employee(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
Napisałem zwykłą klasę typu POJO. Teraz wrzucę ją do mojej listy typu raw type i spróbuję w pętli ją przeiterować.
List list = new ArrayList(); list.add("1"); list.add(new Employee("Mariusz", 22)); for (Iterator iterator = list.iterator(); iterator.hasNext();) { Object object = (Object) iterator.next(); System.out.println(object); }
Typ wieloznaczny – wildcard
Taki kod też się skompiluje, ale zatraciłem wiedzę o danych przechowywanych na liście. Spróbuj teraz na przykład, wyciągnąć ze zmiennej object jakąś informację o pracowniku (np. wiek). Jest to nie możliwe. Dlatego unikaj tego typu programowania jak ognia i zawsze korzystaj z generyków. Gdy już kompletnie nie jesteś wstanie określić typu, jaki może być użyty w klasie generycznej, to skorzystaj z typu wildcard. W ten sposób wymuszasz na kompilatorze sprawdzenie
List<?> wildcardList = new ArrayList<>(); // wildcardList.add("test"); blad, nie mozesz wrzucic do listy elementu typu String
Do tak stworzonej listy możesz dodawać tylko wartość null.
Typ wieloznaczny ma jednak większe zastosowanie niż zastępowanie w powyższym przypadku. Przeanalizuj kolejny przykład.
public static void useWildcardList(List<? extends Employee> emloyeesAndOthers) { // kod metody }
Lista jest typem generycznym, więc powinieneś zadeklarować jakiego ma być typu (np. String). Jeśli jednak wciąż nie wiadomo jakiego typu ma być generyk, to można używać właśnie symbolu wieloznaczności wraz z odpowiednim ograniczeniem. Teraz do metody zamiast samej wypełnionej pracownikami, można wypełnić ją innymi obiektami, które dziedziczą po klasie Employee.
List<Employee> employees = new ArrayList<>(); useWildcardList(employees); // useWildcardList(wildcardList); blad, nie dziedziczy po Employee
Wildcard można tak samo ograniczać jak każdy inny typ generyczny. Różnica jest taka, że drugi z nich przydatny jest do definiowania jakiejś abstrakcji (tak jak w matematyce). W powyższym przypadku nie możesz, tak po prostu, zażądać aby korzystać z typu generycznego (zamień znak zapytania na literę T i zobaczysz, że taki kod się nie skompiluje), bo już go zadeklarowano go w danej klasie generycznej. Każde użycie typu generycznego, wymaga, aby wrzucić tam jakiś realny typ. Co wtedy gdy nie wiesz, jaki typ uogólniony ma być określony np. w parametrze w metodzie? Używaj wtedy właśnie typ wieloznaczny.
Typ wieloznaczny można ograniczać nie tylko za pomocą extends ale także 'z drugiej strony’ poprzez słowo kluczowe super. W moim przykładzie wyglądałoby to następująco: List<? super Employee> emloyeesAndOthers. Oczywiście takie użycie musi zgadzać się z hierarchą dziedziczenia. W moim przypadku oznacza to, że lista obiektów (typu Object) będzie tu pasować, z racji że niejawnie każda klasa dziedziczy po Object**. Nie będę się tu podawał konkretnego przykładu, ponieważ zasada użycia jest dokładnie taka sama, jak w przypadku extends. Pamiętaj jedynie, aby nie używać obu ograniczeń jednocześnie.
*Czyli po prostu zmiana kodu pod kątem 'estetycznym’ a nie biznesowym.
Link do kodu: https://github.com/developeronthego/java-advanced/tree/master/src/main/java/advanced/lesson3
**Klasa Object to 'praklasa’ wszystkich klas w Javie (Java #26: klasa Object)