Kapitola 1. Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. Obsah Generické typy ............................... Generické datové typy (generics) Základní syntaxe..................... Jednoduché využití v metodách .. První příklad použití ................ Cyklus foreach ....................... Žolíci (wildcards).................... Generické metody ................... Generics metody vs. wildcards ... Pole...................................... Vícenásobná vazba generics...... Závěr.................................... Generické typy Generické datové typy (generics) Pokud si vezmeme anglicko-český slovník, zjistíme, že v překladu to znamená něco obecně použitelného, tedy mohli bychom použít termínu zobecnění. A přesně tím generics jsou ~ zobecněním. Jak již víme, struktura tříd v Jávě má společného předka, třídu Ob j ect. Tato skutečnost zjednodušuje implementaci nejednoho programu ~ potřebujeme-li pracovat s nějakými objekty, o kterých tak úplně nevíme, co jsou zač, můžeme využít společného předka a pracovat s ním. Každý objekt v programu je totiž i instancí třídy Ob j ect. To v praxi umožňuje například snadnou implementaci spojových seznamů, hashovacích tabulek, ale například i využití reflexe. Jakkoliv je společný předek jistě výhodou, přináší i některé obtíže. Uvažujme opět (spojový) seznam. Pracujeme-li v programu s nějakým, víme jenom, zeje to seznam objektů a nic více. Samozřejmě si můžeme bokem pamatovat, že tam ukládáme jenom řetězce, ale to nic nemění na tom, že runtime systému je srdečně jedno, jaké objekty do seznamu vkládáme. Stejně tak, chceme-li z tohoto seznamu číst, získáváme zase jenom obecné objekty, které musíme explicitně přetypovat na řetězec (tj. třídu String). A opět ~ ačkoliv jako programátoři víme, co v seznamu má být, obecně si nemůžeme být jisti, zda to tam skutečně je. Což vede buď k tomu, že se budeme ptát, zda získávaný objekt je skutečně řetězcem a až poté jej pretypujeme nebo budeme „riskovat" runtime výjimku ClassCastException. Jak vidíme, 1 .1 .1 .2 .2 .3 .4 .5 .8 .8 .9 .9 10 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. nevýhody jsou a to docela významné. Řešením v této situaci je příchod Javy verze 1.5, která mimojiné přináší i zmiňovaná generics. Opět využijeme našeho spojového seznamu. Vraťme se zpět k jeho definici. Co od něj vlastně požadujeme? • Aby byl seznamem čehokoliv a tak byl universální (což je stav popsaný výše)? • Nebo aby to byl seznam nějakého obecného, předem nedefinovaného typu a umožnil tento typ při vytvoření instance určit? Jelikož předchozí otázky jsou řečnické, odpověď následuje ihned. Samozřejmě, že druhá uvedená možnost je lepší (už proto, že má veškerou sílu první možnosti, stačí nadefinovat, aby tím obecným typem byl Ob j ect). Ve zbytku textu si tedy ukážeme, jak toho generics docilují a jak je používat. Základní syntaxe V úvodu jsme se lehce zmínili o spojovém seznamu. Nyní si budeme ukazovat příklady na obecném seznamu (rozhraní j ava . util. List). Takhle vypadá deklarace List bez použití generics: public interface List { } A takhle s nimi: public interface List { } Jak vidíme, úvod je velmi jednoduchý. Do špičatých závorek pouze umístíme symbol, kterým říkáme, že seznam bude obsahovat prvky E (předem neznámého) typu. Zde uděláme malou odbočku - je doporučováno používat velké, jednopísmenné deklarace generics. Toto písmeno by zároveň mělo vystihovat použití resp. význam takového zobecnění. Tedy Tje typ, £ je prvek (element) a tak podobně Jednoduché využití v metodách 2 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. Pouhá deklarace u jména třídy resp. rozhraní samozřejmě nemůže stačit. Zjednodušeně řečeno, zdrojový kód využívající generics musí typ E použít všude tam, kde by dříve použil obecný Ob j ect. To jest například místo Object get(int index); se použije E get(int index); Co jsme nyní udělali? Touto definicí jsme řekli, že metoda get vrací pouze objekty, které jsou typu E na místo libovolného objektu, což je přesně to, co od generics vyžadujeme. Všimněte si, že nyní už s £ pracujeme jako s jakoukoliv jinou třídou nebo rozhraním. Totožně postupujeme i u metod, které do seznamu prvky typu E přidávají. Viz boolean add(E o); Dovolím si další malou poznámku na okraj ~ výše zmíněné metody by samozřejmě mohly pracovat s typem Ob j ect. Překladač by proti tomu nic nenamítal, nicméně očekávaná funkcionalita by byla pryč. První příklad použití Nyní tedy máme seznam, který při použití bude obsahovat nějaké prvky typu E. Nyní chceme takový seznam použít někde v našem kódu a užívat si výhod generics. Vytvoříme jej následovně: List = new ArrayList() ; Použití je opět jednoduché a velmi intuitivní. Nyní následují dva příklady demonstrující výhody generics (první je napsán „postaru"). 3 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. Object number = new Integer(2); List numbers = new ArrayList(); numbers.add(new Integer(l)); numbers.add(number); Number n = (Number)numbers.get(0); Number o = (Number)numbers.get(1); Object number = 2; List numbers = new ArrayList(); numbers.add(1); numbers.add((Number)number); Number n = numbers.get(0); Number o = numbers.get(1); Jak vidíme v horním příkladu, do seznamu lze vložit libovolný objekt (byť zde jsme měli „štěstí" a bylo to číslo) a při získávání objektů se spoléháme na to, že se jedná o číslo. Níže naopak nelze obecný objekt vložit, je nutné jej explicitně pretypovat na číslo, teprve poté překladač kód zkompiluje. Podotkněme, že pokud bychom na Number pretypovali například String, program se také přeloží, ale v okamžiku zavolání takového příkazu se logicky vyvolá výjimka ClassCastException. Získání čísel ze seznamu je ovšem přímočaré ~ stačí pouze zavolat metodu get, která má správný návratový typ. Povšimněte si rovněž použití další vlastnosti nové Javy, tzv. autoboxingu, kdy primitivní typ je automaticky převeden na odpovídající objekt (a vice versa). Cyklus foreach Přestože konstrukce cyklu for patří svou povahou jinam, zmiňujeme se o nich zde, u dynamických struktur - kontejnerů, neboť se převážně používá k iterování (procházení) prvků seznamů, množin a dalších struktur. Obecný tvar cyklu "foreach" je syntaktickou variantou běžného "for": for (TypRidiciPromenne ridici proměnna : dyn struktura) { // co se dela pro každou hodnotu ze struktury... } 4 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. V následujícím příkladu jsou v jednotlivých průchodech cyklem for postupně ze seznamu vybírány a do řídicí proměnné e přiřazovány všechny jeho prvky (objekty). for (Object e : seznam) { System.out.println(e) ; } Žolíci (wildcards) V předchozích částech jsme se seznámili s hlavní myšlenkou generics a její realizací, a sice nahrazení konkrétního nadtypu (většinou Ob j e et) typem obecným. Nicméně tohle samo o sobě je velmi omezující a nedostačující. Nyní se tedy ponoříme hlouběji do tajů generics. Představme si následující situaci. V programu chceme mít seznam, kde budou jako prvky různé jiné seznamy. První nápad, jak jej nadeklarovat může být třeba tento: List. Nicméně tato úvaha je chybná. Uvažujme následující kód: List cisla = new ArrayList(); List obecný = cisla; obecný.add("Ja nejsem cislo"); Jak vidíme, „něco je špatně." To, že se pokoušíme přiřadit do seznamu objektů obecný řetězec "Ja ne j sem cislo" je přece naprosto v pořádku, do seznamu objektů můžeme skutečně vložit cokoliv. V tom případě ale musí být špatně přiřazení na druhém řádku. To znamená, že seznam čísel není seznamem objektů! Zde je vidět rozdíl oproti „klasickému" uvažování v mezích dědičnosti. Přečtěte si pozorně následující větu a pokuste se pochopit její význam. Do seznamu, který obsahuje nejvýše čísla lze vkládat pouze objekty, které jsou alespoň čísly. Z toho vyplývá, že je nelegální přiřazovat objekt „seznam čísel" do objektu „seznam objektů." Tedy, 5 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. vrátíme-li se k našemu přikladu se seznamem seznamů, vidíme, proč byla naše úvaha chybná. Do námi definovaného seznamu totiž lze ukládat pouze seznamy objektů a ne libovolné seznamy. Jak tedy docílíme kýženého jevu? K tomuto účelu nám generics poskytují nástroj zvaný žolík, anglicky wildcard, který se zapisuje jako ?. Vraťme se nyní k předchozímu příkladu: List cisla = new ArrayList(); List obecný = cisla; // tohle je OK obecný.add("Ja nejsem cislo"); // tohle nelze přeložit Jak je již v komentáři kódu naznačeno, poslední řádek neprojde překladačem. Proč? Protože pomocí List říkáme, že obecný je seznamem neznámých prvků. A jelikož nevíme, jaké prvky v seznamu jsou, nemůžeme do něj ani žádné prvky přidávat. Jedinou výjimkou je „žádný" prvek, totiž null, který lze přidat kamkoliv. Mírně filosoficky řečeno, null není ničím a tak je zároveň vším. Naopak, ze seznamu neznámých objektů můžeme samozřejmě prvky číst, neboť každý prvek je určitě alespoň instancí třídy Ob j e ct. Ukážeme si praktické použití žolíku. public static void tiskniSeznam(List seznam) { for (Object e : seznam) { System.out.println(e); } } Nyní si představme, že chceme metodu, která udělá z nějakého seznamu čísel jeho sumu. Uvažujme tedy následující (a pomiňme možné přetečení nebo podtečení rozsahu double): public static double suma(List cisla) { double result = 0; for (Number e : cisla) { result += e.doubleValue() } return result; } Opět, metoda se jeví jako bezproblémová. Nic ale není tak jednoduché, jak by se mohlo zdát. Nyní zku- 6 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. símě uvažovat bez příkladu. Představme si, že máme seznam celých čísel, u kterého chceme provést sumu. Jistě není sporu o tom, že celá čísla jsou zároveň obecná čísla a přesto seznam List nelze použít jako parametr výše deklarované metody z naprosto stejného důvodu, kvůli kterému nešlo říci, že seznam objeků je seznam čísel. Samozřejmě je tu opět řešení. Zkusme nejdříve uvažovat selským rozumem. Výše jsme říkali, že místo seznamu objektů chceme seznam neznámých prvků. Nyní jsme v podobné situaci, pouze se nacházíme na jiném místě v hierarchii tříd. Zkusme tedy obdobnou úvahu použít i zde. Nechceme seznam čísel nýbrž seznam neznámých prvků, které jsou nejvýše čísly. Nyní je již pouze třeba ozřejmit syntaxi takové „úvahy". public static double suma(List cisla) { } Toto použití žolíku má uplatnění i v samotném rozhraní List a sice v metodě „přidej vše". Zamyslete se nad tím, proč tomu tak je. boolean addAll(Collection c) ; Uvědomte si prosím následující ~ prostý žolík je vlastně „zkratka" pro „neznámý prvek rozšiřující Ob- j ect". Ač by se tak mohlo zdát, možnosti wildcards jsme ještě nevyčerpali. Představme si situaci, kdy potřebujeme, aby možnou hodnotou byla instance třídy, která je v hierarchii mezi třídou specifikovanou naším obecným prvkem E a třídou Ob j ect. Pokud přemýšlíte, k čemu je něco takového dobré, představte si, že máte množinu celých čísel, které chcete setřídit. Jak lze taková čísla třídit? Například obecně podle hodnoty metody hashCode (), tedy na úrovni třídy Ob j ect. Nebo jako obecné číslo, tj. na úrovni třídy Number. A konečně i jako celé číslo na úrovni třídy Integer. Skutečně, níže již jít nemůžeme, protože libovolné zjemnění této třídy například na celá kladná čísla by nemohlo třídit obecná celá čísla. Následující příklad demonstruje syntaxi a použití popsané konstrukce public TreeMap(Comparator c); Jedná se o konstruktor stromové mapy, tj. mapy klíč/hodnota, která je navíc setříděna podle klíče. Nyní 7 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. opět trochu odbočíme a podíváme se, jak vypadá deklarace obecného rozhraní setříděné mapy. public interface SortedMap extends Map { Máme zde nový prvek ~ je-li třeba použít více nezávislých obecných typů, zapíšeme je opět do „zobáčků" jako seznam hodnot oddělených čárkou. Povšimněte si opět mnemotechniky ~ K je key (klíč), V je value (hodnota). Je-li to třeba, je možné použít i žolíků. Viz následující příklad konstruktorů naší staré známé stromové mapy. public TreeMap(Map m) ; public TreeMap(SortedMap m); Generické metody Tato část bude relativně krátká a stručná, poněvadž pro používání generics a žolíků platí stále stejná pravidla. Generickou metodou rozumíme takovou, která je parametrizována alespoň jedním obecným typem, který nějakým způsobem „váže" typy proměnných a/nebo návratové hodnoty metody. Představme si například, že chceme statickou metodu, která přenese prvky z pole nějakého typu přidá hodnoty do seznamu s prvky téhož typu. static void arrayToList(T[] array, List list) { for (T o : array) { list.add(o); } } Zde narážíme na malou záludnost. Ve skutečnosti nemusí být seznam list téhož typu, stačí, aby jeho typ byl nadtřídou typu pole array. To se může jevit jako velmi matoucí, ovšem pouze do té chvíle, dokud si neuvědomíme, že pokud máme např. pole celých čísel, tj. Integer a seznam obecných čísel Number, pak platí, že pole prvků typu Integer JE polem prvků typu Number! Skutečně, zde se dostáváme zpět ke klasické dědičnosti a nesmí nás mást pravidla, která platí pro obecné typy ve třídách. Generics metody vs. wildcards Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. Jak již bylo zmíněno, je žádoucí, aby typ použitý u generické metody spojoval alespoň dva parametry nebo parametr a návratovou hodnotu. Následující příklad demonstruje nesprávné použití generické metody. public static void copy(List destination, List source Příklad je syntakticky bezproblémový, dokonce jej lze i přeložit a bude fungovat dle očekávání. Nicméně správný zápis by měl být následující. public static void copy(List destination, List source); Zde je již vidět požadovaná vlastnost ~ T spojuje dva parametry metody a přebytečné s je nahrazené žolíkem. V prvním příkladu si všimněte zápisu s extends T . Ukazuje další možnou deklaraci generics. Pole Při deklaraci pole nelze použít parametrizovanou třídu, pouze třídu s žolíkem, který není vázaný (nebo bez použití žolíku). Tj. jediná správná deklarace je následující: List[] pole = new List[10]; Parametrizovanou třídu v seznamu nelze použít z toho důvodu, že při vkládání prvků do nich runtime systém kontroluje pouze typ vkládaného prvku, nikoliv už to, zda využívá generics a zda tento odpovídá deklarovanému typu. To znamená, že měli bychom například pole seznamů, které obsahují pouze řetězce, mohli bychom do něj bez problémů vložit pole čísel. To by samo o sobě nic nezpůsobilo, ovšem mohlo by dojít k „přeměně" typu generics, čímž by se seznam čísel „proměnil" na seznam řetězců, což by bylo špatně. Vícenásobná vazba generics Uvažujme následující metodu (bez použití generics), která vyhledává maximální prvek nějaké kolekce. Navíc platí, že prvky kolekce musí implementovat rozhraní Comparable, což, jak lze snadno nahlédnout, není syntaxí vůbec podchyceno a tudíž zavolání této metody může vyvolat výjimku ClassCas-tException . 9 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. public static Object max(Collection c); Nyní se pokusíme vymyslet, jak zapsat tuto metodu za použití generics. Chceme, aby prvky kolekce implementovali rozhraní Comparable. Podíváme-li se na toto rozhraní, zjistíme, že je též parametrizované generics. Potřebujeme tedy takovou instanci, která je schopná porovnat libovolné třídy v hierarchii nad třídou, která bude prvkem vstupní kolekce. První pokus, jak zapsat požadované. public static > T max(Collection Tento zápis je relativně OK. Metoda správně vrátí proměnnou stejného typu, jaký je prvkem v kolekci, dokonce i použití Comparable je správné. Nicméně, pokud bychom se zajímali o signaturu metody po „výmazu" generics, dostaneme následující. public static Comparable max(Collection c); To neodpovídá signatuře metody výše. Využijeme tedy vícenásobné vazby. i public static > T max (Coll Nyní, po „výmazu" má již metoda správnou signaturu, protože v úvahu se bere první zmíněná třída. Obecně lze použít více vazeb pro generics, například chceme-li, aby obecný prvek byl implementací více rozhraní. Závěr V článku jsme se seznámili se základními i některými pokročilými technikami použití generics. Tato technologie má i další využití, například u reflexe. Tohle však již překračuje rámec začátečnického se- lV materiálu, ze kterého čerpám, je navíc Collection Domnívám se ovšem, že metoda zmíněná v tomto článku má stejnou funkcionalitu. Pokud se někomu podaří nalézt protipříklad, budu rád. 10 Přednáška 9 - dokončení dynamických datových struktur (Mapy). Generické typy. znamování s Jávou. Celý článek vychází z materiálů, které jsou volně k disposici na oficiálních stránkách Javy firmy Sun [http://java.sun.com/], zejména pak z Generics in the Java Programming Language od Gilada Brachy. Některé příklady v této stati jsou převzaty ze zmíněného článku. 11