#Niezbędnik Juniora. Generyki

By 24 September 2015 Niezbędnik Juniora

Gen­ery­ki są jed­nym z tem­atów, z który­mi spotkałaś się na naszym kur­sie, gdy pisal­iśmy o kolekc­jach, ale nie nazy­wal­iśmy ich bezpośred­nio. Dzisi­aj uzu­pełnie­nie, które poz­woli Ci zrozu­mieć czym są i jakie jest ich zastosowanie. 

Czym są generyki?

Gen­ery­ki (gener­ics) zostały dodane do Javy 1.5, po to, aby umożli­wić para­me­try­zowanie klasy, metody oraz inter­fe­j­su. Dzieje się tak poprzez podawanie typu (lub typów) argu­men­tu, przyj­mowanego przez metody danej klasy/interfejsu dopiero w momen­cie jej uży­cia w kodzie.

Ze wzglę­du na powyższy fakt, może­my wyko­rzysty­wać pewne częś­ci kodu, które nie są ściśle związane z konkret­ną imple­men­tacją wielokrot­nie. Korzys­tamy wtedy ze ścisłego typowa­nia w kon­tekś­cie danej sytu­acji. Jest to jeden ze sposobów na wcie­le­nie polimor­fiz­mu do naszego kodu.

Przykłady generyków

Na początek krót­ka wzmi­an­ka o korzyś­ci­ach związanych z uży­ciem gen­eryków. Jeśli poprosilibyśmy Cię o napisanie “uni­w­er­sal­nej” klasy Wal­iz­ka z polem przed­miot, które może być różnego typu, a Ty nie znałabyś gen­eryków, to najpraw­dopodob­niej stworzyłabyś coś takiego:

public class Walizka {
    private Object przedmiot;

    public void set(Object przedmiot) { this.przedmiot = przedmiot; }
    public Object get() { return przedmiot; }
}

Takie rozwiązanie na pier­wszy rzut oka wyda­je się być praw­idłowe. Mamy pole typu Object, które może ‘prze­chowywać’ dowol­ny typ obiek­tu, dziedz­iczą­cy po Object (czyli wszys­tko poza prymi­ty­wa­mi). ist­nieje możli­wość stworzenia obiek­tów wal­iz­ka, które będą miały wartość pola przed­miot jako Inte­ger, String, czy też np. typu Kos­me­ty­ki (jeśli stworzymy taką klasę w ramach naszego pro­gra­mu). Sposób nadal wyda­je się odpowied­nim, ale poszuka­jmy w nim zagrożeń. Jeśli nasze pole może przyjąć wszys­tko, to jak uod­pornić je na błędy związane z nieod­powied­nim typem, w konkret­nej sytu­acji związanej z  jego uży­ciem? Jak automaty­cznie sprawdz­ić, czy typ obiek­tu, który jest przyp­isy­wany do pola przed­miot, jest tym właś­ci­wym w danym momen­cie? W przy­pad­ku takiego rozwiąza­nia, nie potrafimy w trak­cie kom­pi­lacji “wyła­pać” niepraw­idłowego uży­cia. Przez to może­my wywołać metodę, która nie jest dla danego typu. Tym samym wygeneru­je­my błąd, bo Java jest w stanie sprawdz­ić tylko i wyłącznie, czy dany obiekt jest typu Object. Słowem, nie rób­cie tego w domu! :)

Rozwiązaniem powyższego prob­le­mu jest stworze­nie możli­woś­ci poda­nia konkret­nego typu naszego pola, w momen­cie zas­tosowa­nia w kodzie. Wtedy kom­pi­la­tor będzie mógł zwery­fikować, czy jest ono dobrze uży­wane. W ten sposób metody mogą zwracać właś­ci­wy typ, a nie Object, który musimy rzu­tować w zależnoś­ci od sytu­acji. I tak właśnie dzi­ała­ją generyki.

Nasza klasa w wyda­niu gen­erycznym będzie wyglą­dała następująco:

public class GenerycznaWalizka<T> {
    private T przedmiot;

    public void set(T przedmiot) { this.przedmiot = przedmiot; }
    public T get() { return przedmiot; }
}

