Programowanie zaawansowane #3: typ wieloznaczny

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<?>). Typowym wykorzystaniem tego mechanizmu jest sytuacja, gdy wiedza o tym, jaki typ finalnie będzie użyty jest kompletnie nie znana. Świetnym przykładem używania typu wieloznacznego 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). 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);
}

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 generyczny ma być określony np. w parametrze w metodzie? Używaj wtedy właśnie symbolu wieloznaczności.

*Czyli po prostu zmiana kodu pod kątem ‚estetycznym’ a nie biznesowym.

Dodaj komentarz