Wczoraj poruszyłem kwestią związaną z sortowaniem kolekcji. Nie możemy iść dalej z kontynuacją tego tematu bez podstawowego rozumienia typów generycznych. Typy generyczne w języku Java stanowią kręgosłup wszystkich wykorzystywanych kolekcji (i to nie tylko w tym języku!). Zrobię teraz do tego wstęp, aby artykuł jak każdy inny, spełniał swoją rolę jak najlepiej.
TYPY GENERYCZNE W JĘZYKU JAVA W OGÓLE I W SZCZEGÓLE
Termin określany jest wielorako: typ generyczny, typ ogólny, typ uogólniony, "generic", natomiast wszystko sprowadza się do tego samego. Jest to typ nieposiadający żadnego uszczegółowionego fragmentu, który sugerowałby jakikolwiek związek z jakąś klasą, strukturą czy nawet samym zastosowaniem. Przy dziedziczeniu i klasie abstrakcyjnej to skonkretyzowanie, choć częściowe, to zachodzi. Dane składowe mogą sugerować na przykład stan zdrowia zwierzęcia, a metody mogą wskazywać na zachowanie przycisku. Tutaj taka sytuacja nie zachodzi. Typy generyczne zostały wprowadzone w wersji 5.0 Javy i warto je opanować z kilku powodów:
- podnoszą bezpieczeństwo typów,
- znacznie redukują powtarzalność kodu,
- mimo ich "ogólności" można wymuszać od jakiej klasy zmienna typu generycznego musi pochodzić np. klasa musi dziedziczyć od jakiejś konkretnie podanej albo musi implementować konkretny interfejs - to się nazywa "ograniczaniem typu".
Przede wszystkim, uzmysłowić sobie jedno: jeżeli pada hasło "typy generyczne", to masz to rozumieć jako "typy uniwersalne". Nie ma w nim niczego konkretnego, co mogłoby być dostosowane pod jakąkolwiek strukturę czy "rodzinę" klas. Niemniej jednak jesteśmy w stanie ograniczać dopuszczanie typów do klas generycznych wymuszając, aby "wpuszczane" były tylko te, które spełniają warunek dziedziczenia od klasy X czy też implementowały interfejs Y.
ZNAK ROZPOZNAWCZY
Kilka słów teraz o rozpoznaniu czegoś takiego w kodzie. Typy generyczne rozpoznasz natychmiastowo po nawiasach kątowych (<>) oraz pojedynczej dużej literce. Najczęściej spotykane, to: T, E oraz K i V dla par, aczkolwiek język nie każe Ci tego pisać w ten sposób (mogą być dowolne nazwy). Standardowa konwencja nazewnicza nakazuje, aby domyślnie trzymać się litery T (od "type") - może być też E (od "element") jeśli programujemy kolekcję albo K i V, gdy będziemy operować na parze klucz-wartość. Każda struktura akceptująca typy generyczne może jak najbardziej przyjmować dowolną liczbę typów generycznych. Wtedy idziemy dalej z alfabetem, czyli U, V itd.
CO MOŻE BYĆ GENERYCZNE?
Co do kodu źródłowego, dwie najczęściej używane struktury to: klasa i metoda. Interfejs też może przyjmować typy generyczne w języku Java, aczkolwiek rzadko się korzysta z generyczności w interfejsach.
KLASA GENERYCZNA
Klasę generyczną tworzymy tak samo jak regularną, jednak zaraz po nazwie wprowadzamy jeden dodatkowy budulec:
public class GenericClass<T> {
}
Typ ogólny wewnątrz nawiasów kątowych. Mamy pełne prawo podstawiać go jako daną składową oraz w metodach w dowolnej formie:
public class GenericClass<T> {
private final T instance;
public GenericClass(T instance) {
this.instance = instance;
}
public T getInstance() {
return instance;
}
}
Więcej informacji na temat klas generycznych zostawiłem w odrębnym artykule.
METODA GENERYCZNA
Metoda generyczna widziana za pierwszym razem może przyprawiać o niemałe zdziwienie, a to za sprawą nie samego typu, a tego, w którym miejscu go umieszczamy:
private <T extends BaseClass> void doSomething(List<T> list) {
}
Dokładnie - po modyfikatorze dostępu, lecz PRZED typem zwracanej wartości! O samych metodach generycznych także jest osobny artykuł.
NAKŁADANIE OGRANICZEŃ
To nie wszystko co oferują typy generyczne w języku Java. Często bywa tak, że chcemy wewnątrz struktury generycznej powołać się na coś konkretnego z danej hierarchii dziedziczenia, na jakąś metodę czy daną składową. Sam "goły" typ T spowoduje błąd kompilacji, ponieważ typ T nie zabroni zaaplikować jakiegokolwiek typu danych (może to być nawet "Object"!). Wtedy musisz nałożyć tzw. ograniczenie.
Ograniczenie tworzy restrykcje dla typów danych jakie mogą zostać zaakceptowane przez kompilator w momencie tworzenia obiektu. Kiedy okaże się, że podany typ nie kwalifikuje się, bo nie należy do hierarchii dziedziczenia bądź nie implementuje żądanego interfejsu, kompilator wskaże tę instancję jako błąd i sprzeciwi się kompilacji.
Ograniczenia definiujemy poprzez słowo kluczowe "extends" (tak, to samo jakie używamy do dziedziczenia 😀), które wstawiamy za typem T, a po nim wskazujemy na klasę lub interfejs, którego mamy wymagać od typu podawanego podczas tworzenia obiektu:
<T extends BaseClass>
Powyższy zapis możemy bez problemu nałożyć dla klasy oraz metody generycznej (w miejsce T). Tutaj także pozwolę tylko o tym wspomnieć, a po resztę informacji odesłać do stosownego wpisu.
Typy generyczne w języku Java pozwalają na "podstawianie" obiektów dowolnego typu spełniającego określone w definicji warunki (jeśli zostały określone), które wówczas "transformują się" na typ skonkretyzowany.
Wystarczy jak na lekkie wprowadzenie. Będzie wiele części na ten temat, gdyż typy generyczne są naprawdę rozległe, zarówno pod kątem merytorycznym, jak i funkcjonalnym.