Jak widz­imy, pojaw­iły się charak­terysty­czne naw­iasy trójkątne, które zaw­ier­a­ją w sobie T (zwycza­jowo, jako akro­n­im od type, ale może­my użyć dowol­nej nazwy: K, V, ABC czy innej wybranej) Pole zami­ast typu Object jest ter­az właśnie typu T. Taki naw­ias na pewno kojarzysz już z pra­cy np. z ArrayListą, gdzie w naw­iasach <> podawałaś typ przyj­mowanych argu­men­tów. I bard­zo dobrze, że zauważyłaś pow­iązanie, bo kolekc­je są właśnie przykła­dem generyków ;)

T może reprezen­tować wszys­tko, co nie jest prymi­ty­wem. Może­my dowol­nie wybrać nazwę para­metru. Warto jed­nak pamię­tać, że ist­nieje pew­na kon­wenc­ja nazewnict­wa i tak: E — Ele­ment, K — Key, N — Num­ber, T — Type, V — Val­ue, S,U,V etc. — dru­gi, trze­ci, czwarty parametr).

Należy też wspom­nieć, że klasa może przyj­mować wiele para­metrów. Poda­je­my je wtedy po przecinkach, np:

public class Mapa<K,V> {
    private K klucz;
    private V wartosc;

    //...

}

Klasa ta przyj­mu­je dwa typy, które nazwal­iśmy K oraz V.

Korzystanie z generyków w kodzie

Aby uży­wać klasy gen­erycznej w kodzie, powin­niśmy skonkre­ty­zować wszys­tkie para­me­try, tzn. podać konkretne klasy w miejsce każdego z nich (co ważne — nie jest to ściśle wyma­gane, niepo­danie typu spowodu­je wyświ­etle­nie uwa­gi, ale kod się skom­pilu­je). W naszym przy­pad­ku będzie to np:

Walizka<String> stringWalizka = ...;

Jeśli musimy skonkre­ty­zować więcej para­metrów, poda­je­my typy po przecinku, np:

Map<Integer, String> mapa = ...;

Gen­eryków uży­wamy wszędzie tam, gdzie nasz kod nie zależy od dokład­nej imple­men­tacji obiek­tu, a jedynie ‘przesyła’ go. Dobrym przykła­dem są kolekc­je (np. Lista prze­chowu­je wszys­tko, nie ma znaczenia jakiego typu są to obiek­ty) czy bazowe ser­wisy (takie, po których dziedz­iczą ser­wisy specy­ficzne dla danego obiek­tu) lub obiek­ty typu DAO. Co więcej — typów może­my uży­wać para­me­tryzu­jąc także pola klasy czy metody abstrak­cyjne, jak np. w poniższym przykładzie:

public abstract class BaseService<K, T> {

    public abstract BaseDAO<K, T> getBaseDAO();

    public T getById(K klucz) {
        return getBaseDAO().getById(K);
    }

    //...

}

Ograniczenia typów (bounded type parameters)

Cza­sa­mi zdarza się sytu­ac­ja, w której chce­my wyko­rzys­tać funkcjon­al­ność gen­eryków, ale ogranicza­jąc ją tylko do klas dziedz­iczą­cych po określonym typ­ie lub takich, po których określony typ dziedziczy.

Ograniczenie górne (upper bound)

Ogranicze­nie górne pozwala nam ‘zawęz­ić’ potenc­jal­nie uży­wane typy tylko do tych, które dziedz­iczą po określonej klasie lub imple­men­tu­ją określony inter­fe­js. Roz­pa­trzmy przykład wal­iz­ki z aka­pitu wyżej. Do wal­iz­ki chce­my zapakować wyłącznie ubranie — nie jakieś konkretne, ale cokol­wiek, co mieś­ci się w poję­ciu odzieży. Nasza deklarac­ja klasy będzie w tej sytu­acji wyglą­dała następująco:

public class GenerycznaWalizka<T extends Ubranie> {
    //...
}

Co najważniejsze, w dowol­nym miejs­cu naszego kodu może­my uży­wać wszys­t­kich metod klasy bazowej (w tym wypad­ku Ubranie) dla dowol­nej zmi­en­nej, której typ określimy jako ‘T’. Np:

public class GenerycznaWalizka<T extends Ubranie> {
    private T zawartosc;

    public String opiszZawartosc() {
        return zawartosc.getOpisZMetki();
    }

    //...

}

Oczy­wiś­cie powyższy kod zadzi­ała przy założe­niu, że klasa (lub inter­fe­js) Ubranie deklaru­je metodę String getOpisZMetki().

Ograniczenie górne wielokrotne

Pewnym rozwinię­ciem tego kon­cep­tu są wielokrotne ograniczenia górne. Zna­j­du­ją one zas­tosowanie  w momen­tach, kiedy chcielibyśmy ograniczyć obiek­ty ‘przetwarzane’ przez naszą klasę tylko do takich, które imple­men­tu­ją np. wiele inter­fe­jsów, czy też klasę oraz inter­fe­js. Ponown­ie wraca­jąc do przykładu powyżej, załóżmy, że mamy dodatkowo inter­fe­js ‘Moz­naSkladac’. Chce­my tak opisać naszą wal­izkę, aby przyj­mowała tylko ubranie, które moż­na składać. W kodzie będzie to wyglą­dało następująco:

public class GenerycznaWalizka<T extends Ubranie & MoznaSkladac> {
    //...
}

Równie dobrze może­my dodać kole­jne inter­fe­jsy, jak np. MaKieszenie:

public class GenerycznaWalizka<T extends Ubranie & MoznaSkladac & MaKieszenie> {

    //...

}

Związane są z tym pewne ograniczenia: może­my w powyższy sposób wymienić tylko jed­ną klasę (ponieważ Java nie obsługu­je wielokrot­nego dziedz­iczenia). Jeśli wymieni­amy zarówno klasę, jak i inter­fe­jsy, klasa musi być wymieniona jako pierwsza.

Ograniczenie dolne (lower bound)

Drugim rodza­jem ograniczeń, jakie moż­na zas­tosować, są ograniczenia dolne. Ogranicza­my  w nich przyj­mowane typy tylko do klas, po których dziedz­iczy określony typ. Tego rodza­ju ogranicze­nie ma jed­nak bard­zo specy­ficzne zas­tosowanie. Warto wiedzieć, że ono ist­nieje, najpraw­dopodob­niej jed­nak nie spotkasz się z nim w praktyce.

Zain­tere­sowanych przykła­dem uży­cia odsyłam do strony Ora­cle.

Metody generyczne

Cza­sem nie chce­my lub nie może­my uczynić całej klasy gen­erycznej. Potrze­bu­je­my za to, by meto­da zwracała nam coś o typ­ie takim jak argu­ment (np. w przy­pad­ku wal­i­dacji, metod pomoc­nicznych builderów itp). Załóżmy, że mamy następu­ją­cy interfejs:

public interface PosiadaId {
    public String getId();
    public void setId();
}

i chce­my stworzyć metodę pomoc­niczą, która dla wybranego obiek­tu skopi­u­je go, ustawi kopie losowe id oraz zwró­ci kopie. Meto­da ta bez gen­eryków wyglą­dała­by następująco:

public PosiadaId skopiujINadajId(PosiadaId obiekt) {
    //...
}

Prob­le­mem jest to, że wyżej wymieniona meto­da nie zwraca właś­ci­wego typu i każ­do­ra­zowo musielibyśmy rzu­tować otrzy­many obiekt na właś­ci­wy typ. Wer­s­ja gen­erycz­na tego sposobu miała­by postać następującą:

public  T skopiujINadajId(T obiekt) {
    //...
}

W tym wypad­ku otrzy­many przez nas obiekt był­by właś­ci­wego typu. Jed­nocześnie nie musimy deklarować całej klasy jako gen­erycznej, wyko­rzys­tu­jąc jeden obiekt dla wielu typów. Meto­da ta może być również statyczna.

Generyki w praktyce…

… czyli: co to znaczy, że są one tylko i wyłącznie infor­ma­cją dla kom­pi­la­to­ra? Otóż wiado­mość o typ­ie para­metrów klasy gen­erycznej nie jest wery­fikowana, czy wręcz dostęp­na w skom­pi­lowanej klasie. Oznacza to, że dla maszyny wirtu­al­nej Javy ArrayList<Integer> to w prak­tyce po pros­tu ArrayList. Jeśli w ten sposób będziemy się odnosić do listy w kodzie, umożli­wi nam to dodanie do niej np. Stringa! Oczy­wiś­cie już w momen­cie pró­by odczy­tu otrzy­mamy wyjątek Class­Cas­tEx­cep­tion, ale uży­wanie nies­para­me­try­zowanych typów może spowodować bard­zo duże zamieszanie. Weźmy za przykład poniższy fragment:

Vector strings = new Vector(10);
Vector objects = strings; //typ niesparametryzowany!! Ale technicznie jest to prawidłowy kod, który się skompiluje
objects.add(new Object()); //poprawne, bo object nie ma parametrów - choć wskazuje na ten sam obiekt, co strings
Object anObject = strings.get(0); //to jest nadal poprawne, ponieważ pierwszy element w istocie jest obiektem
String aString = strings.get(0); //wyjątek, pierwszy element nie jest Stringiem

Nietrud­no sobie wyobraz­ić, że dru­ga i trze­cia lin­ij­ka są zaszyte w jakimś mod­ule i moz­na ich pros­to odnaleźć. Uruchami­amy aplikację i otrzy­mu­je­my wyjątek, że ele­ment pobrany z kolekcji Stringów nie jest Stringiem. Mylące, prawda?

Wynika­ją z tego różne ograniczenia — typów gen­erycznych nie moż­na np. rzu­cać czy łapać (throw i catch), nie moż­na rzu­tować na typy para­me­try­zowane, wery­fikować warunk­ów typu instance­of, czy też tworzyć tablic obiek­tów typów para­me­try­zowanych. Bari­ery te są opisane szczegółowo na stron­ie Ora­cle, zajrzyj tam, jeśli intere­su­ją Cię powody tego stanu rzeczy.

Jeszcze krótkie wyjaśnie­nie: tego rodza­ju wiedza nie jest oczeki­wana od junio­ra — ale bard­zo ważne, żebyś uży­wała typów gen­erycznych praw­idłowo, w prze­ci­wnym razie możesz przys­porzyć sobie bólu głowy, szuka­jąc źródła problemu.

Generyki w API Javy

W samym API Javy zna­jdziemy wiele przykładów uży­cia typów gen­erycznych — poniżej kil­ka najczęś­ciej stosowanych oraz najbardziej znanych.

Kolekcje

Wszys­tkie kolekc­je w Javie są para­me­try­zowane — każ­da imple­men­tac­ja List, Map czy Set posi­a­da para­me­try, które może­my (powin­niśmy) określić.

Optional

Option­al to typ, który pozwala na uniknię­cie sprawdza­nia, czy dana wartość jest null’em oraz tych nieob­służonych Null­Point­erEx­cep­tion. Jest to klasa coraz częś­ciej pojaw­ia­ją­ca się w różnego rodza­ju API, która służy do ‘pokaza­nia’, że dany para­metr jest opcjonalny.

Lambdy

Choć nie wprost, także lamb­dy są przykła­dem uży­cia typów gen­erycznych. Wiele klas w pakiecie java.util.function jest para­me­try­zowanych i reprezen­tu­je np. ele­men­ty stru­mieni, czy bezpośred­nio funkc­je, które może­my zapisy­wać w postaci lambd

Podsumowanie

Typy gen­eryczne to niezwyk­le przy­datne usprawnie­nie w języku Java, które pozwala pisać bardziej czytel­ny i uni­w­er­sal­ny kod. Trze­ba jed­nak uważać, by nie wprowadz­ić potenc­jal­nie ryzykownych frag­men­tów kodu poprzez niewłaś­ci­we ich użycie.

Gen­ery­ki są również jed­nym z ele­men­tów języ­ka, których najpraw­dopodob­niej uży­wałaś, zan­im dowiedzi­ałaś się o ich istnieniu ;)