Sun, Sun Microsystems, Java a všechny obchodní známky a loga obsahující Sun nebo Java jsou ochrannými známkami nebo registrovanými ochrannými známkami firmy Sun Microsystems, Inc. v USA a v ostatních zemích. Windows je registrovaná obchodní známka firmy Microsoft v USA a v ostatních zemích. V knize použité názvy programových produktů, firem apod. mohou být ochrannými známkami nebo registrovanými ochrannými známkami příslušných vlastníků.
© Ing. Jarmila Pavlíčková, Ing. Luboš Pavlíček – Praha 2005 ISBN 80-245-0963-6
Obsah
strana 3
Obsah 1.
Úvod ..................................................................................................................................................... 9
2. Objekty .............................................................................................................................................. 11 2.1. Objekty a abstrakce...................................................................................................................... 11 2.2. Třída a instance ............................................................................................................................ 12 2.3. Volání metod (posílání zpráv) ..................................................................................................... 14 2.4. Zapouzdření ................................................................................................................................. 14 2.5. Ukrývání implementace ............................................................................................................... 14 2.6. Vytváření tříd v Javě.................................................................................................................... 15 2.6.1. Identifikátory .......................................................................................................................... 15 2.6.2. Datové atributy instance ......................................................................................................... 16 2.6.3. Metody instance...................................................................................................................... 17 2.6.4. Modifikátory přístupu k datovým atributům a metodám ........................................................ 20 2.6.5. Konstruktor ............................................................................................................................. 20 2.7. Vytváření instancí a volání metod v Javě .................................................................................... 22 2.7.1. Volání metod v rámci jedné instance (this) ............................................................................ 23 2.8. Modifikátor final.......................................................................................................................... 24 2.9. Rušení objektů ............................................................................................................................. 24 2.10. Balíčky (package) ........................................................................................................................ 25 2.11. Komentáře.................................................................................................................................... 26 3. Datové typy........................................................................................................................................ 29 3.1. Primitivní datové typy.................................................................................................................. 29 3.1.1. Deklarace a inicializace proměnné primitivního typu ............................................................ 29 3.1.2. Konstanty................................................................................................................................ 30 3.1.3. Pojmenované konstanty .......................................................................................................... 30 3.1.4. Přetypování ............................................................................................................................. 31 3.1.5. Přetečení ................................................................................................................................. 31 3.2. Výrazy a operátory....................................................................................................................... 31 3.2.1. Operátor přiřazení (=), přiřazovací příkaz .............................................................................. 31 3.2.2. Aritmetické operátory ............................................................................................................. 32 3.2.3. Relační operátory.................................................................................................................... 33 3.2.4. Logické operátory ................................................................................................................... 34 3.2.5. Bitové operátory ..................................................................................................................... 34 3.2.6. Podmíněný výraz .................................................................................................................... 34 3.2.7. Kulaté závorky........................................................................................................................ 34 3.3. Referenční datové typy ................................................................................................................ 35 3.3.1. Rozdíl mezi primitivními a referenčními datovými typy ....................................................... 35 3.3.2. Konstanta null ......................................................................................................................... 35 3.3.3. Přetypování referenčních typů ................................................................................................ 35 3.3.4. Operátor přiřazení (přiřazovací příkaz) .................................................................................. 36 3.3.5. Relační operátory == a != ....................................................................................................... 36 3.3.6. Relační operátor instanceof .................................................................................................... 37 3.3.7. Volání metod .......................................................................................................................... 37 3.4. Obalové třídy pro primitivní typy ................................................................................................ 37
Obsah
strana 4
4. Základní konstrukce metod ............................................................................................................. 41 4.1. Sekvence ...................................................................................................................................... 41 4.1.1. Rozsah platnosti lokálních proměnných ................................................................................. 41 4.2. Selekce ......................................................................................................................................... 42 4.2.1. Příkaz if................................................................................................................................... 42 4.2.2. Příkaz switch........................................................................................................................... 43 4.3. Iterace (cykly) .............................................................................................................................. 45 4.3.1. Příkaz while ............................................................................................................................ 45 4.3.2. Příkaz do-while ....................................................................................................................... 46 4.3.3. Příkaz for ................................................................................................................................ 46 4.3.4. Příkazy break a continue......................................................................................................... 47 4.4. Prázdný příkaz ............................................................................................................................. 48 5. Řetězce (třída String) ....................................................................................................................... 49 5.1. Porovnávání řetězců..................................................................................................................... 49 5.2. Další operace s řetězci ................................................................................................................. 50 5.3. Speciální (escape) znaky v řetězcích ........................................................................................... 51 5.4. Formátování řetězců..................................................................................................................... 51 5.5. Regulární výrazy.......................................................................................................................... 53 5.6. Třídy StringBuffer a StringBuilder .............................................................................................. 55 6. Třída Object...................................................................................................................................... 57 6.1. Metoda toString()......................................................................................................................... 57 6.2. Metoda equals() ........................................................................................................................... 57 6.3. Metoda hashCode()...................................................................................................................... 58 6.4. Metoda getClass() ........................................................................................................................ 58 6.5. Metoda clone() ............................................................................................................................. 58 6.6. Metoda finalize().......................................................................................................................... 58 6.7. Metody notify(), notifyAll(), wait() ............................................................................................. 59 7. Statické prvky třídy.......................................................................................................................... 61 7.1. Statické datové atributy (statické proměnné, proměnné třídy) .................................................... 61 7.2. Statická metoda (metoda třídy) .................................................................................................... 62 7.3. Statické datové atributy a metody ve třídách Math a System ...................................................... 62 7.3.1. Třída Math .............................................................................................................................. 62 7.3.2. Třída System........................................................................................................................... 63 7.4. Standardní spouštění aplikace – metoda main ............................................................................. 64 7.5. Počítání vytvořených instancí ...................................................................................................... 65 8. Výčtový typ........................................................................................................................................ 67 8.1. Pojmenované konstanty ............................................................................................................... 67 8.2. Výčtový typ (enum) ..................................................................................................................... 68 8.3. Metody výčtového typu ............................................................................................................... 69 8.4. Rozšíření příkazu switch.............................................................................................................. 69 8.5. Výčtový typ uvnitř třídy............................................................................................................... 69 8.6. Výčtový typ a datové struktury.................................................................................................... 70 8.7. Rozšiřování výčtového typu ........................................................................................................ 70
Obsah
strana 5
9. Polymorfismus a rozhraní................................................................................................................ 73 9.1. Přetěžování metod a polymorfismus............................................................................................ 73 9.1.1. Přetěžování metod................................................................................................................... 73 9.1.2. Příklad polymorfismu s přetěžováním metod ......................................................................... 74 9.2. Rozhraní....................................................................................................................................... 75 9.2.1. Deklarace rozhraní.................................................................................................................. 76 9.2.2. Rozhraní umožňuje volbu implementace................................................................................ 77 9.2.3. Rozhraní a polymorfismus...................................................................................................... 78 9.2.4. Rozhraní umožňuje předávat metody jako parametr .............................................................. 79 9.2.5. Rozhraní a vícenásobná dědičnost .......................................................................................... 79 9.2.6. Dědičnost mezi rozhraními ..................................................................................................... 79 10. Datové struktury............................................................................................................................... 81 10.1. Collections framework................................................................................................................. 81 10.2. Základní rozhraní ......................................................................................................................... 82 10.3. Kolekce ........................................................................................................................................ 82 10.3.1. Seznamy (List)........................................................................................................................ 83 10.3.2. Množiny (Set) ......................................................................................................................... 84 10.3.3. Procházení kolekce (rozšířený příkaz for) .............................................................................. 85 10.3.4. Používání indexů u seznamů................................................................................................... 86 10.3.5. Používání iterátoru.................................................................................................................. 87 10.3.6. Prohledávání kolekce.............................................................................................................. 88 10.3.7. Třídění kolekcí........................................................................................................................ 89 10.4. Mapy (Map) ................................................................................................................................. 93 10.4.1. Procházení mapy..................................................................................................................... 95 10.4.2. Mapa pro urychlení vyhledávání ............................................................................................ 96 10.4.3. Mapy obsahující seznamy....................................................................................................... 96 10.5. Pole (array) .................................................................................................................................. 97 10.5.1. Jednorozměrné pole ................................................................................................................ 98 10.5.2. Procházení jednorozměrného pole.......................................................................................... 99 10.5.3. Proměnlivý počet parametrů metody ...................................................................................... 99 10.5.4. Vícerozměrná pole................................................................................................................ 100 10.5.5. Parametry vstupní řádky ....................................................................................................... 101 11. Dědičnost ......................................................................................................................................... 103 11.1. Dědičnost a konstruktory, klíčové slovo super .......................................................................... 106 11.2. Vytvoření instance při dědičnosti .............................................................................................. 107 11.3. Modifikátor přístupu protected .................................................................................................. 108 11.4. Abstraktní třídy .......................................................................................................................... 108 11.5. Polymorfismus a překrytí metod................................................................................................ 111 11.5.1. Přetypování referenčních typů .............................................................................................. 111 11.5.2. Překrývání metod, pozdní vazba a polymorfismus............................................................... 112 11.5.3. Příklad polymorfismu ........................................................................................................... 113 11.6. Použití dědičnosti....................................................................................................................... 115 11.6.1. Důvody pro použití dědičnosti.............................................................................................. 115 11.6.2. Porušení vztahu „je nějakým“ při návrhu dědičnosti............................................................ 116 11.6.3. Dědičnost narušuje zapouzdření ........................................................................................... 117 12. Výjimky ........................................................................................................................................... 119 12.1. Druhy výjimek ........................................................................................................................... 119 12.2. Vyvolání výjimky ...................................................................................................................... 120 12.3. Ošetření výjimky........................................................................................................................ 121 12.3.1. Odchytávání výjimek (try catch) .......................................................................................... 121 12.3.2. Throws .................................................................................................................................. 123 12.4. Často používané typy výjimek................................................................................................... 123 13. Vstupy a výstupy............................................................................................................................. 125
Obsah
strana 6
13.1. Základní principy práce se soubory ........................................................................................... 125 13.2. Vstupy a výstupy v Javě ............................................................................................................ 126 13.3. Vstupní proudy........................................................................................................................... 127 13.3.1. Čtení z textového souboru .................................................................................................... 128 13.3.2. Čtení z konzole ..................................................................................................................... 129 13.4. Výstupní proudy......................................................................................................................... 130 13.4.1. Zápis do textového souboru.................................................................................................. 131 13.5. Další třídy a metody pro práci s proudy..................................................................................... 132 13.5.1. Čtení ze sítě........................................................................................................................... 132 13.5.2. Komprimace a dekomprimace souborů ................................................................................ 133 13.6. Třída File.................................................................................................................................... 133 14. Projekt Obrázek ............................................................................................................................. 135 14.1. Základní popis, zadání úkolu ..................................................................................................... 135 14.2. Struktura tříd .............................................................................................................................. 135 14.3. Popis metod jednotlivých tříd .................................................................................................... 136 14.4. Kód třídy Obrazek...................................................................................................................... 137 14.5. Postup řešení .............................................................................................................................. 139 14.5.1. Dokreslení obrázku (přidání slunce)..................................................................................... 139 14.5.2. Průjezd auta .......................................................................................................................... 140 14.5.3. Východ a západ slunce ......................................................................................................... 140 14.6. Domácí úkoly............................................................................................................................. 142 15. Projekt Kalkulačka......................................................................................................................... 143 15.1. Základní popis, zadání úkolu ..................................................................................................... 143 15.2. Struktura tříd .............................................................................................................................. 143 15.3. Popis komunikace mezi objekty ................................................................................................ 144 15.4. Popis kódu třídy Kalkulator ....................................................................................................... 144 15.5. Postup řešení: ............................................................................................................................. 146 15.5.1. Doplnění autora a verze ........................................................................................................ 146 15.5.2. Vkládání prvního čísla .......................................................................................................... 147 15.5.3. Operace výmaz (C) ............................................................................................................... 148 15.5.4. Operace plus (+) ................................................................................................................... 149 15.5.5. Operace mínus (−) ................................................................................................................ 149 15.5.6. Komunikace mezi instancemi............................................................................................... 151 15.5.7. Vkládání dalších čísel ........................................................................................................... 151 15.5.8. Operace plus – pokračování.................................................................................................. 153 15.5.9. Řešení s konstantami ............................................................................................................ 154 15.6. Domácí úkoly............................................................................................................................. 154 16. Projekt Hádání slov ........................................................................................................................ 157 16.1. Základní popis, zadání úkolu ..................................................................................................... 157 16.2. Struktura tříd .............................................................................................................................. 157 16.3. Popis komunikace mezi objekty ................................................................................................ 158 16.4. Popis kódu třídy PoskytovatelSlov ............................................................................................ 159 16.5. Postup řešení .............................................................................................................................. 161 16.5.1. Přidání možnosti hádat více slov .......................................................................................... 161 16.5.2. Poskytování slov k hádání v náhodném pořadí..................................................................... 162 16.6. Domácí úkol............................................................................................................................... 164 17. Projekt Trojúhelníky...................................................................................................................... 165 17.1. Základní popis, zadání úkolu ..................................................................................................... 165 17.2. Struktura tříd .............................................................................................................................. 165 17.3. Popis komunikace mezi objekty ................................................................................................ 166 17.4. Popis výčtového typu (enum) TypTrojuhelnika ........................................................................ 168 17.5. Popis kódu třídy Trojuhelnik ..................................................................................................... 168 17.6. Popis kódu třídy Trojuhelniky ................................................................................................... 172
Obsah
strana 7
17.7. Postup řešení .............................................................................................................................. 174 17.7.1. Vytvoření metody main, pro spouštění aplikace z příkazové řádky ..................................... 174 17.7.2. Přidání nového typu trojúhelníka (rovnostranného) ............................................................. 174 17.7.3. Zjišťování chyb ve třídě Trojuhelnik, generování výjimek .................................................. 175 17.7.4. Ošetřování výjimek ve třídě Trojuhelniky............................................................................ 177 17.8. Domácí úkoly............................................................................................................................. 178 18. Projekt Škola................................................................................................................................... 179 18.1. Základní popis, zadání úkolu ..................................................................................................... 179 18.2. Struktura tříd .............................................................................................................................. 179 18.3. Popis kódu třídy Skola ............................................................................................................... 180 18.4. Popis kódu tříd Utvar a Osoba ................................................................................................... 181 18.5. Postup řešení .............................................................................................................................. 182 18.5.1. Deklarace datových atributů a vkládání instancí .................................................................. 182 18.5.2. Výpis organizační struktury.................................................................................................. 183 18.5.3. Setřídění výpisu .................................................................................................................... 184 18.5.4. Zvýraznění organizační struktury (odsazování).................................................................... 185 18.5.5. Doplnění výpisu osob ........................................................................................................... 186 18.6. Domácí úkoly............................................................................................................................. 186 19. Projekt Adventura .......................................................................................................................... 189 19.1. Základní popis, zadání úkolu ..................................................................................................... 189 19.2. Struktura tříd .............................................................................................................................. 189 19.3. Popis komunikace mezi objekty ................................................................................................ 190 19.4. Výpis kódu třídy Místnost ......................................................................................................... 192 19.5. Výpis kódu třídy Hra ................................................................................................................. 194 19.6. Výpis kódu třídy RizeniHry....................................................................................................... 198 19.7. Výpis kódu třídy SeznamPrikazu............................................................................................... 199 19.8. Výpis kódu třídy Prikaz ............................................................................................................. 200 19.9. Postup řešení .............................................................................................................................. 201 19.9.1. Úkol 1: doplnění další místnosti ........................................................................................... 201 19.9.2. Úkol 2: změna příkazu „napoveda“ na „pomoc“ .................................................................. 202 19.9.3. Úkol 3: doplnění vítězství..................................................................................................... 204 19.9.4. Úkol 4: doplnění věcí do místností ....................................................................................... 205 19.10. Domácí úkol............................................................................................................................... 209 20. Projekt Domácí mediotéka............................................................................................................. 211 20.1. Základní popis, zadání úkolu ..................................................................................................... 211 20.2. Struktura tříd .............................................................................................................................. 211 20.3. Výpis kódu třídy Evidence......................................................................................................... 212 20.4. Výpis kódu tříd Video a CD ...................................................................................................... 213 20.5. Postup řešení .............................................................................................................................. 216 20.5.1. Vytvoření předka tříd Video a CD........................................................................................ 216 20.5.2. Přesun titulu a délky do předka............................................................................................. 218 20.5.3. Jeden seznam ve třídě Evidence ........................................................................................... 220 20.5.4. Přidání knih........................................................................................................................... 222 20.6. Domácí úkoly............................................................................................................................. 224 21. Seznam termínů a zkratek ............................................................................................................. 225 22. Literatura a webové zdroje............................................................................................................ 227
Úvod
strana 9
1. Úvod Tento text je určen pro studenty úvodního kurzu programování studijního oboru Informatika. Obsahuje úvod do objektové teorie, její realizaci v jazyce Java a popis základních jazykových konstrukcí. Javu učíme v základním kurzu programování od roku 2000, ze začátku paralelně s Pascalem, od roku 2004 výhradně Javu. Do roku 2004 jsme první semestr učili studenty programovat v Javě procedurálně a až druhý semestr jsme se snažili vysvětlit objekty. Naráželi jsme však na následující problémy: ♦ Část studentů v úvodním kurzu již měla předchozí znalosti programování – obvykle Pascalu či PHP. Vzhledem ke svým předchozím zkušenostem s programováním a algoritmizací se tito studenti na úvodních cvičeních nudili a nemuseli se programování věnovat mimo cvičení. Pak se jim často stávalo, že „zaspali“ okamžik, kdy by se měli začít učit. Vytvářelo to i špatnou atmosféru pro začínající studenty – ti viděli, že někteří jejich kolegové nemají žádné problémy s programy a byli z toho zbytečně frustrovaní. ♦ Mezi algoritmickým myšlením a chápáním objektů je poměrně velký myšlenkový rozdíl. Mnoho studentů nebylo schopno v rámci druhého semestru přejít od algoritmického myšlení k objektovému. Nechápali, proč mají používat objekty, když předtím používali objektový jazyk neobjektově. Vzhledem k rozsahu jimi laděných programů je obtížné ukázat jim výhody objektového přístupu pro řešení složitých úloh. V Computing Curricula [CC2001] z roku 2001 se doporučuje v úvodních kurzech programování začít objektovým přístupem, tj. od začátku výuky programování soustředit pozornost na objektověorientované programování a návrh. Za základní výhodu tohoto přístupu je označováno brzké seznámení s objektově orientovaným programováním, které je standardem při tvorbě programového vybavení. Objektový přístup je výhodný pro složitější úlohy, tudíž studenti se při výuce budou od začátku seznamovat se složitějšími aplikacemi, které mají složitější vnitřní vazby. V rámci katedry jsme se rozhodli přejít při výuce programování k přístupu object-first. V úvodním kurzu tedy začínáme výukou objektů, v navazujícím kurzu Základy softwarového inženýrství si pak studenti tyto znalosti prohloubí a seznámí se i s návrhovými vzory, testováním tříd atd. Objekty byly navrženy pro pochopení/zvládnutí složitých problémů. Studentům je potřeba ukazovat používání objektů na „složitých“ problémech. Přitom je potřeba výuku připravit tak, aby studenti mohli postupovat od jednoduchého ke složitějšímu. V naší výuce objektů jsme se inspirovali přístupem M. Köllinga [Kolling], autora výukového vývojového prostředí BlueJ a propagátora přístupu k výuce object-first. Rozhodli jsme se pro následující postup ve výuce: ♦ nejdříve ukážeme studentům na existující aplikaci, jak fungují objekty, jaký je rozdíl mezi třídou a instancí (vytvořit více instancí), jak volat metody instancí, ♦ na jiné úloze studenti mění hodnoty řetězců, naučí se na tom překládat a rozumět chybovým hlášením překladače, ♦ dále následuje úprava obsahu metod či psaní obsahu metod, ♦ dalším krokem je návrh metod v existujících třídách, ♦ dále studenti k existujícím třídám navrhují další třídy, ♦ posledním krokem je návrh aplikace od začátku do konce. Na přednáškách vykládáme objekty a jazyk, na cvičeních se řeší problémy. Cílem cvičení je trénování objektového myšlení na řešení předložených problémů, tj. ukázat jim problémy, diskutovat s nimi jak je řešit. Tomuto členění odpovídá i rozdělení tohoto textu do dvou částí. První část (kapitoly 2 – 13) obsahuje výklad jazykových konstrukcí. Zdrojové kódy v těchto kapitolách jsou většinou krátké ukázky, nejsou u nich uvedeny komentáře, protože jsou podrobně vysvětleny v textu. Jednotlivé kapitoly jsou řazeny tak, aby se postupovalo od základních vlastností a prvků ke složitějším. V druhé části (kapitoly 14 – 20) jsou popsány některé výukové projekty používané na cvičení. Zde jsou uvedeny celé zdrojové kódy, včetně komentářů. Studenti by se měli mimo jiné naučit rozumět cizímu zdrojovému kódu. Každý projekt končí zadáním domácích úkolů, které mají sloužit jako inspirace pro samostatnou práci studentů. I tyto projekty jsou řazeny od jednodušších ke složitějším.
Úvod
strana 10
Tato skripta, stejně jako kurz pro který jsou určena, si nekladou za cíl naučit studenty všechny jazykové konstrukce a možnosti Javy. Z těchto důvodů ve skriptech nejsou popsány některé jazykové konstrukce z jazyka Java: ♦ vnitřní třídy a anonymní vnitřní třídy, ♦ generické typy (ve skriptech je popsáno pouze jejich používání), ♦ anotace, ♦ serializace objektů, ♦ problematika vláken a synchronizace. Nejsou zde uvedena pravidla pro navrhování tříd a návrhové vzory. Nejsou zde mnohé standardní knihovny Javy – např. Swing a AWT, pomocí kterých se vytvářejí grafické aplikace, dále práce s databázemi, komunikace po síti, vytváření webových aplikací atd. Závěrem bychom chtěli poděkovat za cenné připomínky Ing. Martině Jandové, Ing. Rudolfu Pecinovskému, CSc. a za recenzní posudky Ing. Aleně Buchalcevové, PhD. a Prof. Janu Štelovskému. Doufáme, že tato skripta pomohou čtenářům při pronikání do tajů objektově orientovaného programování a programovacího jazyka Java. autoři
Objekty
strana 11
2. Objekty Java je objektově orientovaný programovací jazyk přenositelný na různé platformy. Pracuje tedy s objekty. Co to vlastně jsou objekty? Jedná se o abstrakci z reality, každý objekt představuje spojení dat (údajů, proměnných, datových atributů) a činností s těmito daty (metod). Tato abstrakce je vždy účelová, o každém reálném objektu se sledují ty údaje, které jsou relevantní pro aplikaci. Pokud chceme pracovat s objekty, je nutné vědět, jaké jsou obecné vlastnosti objektů. Nyní tyto vlastnosti uvedeme a postupně si je objasníme. V této kapitole se také seznámíme se spoustou pojmů z objektového programování. Obecné objektové vlastnosti: ♦ používání abstrakce ♦ definování tříd objektů ♦ existence objektů (instancí) ♦ komunikace objektů (posílání zpráv, volání metod) ♦ zapouzdření a ukrývání implementace ♦ dědičnost ♦ polymorfismus
Základní pojmy: ♦ objekty ♦ třídy ♦ instance ♦ datové atributy ♦ metody ♦ konstruktory ♦ balíčky ♦ deklarace ♦ inicializace ♦ identifikátor ♦ formální parametr metody ♦ skutečný parametr metody ♦ pomocná proměnná
2.1. Objekty a abstrakce Abstrakce je základní objektovou vlastností. Skutečnost, kterou chceme do programu promítnout, musíme vždy zjednodušit, pracovat jen s těmi daty, která jsou pro nás důležitá. Uvedeme si několik jednoduchých příkladů. První příklad: Když chceme udělat počítačovou evidenci knih, které jsou k dispozici v obchodě, bude základem abstrakce knihy. V knihkupectví nás bude pravděpodobně u každé knihy zajímat: autor, název, ISBN, vydavatel, žánr, cena, počet kusů na skladě. S knihou budeme provádět např. tyto činnosti: založení nové knihy, změna množství na skladě, změna ceny. Druhý příklad: Vytváříme jednoduchou evidenci knih ve své knihovně, abychom měli přehled o knihách. Protože je často půjčujeme, chceme si půjčky evidovat. Základem je opět kniha, ale tentokrát nás o ní bude zajímat autor, název, žánr, komu je zapůjčena atd. Činnosti budou např.: zapiš výpůjčku nebo kniha vrácena. Třetí příklad: Chceme napsat aplikaci pro kreslení. Tvary (čtverce, obdélníky, kruhy, trojúhelníky atd.), které budou nakresleny, jsou objekty. U každého nakresleného tvaru musíme sledovat např. tato data: souřadnice umístění, rozměry tvaru, barvu čáry, barvu výplně. Metody neboli činnosti, které bude možno v takové aplikaci provádět s jednotlivými tvary, budou např. tyto: nakreslení, zvětšení, zmenšení, posun, změna barvy, vymazání. Čtvrtý příklad: Pokud budeme psát aplikaci pro počítání obvodů a obsahů dvourozměrných tvarů, budou zde podobné objekty, ale s jinými daty a metodami. U jednotlivých tvarů stačí sledovat rozměry stran a popřípadě velikosti úhlů. Metody, které budou k dispozici, budou metody pro výpočet obsahu a obvodu.
Objekty
strana 12
Pátý příklad: Jedná se o velmi zjednodušenou „banku“. Základem každé banky jsou účty jejích klientů. Každý učet v naší bance bude mít pouze číslo účtu, jméno vlastníka a uloženou částku. Činnosti, které bude možné s jednotlivými účty provádět, mohou být např. tyto: založení účtu, zjištění stavu účtu, výběr z účtu, uložení na účet, převod na jiný učet. Tento příklad budeme dále používat pro objasňování dalších pojmů a jejich realizace v Javě.
2.2. Třída a instance Vraťme se k našemu příkladu s bankou. V bance je samozřejmě mnoho účtů. Programátor musí vytvořit obecný popis všech účtů, vytvořit třídu Účet. Třída je obecný popis, ve kterém se deklarují (určí) data (datové atributy), která budou popisovat stav objektu, a metody, které definují činnosti, jaké je možné s objekty provádět. Jak je vidět na následujícím obrázku, programátor definuje, které údaje se budou o objektech sledovat a jaké činnosti bude s těmi daty možno provádět a jak se to bude dělat.
Obrázek 2.1 Popis jednotlivých částí třídy V programu se poté vytvoří několik instancí této třídy, představující jednotlivé účty. Instance je tedy vytvořena v paměti počítače a vytváří jakýsi obraz reálného objektu např. učet Josefa Nováka nebo účet Jarmily Pavlíčkové.
Obrázek 2.2 Znázornění rozdílu mezi třídou a jejími instancemi
Objekty
strana 13
Data, která budeme o každé instanci sledovat, označujeme jako datové atributy instance. Činnosti, které je možné s danými instancemi provádět, označujeme jako metody (metody instance). Konstruktor je specifická metoda, pomocí které se vytvářejí instance. Při spouštění konstruktoru si musíme uložit odkaz (referenci) na vznikající instanci. K již vzniklé instanci je možno přiřadit i další odkazy. Na obrázku 2.3 máme znázorněny odkazy na účty s čísly 1 až 3. Na účet číslo 4 odkaz nemáme a nemůžeme s ním dále pracovat. Nelze na něm zavolat žádnou metodu (poslat mu zprávu). Na účty s čísly 1 a 3 byly vytvořeny další odkazy. Je možné jim poslat zprávu „ze dvou míst“.
Obrázek 2.3 Znázornění odkazů na instance tříd V objektovém programování se používají pojmy objekt a instance jako ekvivalenty, my budeme používat termín instance, protože je přesnější. Abychom si tyto pojmy ještě více ujasnili, uvedeme si další příklad. Budeme vytvářet jednoduchou evidenci vozidel. Auta stojící na ulici jsou objekty, o kterých budou v programu „záznamy“. Pomocí abstrakce vytvoříme obecný popis auta vhodný pro náš účel a vytvoříme tak třídu Auto. Pokud bude aplikace fungovat, bude pro každý objekt v realitě vytvořena odpovídající instance třídy Auto v paměti počítače. Obrázek 2.4 znázorňuje popsanou situaci.
Obrázek 2.4 Význam pojmů objekt, třída a instance
Objekty
strana 14
2.3. Volání metod (posílání zpráv) Každá aplikace je tvořena několika třídami, v rámci běhu aplikace jsou vytvářeny instance těchto tříd a volány jejich metody. Volání metod se tak trochu podobá posílání SMS z mobilního telefonu. Můžeme poslat SMS jen tomu, na koho máme číslo. Posíláme např. zprávu „Kup chleba!“ Pokud příjemce neumí česky, je nám to na nic (voláme metodu, kterou daná instance nezná, v Javě tento omyl odchytí již překladač). Pokud pošleme tuto zprávu např. Honzovi, ten na koupení chleba zapomene, když ji pošleme Pepovi, ten chleba přinese. U objektů mohou instance různých tříd reagovat na stejnou zprávu různě, v našem případě by Honza musel být instancí třídy Lajdák a Pepa instancí třídy Svědomitý. Jedna instance může také na stejnou zprávu reagovat různě v závislosti na svém stavu – pokud bude svědomitý Pepa ve stavu spaní, tak žádný chleba nepřinese. Příklad s tvary: Když pošleme instanci třídy Ctverec zprávu kresli(), nakreslí se čtverec. Když pošleme stejnou zprávu (zavoláme metodu kresli()) instanci třídy Kruh, nakreslí se kruh. Tento jev, kdy posíláme stejnou zprávu (tj. voláme metodu stejného jména), ale provádí se činnosti různě implementované, se označuje jako polymorfismus.
2.4. Zapouzdření Pojem zapouzdření (encapsulation) popisuje princip umísťování dat a souvisejících metod k sobě – do jednoho objektu, do jedné metody, atd. Zapouzdření musí být podporováno vhodnou jazykovou konstrukcí, v Javě i ostatních objektových jazycích se realizuje pomocí třída a vytváření instancí. Při zapouzdřování objektů je vhodné dodržovat tato pravidla: ♦ Snažit se umístit data a operace pracující s daty do stejné třídy. V našem příkladu s bankou jsou ve třídě Účet umístěny nejen datové atributy, ale i metody, které s účtem pracují. Chybou zapouzdření by bylo např. umístit metody pro „výběr z účtu“, „uložení na účet“ a pro „převod na jiný účet“ do samostatné třídy1. ♦ Třída by neměla obsahovat jen část dat či část metod, ani by neměla obsahovat více dat či metod, než je nutné pro činnost, za kterou třída odpovídá. Častější chybou je, že třída obsahuje více dat, než kolik potřebuje. V našem příkladu s bankou by např. bylo chybou, kdyby se ve třídě Účet evidovala adresa banky. ♦ Jednotlivé zprávy posílané instanci by pokud možno měly být na sobě nezávislé, metody by měly požadovat jen nezbytně nutné parametry. Chybou tohoto typu by bylo, kdybychom metodu pro převod peněz na jiný účet rozdělili do dvou metod – v první metodě bychom nastavili číslo cílového účtu, ve druhé zadali převáděnou částku a provedli vlastní převod. Správným řešením je, že metoda bude obsahovat dva parametry – číslo cílového účtu a převáděnou částku a po počátečních kontrolách provede převod částky na zadaný účet.
2.5. Ukrývání implementace Každý objekt poskytuje svému okolí metody, které je možné zavolat. Seznam těchto metod (a dostupných datových atributů) je označován jako veřejné rozhraní třídy. V tomto rozhraní by neměly být zahrnuty datové atributy, ty by měly být schovány uvnitř instance a měly by být přístupné pouze pomocí metod. Pro ilustraci v našem jednoduchém příkladě s účty by hodnota stavu účtu neměla být z vnějšku přístupná. K zjištění stavu a změnám hodnot by měly sloužit metody „zjištění stavu účtu“, „výběr z účtu“, „vložení na účet“. Důvodem je, že pomocí metod může docházet k dalším kontrolám a instance se tak nemůže dostat do nesprávného stavu. V případě našeho účtu např. není povolen výběr do mínusu a v metodě pro výběr se tedy bude kontrolovat, zda je možno výběr uskutečnit nebo ne. Tento přístup – poskytování pouze nezbytně nutných metod pro uživatele třídy – se nazývá ukrývání implementace (information hiding). 1
Umísťování metod do samostatných jednotek se používalo dlouho ve strukturovaném programování. Jednou z nevýhod bylo velké množství parametrů těchto metod.
Objekty
strana 15
Při návrhu tříd je vhodné se řídit těmito pravidly: ♦ ukrývejte datové atributy – datové atributy by neměly být dostupné přímo, ale pomocí metod. ♦ ukrývejte implementaci třídy a minimalizujte veřejné rozhraní. Metody, které nejsou nutné pro uživatele třídy, znepřístupněte pro uživatele třídy. Postupy, které jsou složitější či u kterých lze předpokládat změny (např. konkrétní algoritmus třídění či konkrétní algoritmus pro výpočet úroků) umístěte do vnitřních metod, které nebude možné přímo volat. Minimalizace rozhraní by však neměla být na úkor použitelnosti vytvářené třídy. Předpokladem pro ukrývání implementace je, že programovací jazyk podporuje zapouzdření2. Oba pojmy spolu úzce souvisí, což někdy vede k jejich nepřesnému používání či vzájemnému zaměňování.
2.6. Vytváření tříd v Javě Když chceme vytvořit třídu v Javě, musíme napsat její zdrojový kód do souboru. Zdrojové kódy se píší do textových souborů (formát prostý text). Soubor se zdrojovým kódem třídy má koncovku .java a jmenuje se stejně jako třída, která je v něm popsána. V souboru mohou být na začátku uvedeny klauzule package a import, jejichž význam si objasníme později. Popis třídy začíná hlavičkou třídy, v níž musí být uvedeno klíčové slovo class a jméno třídy. Další níže uvedené prvky jsou volitelné a budou objasněny postupně. Tělo třídy je uvedeno mezi složenými závorkami. Zde je třeba uvést, jaké datové atributy bude třída obsahovat a jaké metody bude možné volat. Třída může obsahovat i další prvky, které budou popsány později. Obecně vypadá deklarace třídy takto: [ package jménoBalíčku; ] [ import JménoTřídy;.... ] [public] [final | abstract] class jmeno [extends JménoRodičovskéTřídy] [implements JménoRozhraní …] { datové atributy třídy; datové atributy; vnořené třídy; konstruktory; metody; }
Hlavička třídy Ucet z našeho úvodního příkladu bude vypadat takto: public class Ucet { //zde bude popsáno jaké datové atributy a metody bude třída obsahovat }
2.6.1. Identifikátory Jméno třídy, datového atributu, metody atd. se označuje jako identifikátor. Pojmenování jednotlivých součástí třídy, i třídy samotné, se řídí určitými pravidly. Základní pravidla pro vytváření identifikátorů v Javě jsou následující: ♦ identifikátor je tvořen posloupností písmen, číslic a podtržítka, začíná písmenem, ♦ Java rozlišuje malá a velká písmena: cislo a Cislo jsou dva různé identifikátory, ♦ identifikátor nesmí obsahovat klíčové slovo Javy (seznam klíčových slov pro verzi 5.0 je uveden v tabulce 2.1).
2
Současné objektové programovací jazyky podporují obě vlastnosti, v historii se však najdou jazyky, které podporovaly pouze zapouzdřování.
Objekty abstract assert boolean break byte case catch char class const
strana 16 continue default do double else enum extends final finally float
for if goto implements import instanceof int interface long native
new package private protected public return short static strictfp super
switch synchronized this throw throws transient try void volatile while
Tabulka 2.1 Seznam klíčových slov Javy 5.0 Tato pravidla pro identifikátory jsou závazná a kontroluje je překladač. Další pravidla pro identifikátory stanovená firmou Sun je vhodné dodržovat pro lepší čitelnost a pochopitelnost kódu, ale nejsou kontrolována překladačem. Nyní uvedeme alespoň základní doporučení. ♦ Identifikátor by měl vystihovat obsah proměnné/datového atributu, význam formálního parametru, třídy nebo činnost metody. ♦ Při vytváření identifikátorů se v Javě používá tzv. velbloudí notace (výjimku představují konstanty), u víceslovných identifikátorů je každé první písmeno slova uvedeno velkým písmenem např. rodneCislo, stavUctu. Zda je první písmeno identifikátoru malé či velké se řídí následujícími pravidly: * velké písmeno na začátku, počáteční písmena každého dalšího slova velká, ostatní písmena malá – používá se pro označení třídy a rozhraní, * malé písmeno na začátku, počáteční písmena dalších slov velká, ostatní písmena malá – používá se pro proměnné tříd a instancí, pro pomocné proměnné metod, pro formální parametry metod a pro jména metod, * všechna písmena velká, jednotlivá slova oddělena podtržítkem _ – používá se pro pojmenované konstanty. Nedoporučujeme používání diakritiky v identifikátorech.
2.6.2. Datové atributy instance Datové atributy uchovávají informace o instanci mezi jednotlivými voláními metod. Každý datový atribut musí mít určený typ a jméno (identifikátor). Určení jména a typu se označuje jako deklarace. Přehled datových typů v Javě je uveden v kapitole 3, zde si pouze uvedeme, že vytvořením nové třídy vznikne další datový typ. Před datovým typem je uveden modifikátor přístupu, který určuje úroveň zapouzdření. V případě datových atributů se téměř vždy používá modifikátor private (podrobnosti viz podkapitola „Modifikátory přístupu k datovým atributům a metodám“ na straně 20). Další specifické modifikátory budou objasněny v následujících kapitolách. Deklarace datového atributu se obecně zapisuje takto: [modifikátory] typ identifikátor;
Následují příklady: private int cisloUctu; //typ int je celé číslo private String jmenoVlastnika; //typ String znamená řetězec znaků private double castka; //typ double je desetinné číslo
Nastavení počáteční hodnoty se nazývá inicializace. Pokud není u datových atributů explicitně uvedena, přiřadí se defaultní hodnota. Deklaraci a inicializaci je možné spojit do jednoho příkazu. [modifikátory] typ identifikátor = hodnota;
Objekty
strana 17
Příklad následuje: private double castka = 0;
Každý příkaz v Javě je ukončen středníkem a píše se na samostatný řádek (z důvodu lepší čitelnosti kódu). Deklarace všech datových atributů by měly být na jednom místě v kódu, obvykle se uvádějí na začátku nebo na konci třídy. My budeme deklarace datových atributů uvádět vždy na začátku třídy. Deklarace třídy Ucet včetně datových atributů tedy bude vypadat takto: public class Ucet {
}
private int cisloUctu; private String jmenoVlastnika; private double castka = 0; //zde budou následovat metody
2.6.3. Metody instance Metody představují dovednosti, činnosti, které může objekt (instance) provádět. Metody jsou deklarovány ve třídě. Metoda se skládá s hlavičky (podpisu) metody a těla metody, které je tvořeno pomocí příkazů a deklarací lokálních proměnných uvedených mezi složenými závorkami. [modifikátory] typNávratovéHodnoty jménoMetody ( [formálníParametry] ) [throws výjimky] { [deklarace_proměnných;] [příkazy;] }
Hlavička (podpis) metody má několik částí: ♦ modifikátor přístupu, ♦ další (nepovinné) modifikátory, ♦ typ návratové hodnoty, ♦ jméno (identifikátor), ♦ kulaté závorky (povinné), které mohou obsahovat deklaraci formálních parametrů metody, ♦ vyhazované výjimky (nepovinná část) viz kapitola 12. Metody mohou mít uvedeny různé modifikátory přístupu, jež jsou podrobněji popsány v podkapitole „Modifikátory přístupu k datovým atributům a metodám“. V hlavičce metody mohou být i některé další modifikátory. Typ návratové hodnoty metody je povinný. Metoda může vracet libovolný typ platný v Javě (viz kapitola 3). Pokud žádnou hodnotu nevrací, musí být uvedeno vyhrazené slovo void. Formální parametry metody slouží k předání vstupních hodnot do metody. Každý parametr je v hlavičce metody deklarován podobně jako datové atributy typem a jménem, není možné (ani smysluplné) uvádět modifikátor přístupu (parametr platí pouze v metodě). V hlavičce metody není možné přiřadit parametru defaultní hodnotu. Lokální proměnná (pomocná proměnná metody) se v metodě používá pro uložení nějakého mezivýsledku a po ukončení činnosti metody je zrušena. Deklarace a inicializace pomocné proměnné se od inicializace datového atributu liší tímto: ♦ neuvádějí se modifikátory přístupu, ♦ proměnné nejsou implicitně inicializovány, první hodnotu musí nastavit programátor. Problematika rozsahu platnosti lokálních proměnných je uvedena v kapitole 4 v souvislosti se sekvencí (blokem) příkazů.
Objekty
strana 18
V těle metody, které je zapsáno mezi dvěma složenými závorkami, mohou být následující příkazy: ♦ volání metody, ♦ přiřazení, ♦ příkaz return, ♦ sekvence (posloupnost, blok příkazů), ♦ selekce (rozhodování, větvení), ♦ iterace (cyklus, opakování), ♦ příkaz skoku z cyklu, ♦ vyvolání a obsluha výjimek, ♦ prázdný příkaz, ♦ příkaz assert pro testování metody. Postupně si jednotlivé příkazy vysvětlíme v této i dalších kapitolách. Alespoň některé druhy metod si nyní ukážeme na našem jednoduchém příkladě s účty. Metoda bez parametrů s návratovou hodnotou Příkladem takovéto metody bude ve třídě Ucet metoda pro zjištění stavu účtu. Stav účtu je představován datovým atributem stav typu double, návratová hodnota metody tedy musí být také typu double. Pro vrácení hodnoty z metody má Java příkaz (klíčové slovo) return, uvedením tohoto příkazu se provádění příkazů v metodě ukončí a vrátí se hodnota za ním uvedená. V naší metodě tedy musí být tento příkaz uveden a musí vrátit hodnotu datového atributu stav. Metodu pojmenujeme trochu podivně getStav(). Důvodem pro toto pojmenování je další ze zvyklostí zavedených v Javě. Pokud metoda vrací hodnotu nějakého datového atributu, měla by se jmenovat getJmenoAtributu(), proto tedy getStav() Metoda bude mít modifikátor přístupu public, patří do veřejného rozhraní třídy Ucet. Na následujícím obrázku je znázorněno, jak bude vypadat kód této metody (deklarace metody getStav).
Obrázek 2.5 Popis metody s návratovou hodnotou Důležité je si uvědomit, že ačkoli metoda nemá parametry, kulaté závorky jsou v podpisu metody vždy uvedeny. Metoda s parametry bez návratové hodnoty Příkladem takovéto metody ve třídě Ucet je metoda pro vložení další částky na účet. V tomto případě potřebujeme metodě sdělit, kolik peněz na účet vkládáme. Tento údaj představuje vstupní parametr metody. Při deklaraci metody musíme uvést, jakého typu bude parametr – vzhledem k tomu, že stav je typu double, tak i vkládané částky budou tohoto typu. Metoda přidá uvedenou částku na účet a nevrátí žádnou hodnotu, v deklaraci tedy bude uveden návratový typ void. Metoda, která nevrací hodnotu, nemá v kódu zpravidla uveden příkaz return (pokud ano, nesmí za ním být uvedeno nic) a
Objekty
strana 19
končí provedením všech příkazů uvedených v metodě. Jak bude kód metody vypadat i s vysvětlivkami, vidíte na obrázku 2.6.
Obrázek 2.6 Popis metody s parametrem V hlavičce metody se může deklarovat více parametrů, jednotlivé deklarace jsou od sebe odděleny čárkou. Metoda s parametry a s návratovou hodnotou Jako příklad takovéto metody můžeme ve třídě Ucet uvést metodu pro výběr peněz. Požadovaná částka bude vstupním parametrem metody a bude typu double. Pokud bude na účtu dostatek peněz, metoda provede odpočet částky z účtu a vrátí true (výběr se povedl). Jestliže požadovaná částka bude vyšší než stav účtu, vrátí metoda hodnotu false (výběr se nepovedl) a částka odečtena nebude. Podrobnější vysvětlení příkazu if, který se v metodě použije, najdete v kapitole 4. Nyní si uvedeme, jak bude vypadat deklarace třídy Ucet včetně těchto tří metod. Povšimněte si také odsazování jednotlivých části kódu a zápisu složených závorek. Párování závorek je možné zapisovat dvěma způsoby. První byl uveden na obrázku 2.5 s kódem metody getStav(). Druhý je používán ve všech ostatních případech. Vyberte si ten způsob zápisu kódu, který se vám jeví jako čitelnější. public class Ucet { private int cisloUctu; private String jmenoVlastnika; private double castka = 0; public double getStav(){ return stav; } public void vloz (double castka){ stav = stav + castka; }
Objekty
}
strana 20
public boolean vyber (double castka){ if ((stav – castka) >= 0) { stav = stav – castka; return true; } else { return false; } } //zde budou další metody
2.6.4. Modifikátory přístupu k datovým atributům a metodám Následující modifikátory uvádějí možnost přístupu k datovému atributu nebo metodě: ♦ private, ♦ (nic neuvedeno), ♦ protected, ♦ public. Označíme-li nějaký datový atribut nebo metodu jako private, znamená to, že je přístupná pouze z metod instance. V příkladě s účty jsou takto označeny všechny datové atributy. Stav účtu si v metodě jiné třídy nemohu přečíst přímo z datového atributu, ale pomocí metody getStav(). Obdobně z metod jiných tříd mohu změnit stav účtu zase jen prostřednictvím metod. Jako druhý modifikátor vlastně není nic uvedeno, ale znamená to, že pokud neuvedeme žádný modifikátor přístupu, použije se přátelský přístup. Datové atributy a metody jsou v tomto případě přístupné v rámci balíčku (viz následující podkapitola). Datové atributy a metody, které mají označení přístupu protected, jsou přístupné v rámci balíčku a také z potomků v rámci dědičné hierarchie (viz kapitola 11). Modifikátor protected se použije u metod, které usnadní psaní potomků, ale které nemají význam pro běžné použití instance třídy (poté by měl být použit modifikátor public). Označení přístupu public znamená, že daný datový atribut či metoda jsou přístupné z jakékoli jiné třídy. Jak již bylo uvedeno, pokud deklarujeme samostatnou třídu, můžeme před klíčovým slovem class uvést pouze modifikátor public nebo žádný. Jejich význam je stejný jako v případě datových atributů a metod. Třídu označenou jako public „vidí“ všechny ostatní třídy, třídu bez modifikátoru public jen třídy ze stejného balíčku.
2.6.5. Konstruktor Jak již bylo v textu uvedeno, instance jsou vytvářeny pomocí speciálních metod, které označujeme jako konstruktory. V Javě platí pro konstruktory několik pravidel, která je odlišují od ostatních metod: ♦ konstruktor se vždy jmenuje jako třída (výjimka z pravidla o malých počátečních písmenech metod), ♦ konstruktor nemá uveden žádný návratový typ (ani void), ♦ pokud žádný konstruktor nenapíšete, vytvoří překladač prázdný veřejný konstruktor bez parametrů, ♦ konstruktory se na rozdíl od ostatních metod nedědí (viz kapitola 11 o dědičnosti). Konstruktor slouží k vytvoření instance a většinou také k inicializaci datových atributů. Ukážeme si, jak bude vypadat konstruktor naší třídy Ucet. V rámci vytváření instance vždy nastavíme číslo účtu a jméno vlastníka. Tyto hodnoty budou představovat vstupní parametry konstruktoru. V průběhu vytváření instance budou tyto hodnoty dosazeny do odpovídajících datových atributů. Kód konstruktoru bude vypadat takto:
Objekty
strana 21
public Ucet (int cislo, String vlastnik){ cisloUctu = cislo; jmenoVlastnika = vlastnik; }
Je tedy možné vytvořit novou instanci třídy Ucet s daným číslem a vlastníkem, stav tohoto účtu bude 0.00. Pokud bychom chtěli vytvořit účet s určitou počáteční hodnotou, která by byla u různých instancí různá, musíme napsat ještě jeden konstruktor. Tento konstruktor bude mít navíc ještě jeden parametr typu double a bude představovat počáteční vklad. Kód tohoto konstruktoru bude vypadat takto: public Ucet (int cislo, String vlastnik, double pocatecniVklad){ cisloUctu = cislo; jmenoVlastnika = vlastnik; stav = pocatecniVklad; }
Jak vidíte, jedna třída může mít libovolný počet konstruktorů, musejí se od sebe lišit počtem, pořadím nebo typem parametrů. Této vlastnosti se říká přetěžování (overloading). Přetížit lze i „obyčejné“ metody, v jedné třídě může být libovolný počet metod stejného jména, které se liší počtem, pořadím nebo typem parametrů. Metodu ale nelze přetížit pouze typem návratové hodnoty. Kód naší třídy Ucet rozšířený o konstruktory je uveden v následujícím výpise. public class Ucet { private int cisloUctu; private String jmenoVlastnika; private double stav = 0; public Ucet (int cislo, String vlastnik){ cisloUctu = cislo; jmenoVlastnika = vlastnik; } public Ucet (int cislo, String vlastnik, double pocatecniVklad){ cisloUctu = cislo; jmenoVlastnika = vlastnik; stav = pocatecniVklad; } public double getStav(){ return stav; } public void vloz (double castka){ stav = stav + castka ; }
}
public boolean vyber (double castka){ if ((stav – castka) >= 0) { stav = stav – castka; return true; } else { return false; } } //zde budou další metody
Objekty
strana 22
2.7. Vytváření instancí a volání metod v Javě Zatím jsme si popsali, jak vytvořit třídu tj. jak udělat obecný popis nějakých objektů z reality. Nyní ukážeme, jak v programu (aplikaci) vytvářet jednotlivé instance a volat jejich metody. V Javě bude tento kód opět umístěn v nějaké třídě a její metodě. Může to být např. třída Banka. Nebudeme zde však uvádět celý kód třídy, ale pouze jednotlivé řádky kódu, které by byly umístěny v metodách. První věc, kterou si musíme objasnit je, jak vytvořit instanci a uložit si na ni odkaz, abychom s ní mohli dále pracovat. Pokud chceme vytvořit instanci třídy Ucet, musí být identifikátor použitý na uložení odkazu typu Ucet, protože každá vytvořená třída je zároveň datovým typem. Identifikátor si pojmenujeme ucet1. Deklarace identifikátoru pro uložení odkazu na instanci třídy Ucet bude vypadat takto: Ucet ucet1;
Vytvoření nové instance proběhne pomocí volání konstruktoru. Volání konstruktoru v Javě je vždy spojeno s klíčovým slovem new. Který ze dvou vytvořených konstruktorů se použije, záleží na počtu skutečně zadaných parametrů. Vytvoření instance a přiřazení odkazu na ni do identifikátoru bude vypadat takto: ucet1 = new Ucet (1,”Pepa”);
Deklaraci a inicializaci je možné spojit dohromady do jednoho příkazu, viz obrázek 2.7.
Obrázek 2.7 Popis vytváření instance pomocí konstruktoru Když máme v identifikátoru uložený odkaz na vytvořenou instanci, můžeme volat metody této instance (můžeme ji posílat zprávy). Pokud metoda nevrací hodnotu, uvedeme při jejím volání pouze odkaz na instanci, tečku a název metody. Pokud má metoda deklarovány formální parametry, musíme při volání uvést skutečné hodnoty, se kterými bude metoda pracovat. Můžeme zadat konstantu nebo identifikátor proměnné, která hodnotu obsahuje nebo na ni odkazuje. Třetí možností je uvést za parametr jinou metodu, která vrací hodnotu příslušného typu. V případě, že metoda vrací hodnotu a my s ní chceme dále pracovat, musíme výsledek metody přiřadit do proměnné odpovídajícího typu. Na následujícím obrázku je uvedeno, jak zavolat metodu vloz() s hodnotou parametru 100 a jak získat informaci o stavu účtu pomocí metody getStav(). Výsledek metody getStav() musíme uložit do proměnné typu double, protože metoda vrací hodnotu tohoto typu.
Obrázek 2.8 Popis volání metod instance
Objekty
strana 23
2.7.1. Volání metod v rámci jedné instance (this) Zatím jsme si ukázali, jak volat metody jiné instance, občas však potřebujeme z jedné metody zavolat jinou metodu stejné instance. V tomto případě potřebujeme odkaz sami na sebe – pro odkaz sama na sebe je v Javě vyhrazeno klíčové slovo this. Následuje ukázka metody pro přičtení úroků, parametrem je procento úroku, které se má přičíst. public void prictiUrok (double procento) { this.vloz(this.getStav() * procento); }
V metodě prictiUrok() se volá metoda getStav() pro zjištění aktuálního stavu účtu a metoda vloz(), která vloží vypočtený úrok na účet. Klíčové slovo this je nepovinné, neboť překladač ho ve většině případů automaticky doplní. Metoda poté může vypadat následovně: public void prictiUrok (double procento) { vloz(getStav() * procento); }
This se používá i pro odkaz na datové atributy instance. Většinou se neuvádí, neboť obdobně jako u volání metod doplní překladač this. V situacích, kdy jméno datového atributu koliduje se jménem lokální proměnné či s identifikátorem parametru metody, se však this uvádět musí. Příkladem je odlišení datových atributů od parametrů metod. Pokud metoda nebo konstruktor přiřazuje hodnotu parametru k datovému atributu, většinou se pro datový atribut a parametr metody používá stejný identifikátor. Má to dvě výhody – kód metody je přehlednější a nemusí se vymýšlet dvojnásobek jmen identifikátorů. Klíčové slovo this se používá ještě při volání jednoho konstruktoru z druhého v rámci jedné třídy. Na druhý konstruktor se odkazujeme pomocí this (tj. ne jménem třídy), správný konstruktor se vybere dle počtu a typů parametrů. Omezením je, že volání druhého konstruktoru musí být prvním příkazem v konstruktoru. S využitím this by deklarace datových atributů a konstruktory třídy Ucet mohly vypadat následovně: public class Ucet { private int cislo; private String vlastnik; private double castka = 0; public Ucet (int cislo, String vlastnik){ this(cislo, vlastnik, 0); // volání druhého konstruktoru } public Ucet (int cislo, String vlastnik, double pocatecniVklad){ this.cislo = cislo; this.vlastnik = vlastnik; castka = pocatecniVklad; } // pokračování třídy Použití stejných jmen pro datové atributy a pro parametry metod je častým důvodem chyb. Pokud použijete stejné názvy, nezapomeňte na this u datových atributů.
Objekty
strana 24
2.8. Modifikátor final Modifikátor final je příznakem neměnnosti/konečnosti. Používá se ve více situacích: final v deklaraci třídy Třída s modifikátorem final je konečná, tj. nelze vytvářet potomky této třídy. Příkladem mohou být třídy String, Math či obalové třídy (Integer, Long, Boolean, atd.). final v deklaraci metody Metoda s modifikátorem final je konečná, tj. tuto metodu nelze překrývat v potomkovi. Modifikátor final se uvádí v případě, že chceme zajistit neměnnost metody v potomcích, tj. u metod, které mají jiný modifikátor přístupu než private. Metody s modifikátorem přístupu private nelze překrýt z principu modifikátoru. Modifikátor final nelze použít u konstruktoru. Pokud je metoda final, může být v potomkovi přetížena – v potomkovi může být metoda stejného jména, která se bude lišit počtem či typy parametrů. final u datového atributu Datovému atributu s modifikátorem final lze přiřadit hodnotu pouze jednou, další pokusy o přiřazení hodnoty končí chybou při překladu. Pokud je datový atribut referenčního typu, je možné pomocí metod měnit obsah instance, nelze však do atributu přiřadit jinou instanci. Datové atributy s modifikátorem final mají obvykle i modifikátor static – datový atribut je poté společný pro všechny instance třídy, je v paměti pouze jednou. Popis modifikátoru static je v kapitole 7. Následuje deklarace třídy Bod, která obsahuje konstantu referenčního typu. class Bod { private int x, y; final static Bod POCATEK = new Bod(0, 0); Bod (int x, int y) { this.x = x; this.y = y; } }
final u pomocné proměnné V deklaraci pomocné proměnné může být uveden jediný modifikátor – final. Význam má stejný jako u datového atributu – do proměnné lze přiřadit hodnotu pouze jednou, další pokusy o přiřazení hodnoty označí překladač za chybu. final u parametru metody Je to jediný modifikátor, který může být uveden v deklaraci parametru metody. Při překladu se kontroluje, že se v těle metody do parametru metody nic nepřiřazuje. Přiřazovat hodnotu k parametru metody nemá význam – jak jsme si řekli již dříve, tak při volání metody se hodnoty primitivních datových typů kopírují, v případě referenčních typů se kopíruje odkaz. Tj. pokud uvedeme modifikátor final u parametru metody, tak nás pouze překladač upozorní na nesmyslnost přiřazení hodnoty/odkazu do parametru metody.
2.9. Rušení objektů V Javě se programátor nemusí starat o rušení instancí (objektů) – JVM (Java Virtual Machine) obsahuje speciální proces Garbage Collector (GC), který pravidelně hledá instance, na které nevede žádný odkaz. Pokud nějakou takovou instanci nalezne (na obrázku 2.3 je takovou instancí „David Ztracený“), tak v prvním kroku zavolá metodu finalize(). Ve druhém kroku GC instanci vymaže z paměti.
Objekty
strana 25
Metodu finalize() má každá instance, neboť ji dědí ze třídy Object. Ve třídě Object je tato metoda prázdná a obvykle se ve třídách nepřekrývá (překrývání – viz kapitola 11 věnovaná dědičnosti). JVM nezaručuje, že se metoda finalize() opravdu provede – typicky při ukončení aplikace se již metoda finalize() nevolá. Tím, že se programátor nemusí starat o rušení objektů, je programování v Javě jednodušší a nedochází obvykle k chybám při práci s pamětí jako v C či v C++. Přesto by se měl programátor snažit dodržovat dvě pravidla: ♦ Omezovat vytváření objektů, čímž se omezí i náročnost jejich rušení (a aplikace má menší paměťové nároky). Omezování vytváření objektů by však nemělo být na úkor přehlednosti či znovupoužitelnosti kódu, ♦ pokud nějaký identifikátor odkazuje na již nepotřebnou instanci, měli bychom přiřadit tomuto identifikátoru hodnotu null (přiřazovacím příkazem promenna = null;). Tím se odkaz na instanci zruší a GC může instanci smazat z paměti (pokud na ni není odkaz odjinud).
2.10. Balíčky (package) Součástí distribuce Javy je několik tisíc již připravených tříd a rozhraní, které je třeba logicky uspořádat. Třídy jsou proto rozděleny do různých balíčků. Balíček (package) tvoří zároveň i základní jmenný prostor, v rámci kterého musí mít třída jednoznačné jméno. Ve více balíčcích mohou být různé třídy stejného jména. Jména balíčků vytvářejí stromovou (adresářovou) strukturu, část struktury balíčku standardu Javy je uvedena na obrázku 2.9. Programátoři v rámci rozsáhlejších aplikací vytvářejí další balíčky – na začátku souborů se zdrojovým kódem uvedou klauzuli package následovanou jménem balíčku. Struktuře balíčků musí odpovídat i adresářová struktura, ve které jsou uloženy zdrojové a přeložené třídy.
Obrázek 2.9 Část adresářové struktury balíčků API Javy 5.0 Existuje ještě tzv. nepojmenovaný balíček (noname package). V nepojmenovaném balíčku jsou všechny třídy, které nemají uvedenu klauzuli package a jsou uloženy ve stejném adresáři. Takto umístěné třídy budeme používat v těchto skriptech.
Objekty
strana 26
Na třídy z nepojmenovaného balíčku se odkazujeme pouze jménem. Obdobně třídy z balíčku java.lang lze v programu používat jen s jejich jménem např. String, Integer, Object... Třídy z ostatních balíčků lze vždy označovat plným jménem včetně balíčku, např. java.util.ArrayList. Variantou k zadání celého jména je použití klauzule import pro příslušnou třídu, poté již lze používat jméno bez jména balíčku: import java.util.ArrayList; . . . . . private ArrayList seznam;
V klauzuli import lze zadat místo jména konkrétní třídy hvězdičku. Poté se importují všechny třídy a rozhraní z příslušného balíčku (ale ne z podřízených balíčků). import java.util.*; . . . . . . private ArrayList seznam;
Při použití importu (a hlavně importů s hvězdičkou) může dojít k tomu, že se z různých balíčků importují dvě třídy stejného jména. V tomto případě je nutné dále používat celé jméno třídy.
2.11. Komentáře Rozlišují se dva typy komentářů: ♦ dokumentační, které se zahrnují do dokumentace vytvářené programem javadoc: /** .... */ ♦ implementační, které jsou víceřádkové (/* ....*/) a jednořádkové ( // ). Dokumentační komentáře musí být uvedeny před každou deklarací třídy, metody, rozhraní či datového atributu, které mají modifikátor přístupu public či protected. Začínají znaky /** a končí znaky */. V textu komentáře by měly být použity klíčová slova pro javadoc – viz tabulka 2.2. Text může též obsahovat formátovací znaky HTML. klíčové slovo
popis
@author
jméno autora třídy
@version
číselné označení verze třídy
@see
odkaz na jinou třídu/metodu či na nějaké URL
@param
jméno_parametru
@return @exception
popis parametru popis návratové hodnoty z metody
jméno_výjimky
popis výjimky, která může nastat v metodě
Tabulka 2.2 Základní značky pro javadoc Následuje příklad dokumentačních komentářů: /** * Tato třída slouží ke generování výstupu s různými způsoby * kódování českých znaků * * @author xabcd01 * @created 27. may 2001 */
Objekty
strana 27
public class Unicode { /** * seznam českých znaků v kódování Unicode v notaci Javy */ public static final String retezec = "\u00e1\u00c1\u00e9 ..."; /** * metoda main dle prvního parametru příkazové řádky vytiskne .* řetězec na standardní výstup v požadovaném kódování * * @param ARGS první parametr příkazové řádky obsahuje řetězec * specifikující požadované kodovani .* @see Supported Encoding v dokumentaci JDK */ public static void main(String ARGS[]) { ...
Pro generování dokumentace se používá program javadoc. Pokud chceme vygenerovat dokumentaci jedné třídy, můžeme zadat příkaz: javadoc Unicode.java
který vygeneruje dokumentaci ve formátu html do aktuálního adresáře. Při spouštění programu javadoc je možné uvést mnoho parametrů, nejčastěji používané jsou v následující tabulce: parametr
popis
-d adresář
výstupní adresář
-public -protected -package -private
tyto parametry určují na základě přístupnosti, které prvky se mají zahrnout do dokumentace
Tabulka 2.3 Nejpoužívanější přepínače pro javadoc Na příkazové řádce je nutné uvést buď seznam zdrojových souborů či jméno balíčku, které musí odpovídat jménu adresáře. Typické spuštění pro soubory v nepojmenovaném balíčku vypadá takto: javadoc –d doc –package *.java
Dokumentace se vygeneruje do podadresáře doc. Implementační komentáře slouží ke komentování implementace třídy a měly by se používat v případě, kdy slouží k lepšímu čtení či porozumění kódu. Neměly by duplikovat informace, které lze snadno vyčíst z vlastního kódu. Potřeba psát implementační komentáře je někdy příznakem nízké kvality návrhu kódu – v tomto případě je vhodnější přepsat kód. Následují příklady implementačních komentářů: /* * komentář, který je na více * řádcích */ /* jednořádkový komentář */ příkaz // komentář, který končí na konci řádku
Objekty
strana 28
Datové typy
strana 29
3. Datové typy Jak již bylo uvedeno, Java je přísně typový jazyk, proto je vždy nutno uvést datový typ datového atributu, formálního parametru metody, návratové hodnoty metody nebo pomocné proměnné v metodě. Datové typy v Javě lze rozdělit do těchto skupin: ♦ primitivní typy: * čísla (byte, short, int, long, float, double), * znaky (char), * logické hodnoty (boolean), ♦ referenční typy: * třídy (class), * výčtový typ (enum) – od verze Java 5, * rozhraní (interface), * pole (array). Výčet všech dostupných primitivních datových typů je uveden v tabulce 3.1. Není možné vytvářet vlastní primitivní typy. Referenční datové typy nelze výčtem vypsat, vytvořením nové třídy nebo rozhraní vzniká nový datový typ. Standardní distribuce Javy obsahuje několik tisíc tříd a rozhraní. Nejdříve si popíšeme primitivní datové typy a operace s nimi spojené. Následovat bude popis rozdílů mezi referenčními a primitivními typy a operací s referenčními typy. Na konci kapitoly uvedeme možnosti převodu mezi primitivními datovými typy a odpovídajícími třídami.
3.1. Primitivní datové typy Java používá pro celočíselné proměnné čtyři datové typy: long, int, short a byte, pro reálná čísla dva typy: double a float, pro znaky typ char a pro logické proměnné typ boolean. Velikost, jakou zabírají v paměti, a rozsah, který jsou schopny obsáhnout, vidíme v následující tabulce. název typu velikost (v bytech) rozsah
implicitní hodnota
long
8
−9 223 372 036 854 775 808 až +9 223 372 036 854 775 807
0
int
4
−2 147 483 648 až +2 147 483 647
0
short
2
−32 768 až +32 767
0
byte
1
−128 až +127
0
double
8
±1.797 693 134 862 315 70 E+308 0.0
float
4
±3.402 823 47 E+38
0.0
char
2
65 536 různých znaků
\u0000
boolean
1 bit
true nebo false
false
Tabulka 3.1 Přehled primitivních datových typů
3.1.1. Deklarace a inicializace proměnné primitivního typu Při deklaraci je třeba uvést identifikátor a datový typ, volitelně lze zadat i počáteční hodnotu. Počáteční hodnota může být uvedena nejen konstantou (hodnotou), ale i výrazem, např.:
Datové typy
strana 30
long delkaDne = 24 * 60 * 60;
Výraz, kterým se přiřazuje hodnota, se musí dát vyhodnotit v době překladu, popř. v okamžiku spuštění programu. Pokud programátor při deklaraci pomocné proměnné metody nepřiřadí počáteční hodnotu, bude obsah proměnné libovolný. Při deklaraci datového atributu přiřadí implicitní hodnotu kompilátor – implicitní hodnoty jednotlivých typů jsou v tabulce 3.1. Z důvodu dobrých programátorských návyků je vhodné explicitně uvádět počáteční hodnotu i u datových atributů. Není určeno, jak se mají překladače zachovat při přiřazení počáteční hodnoty mimo rozsah proměnné (např. byte b = 200), některé překladače přiřadí nesmyslnou hodnotu, některé ohlásí chybu. Z celočíselných typů doporučujeme používat pouze typy int a long, typy byte a short by se měly využívat pouze při předávání hodnot s okolím (s databází, s komponentou v jiném programovacím jazyku, atd.). Při matematických operacích (např. při sčítání) se proměnné těchto typů převádějí na int a poté zpátky. Obdobně u reálných čísel se preferuje typ double.
3.1.2. Konstanty Konstanty se používají nejen při deklaraci proměnných, ale i v rámci výrazů. Celá čísla se zapisují obdobným způsobem jako v běžném životě, např. 9876; zápis nesmí obsahovat mezery či jiné oddělovače (např. tisíců). Je-li prvním znakem nula, znamená to, že číslo zapisujeme v osmičkové (oktalové) soustavě, začíná-li zápis čísla prefixem 0x, jedná se o číslo v hexadecimální (šestnáctkové) soustavě. Následující tři proměnné obsahují stejnou počáteční hodnotu: int cisloDek = 10; int cisloOkt = 012; int cisloHex = 0xa;
Celočíselná konstanta je vždy typu int, pokud potřebujeme konstantu jiného celočíselného typu, je třeba uvést přetypování nebo literál (viz dále). Reálná čísla se zapisují buď s desetinnou tečkou (např. 0.25, 0.00002) nebo se používá tzv. semilogaritmický zápis, kdy číslo 1.54 * 106 se zapíše ve tvaru 1.54e6. Reálné konstanty jsou vždy typu double. Každý znak je v jazyce Java uložen v kódování Unicode ve dvou bytech3. Znaky se píší v jednoduchých uvozovkách, a to buď v defaultním kódu operačního systému (v českých Windows je to kódová stránka CP1250), kdy překladač sám zajistí převod na příslušný znak Unicode, nebo použijeme zápis \uXXXX, ve kterém místo XXXX uvedeme hexadecimální kód příslušného znaku. char cSHackem = ’č’; char nejakyZnak = ’\u12ab’;
Logické hodnoty – pro proměnné typu boolean jsou definovány dvě konstanty – true a false. Programátory se znalostí jazyka C upozorňujeme, že jako logické hodnoty nelze v Javě používat čísla.
3.1.3. Pojmenované konstanty V Javě lze vytvářet pojmenované konstanty primitivních datových typů – definují se stejně jako proměnné s počáteční hodnotou s tím, že se na začátku uvede klíčové slovo final: final long DELKA_DNE = 24 * 60 * 60;
Proměnnou definovanou s modifikátorem final nelze již dále změnit, hodnotu lze přiřadit pouze jednou (tj. jakýkoliv pokus přiřadit do proměnné DELKA_DNE novou hodnotu skončí chybou při překladu). Podle konvencí Javy se ve jménech konstant používají velká písmena a pro oddělení slov podtržítko.
3
Od verze 5 Java podporuje i rozšířené znaky Unicode 4.0 – znaky, které jsou mimo rozsah dvou bytů typu
char. Bližší informace o jejich používání je v dokumentaci Javy u firmy Sun.
Datové typy
strana 31
3.1.4. Přetypování U celočíselné konstanty zapsané v programu se předpokládá datový typ int, u reálných konstant datový typ double. Pokud chceme, aby byla konstanta jiného datového typu, je třeba zapsat ji s literálem. Literál pro long je L, pro float f a pro double d (lze používat malá i velká písmena, u typu long je vhodné používat jen velké L, aby nedošlo k záměně s jedničkou). Zapíšeme-li konstantu 100000L bude uložena jako long, zápis 10000f znamená, že číslo je uloženo jako reálné typu float. Pokud se hodnota proměnné kratšího typu ukládá do proměnné delšího typu, provede Java převod automaticky (neexistuje nebezpečí ztráty informace, byteÆshort Æint Ælong Æfloat Ædouble). Například: int cislo1 = 12; long cislo2; cislo2 = cislo1;
U převodu z kratšího typu na delší nemusí být vždy jasné, kdy se převod provádí. Ve výrazu long vysledek = 10000*10000;
se nejprve provede násobení konstant typu int a poté se výsledek typu int převede na typ long a uloží do proměnné vysledek4. Pokud potřebujeme opačné přiřazení, je třeba provést explicitní přetypování a oznámit tak překladači, že případná ztráta informace nevadí, např.: float desetinneCislo1; double desetinneCislo2 = 0.123456789; desetinneCislo1 = (float) desetinneCislo2;
Při operacích s číselnými proměnnými typu byte a short se tyto proměnné automaticky převádějí na typ int, tj. výsledek operace s dvěma těmito proměnnými je typu int. Při operaci s proměnnými/konstantami různě dlouhých typů je výsledek delšího typu, tj. výsledkem operace proměnných typů int a long je hodnota typu long, výsledkem operace typů int a double je proměnná typu double atd. Proměnná kratšího typu se převede na delší typ před provedením operace. Pokud trváme na uložení do kratšího typu, je nutné explicitně přetypovat výsledek operace: long cislo1 = 10000; int cislo2 = 100; int vysledek = (int) (cislo1 – cislo2);
3.1.5. Přetečení V situaci, kdy informace ukládaná do proměnné má větší hodnotu než je maximální hodnota typu, mluvíme o tzv. přetečení. Java neohlásí žádnou chybu, ale usekne přesahující část a program tak vyprodukuje špatný výsledek. Např. výsledkem operace 100000 * 100000 je číslo 1 410 065 408 typu int místo správného 10 000 000 000, neboť tento výsledek je mimo rozsah typu int. K získání správného výsledku je potřeba aspoň jeden z operátorů označit jako long, tj. operace by měla vypadat následovně: 100000L * 100000.
3.2. Výrazy a operátory 3.2.1. Operátor přiřazení (=), přiřazovací příkaz Operátor přiřazení = se v Javě používá pro přiřazování hodnot u primitivních datových typů. S použitím operátoru přiřazení se vytváří přiřazovací příkaz. Překladač kontroluje typovou správnost 4
V Javě se nejdříve vyhodnotí pravá strana operace přiřazení nezávisle na levé straně.
Datové typy
strana 32
přiřazení – na obou stranách operátoru musí být buď stejné typy, nebo typy musí být v souladu s pravidly přetypování. Pokud přiřadíme hodnotu z jedné proměnné primitivního datového typu do druhé, vznikne kopie této hodnoty. int cislo1 = 5; int cislo2 = cislo1;
Použití operátoru přiřazení pro referenční typy si popíšeme později.
3.2.2. Aritmetické operátory Se základními matematickými operátory jsme se už seznámili. Jejich přehled najdete v následující tabulce. aritmetický operátor význam +
součet
−
rozdíl
*
násobení
/
dělení (celočíselné i desetinné)
%
zbytek po celočíselném dělení
Tabulka 3.2 Přehled aritmetických operátorů U přetypování a přetečení jsme už hovořili o problémech, které mohou nastat při těchto operacích. S dalším problémem se setkáváme u dělení celých čísel. Pokud v programu použijeme tento výraz double vysledek = 9/2;
bude v proměnné vysledek hodnota 4. Výsledkem dělení dvou celých čísel je opět celé číslo bez ohledu na typ proměnné, do které výsledek ukládáme (tj. jedná se o celočíselné dělení). Proto je i zde třeba použít přetypování nebo literál u jednoho z operandů, aby se provedlo desetinné dělení, např.: double vysledek = 9d/2;
V případě proměnných je potřeba jeden z operandů přetypovat, např. pokud cislo1 a cislo2 jsou typu int, bude výraz vypadat následovně5: double vysledek = ((double) cislo1) / cislo2;
Java používá řadu zkrácených zápisů některých operací, jsou uvedeny v následující tabulce. operátor
příklad
význam operátoru
+=
x += y
x=x+y
−=
x −= y
x=x−y
/=
x /= y
x=x/y
*=
x *= y
x=x*y
%=
x %= y
x=x%y
5
Výraz lze zapsat i ve tvaru double vysledek = (double) cislo1 / cislo2, ale zde je nejasné, zda se přetypování týká prvního operandu či výsledku – závisí na prioritách přetypování a dělení. Zápis double vysledek = (double) (cislo1 / cislo2) je určitě chybně, neboť přetypovává až vlastní výsledek operace, která proběhne jako celočíselná.
Datové typy
strana 33
operátor
příklad
význam operátoru
++
x++ ++x
x=x+1
−−
x−− −−x
x=x−1
Tabulka 3.3 Význam složených aritmetických operátorů Pro často se vyskytující operace zvyšování nebo snižování hodnoty proměnné o jedničku lze v Javě použít i zkrácené zápisy x++, ++x, x--, --x. Obvykle se tyto výrazy používají jako samostatné příkazy, lze je však (bohužel6) použít i na pravé straně výrazu. Zde se rozlišuje, zda použijeme ++ jako předponu (prefixový tvar) nebo příponu (postfixový tvar). Pokud se operátor ++ zapíše jako předpona, nejprve se zvýší hodnota proměnné a pak se použije. Pokud je ++ zapsán jako přípona, pracuje se ve výrazu s původní hodnotou a teprve poté je zvýšena. Vysvětlíme si to v následujících příkladech. int puvodni = 10; int nova = puvodni++;
Proměnná puvodni je nyní rovna 11 a proměnná nova je rovna 10. int puvodni = 10; int nova = ++puvodni;
Proměnná puvodni je nyní rovna 11 a proměnná nova je rovna také 11. Pro operátor −− platí stejná pravidla. Stejné problémy jsou i při použití těchto operátorů na místě parametrů metod. Operátor + se používá též pro spojování řetězců – blíže bude popsáno v kapitole 5.
3.2.3. Relační operátory Relační operátory se používají pro porovnání hodnot dvou číselných proměnných (pokud nejsou stejného typu, tak se převedou na delší typ), proměnných typu char a boolean. Výsledkem je vždy hodnota typu boolean, tj. buď pravda (true) nebo nepravda (false). Obvykle se používají v příkazu if a v příkazech cyklu pro vytváření podmínky7. Přehled relačních operátorů je v následující tabulce (pozor, záleží na pořadí znaků v operátorech): relační operátor význam ==
rovná se
!=
nerovná se
<
menší než
>
větší než
<=
menší nebo rovno
>=
větší nebo rovno
Tabulka 3.4 Přehled relačních operátorů Použití operátorů == a != pro referenční typy si popíšeme později.
6
U výrazů se předpokládá, že se při výpočtu nemění hodnoty operátorů použitých na pravé straně výrazu – operátory ++ a −− toto pravidlo porušují, což může vést k obtížně odhalitelným chybám. 7 V textu se používá pojem podmínka místo správnějšího pojmu „výraz s výslednou hodnotou typu boolean“. Pojem podmínka je sice méně přesný, avšak často lépe pochopitelný.
Datové typy
strana 34
3.2.4. Logické operátory Logické operátory slouží pro vyjádření vztahu mezi dvěmi proměnnými/výrazy typu boolean, tj. obvykle k vytváření složených podmínek. Logické operátory používané v Javě jsou uvedeny v tabulce 3.5. logický operátor
význam
&
logická spojka AND, vyhodnocují se oba operandy
|
logická spojka OR, vyhodnocují se oba operandy
&&
podmíněná logická spojka AND, pravý operátor se vyhodnocuje, pouze pokud je levý true
||
podmíněná logická spojka OR, pravý operand se vyhodnocuje, pouze pokud je levý false
!
negace NOT
Tabulka 3.5 Přehled logických operátorů Chceme-li například otestovat, jestli je proměnná i větší nebo rovna 10 a současně menší než 50, zapíšeme podmínku takto: (i >= 10) && (i < 50)
Výsledkem je hodnota true nebo false.
3.2.5. Bitové operátory Java umožňuje pracovat i s jednotlivými bity celočíselných hodnot. Vzhledem k jejich výjimečnému použití zde nejsou popsány, v případě potřeby je nastudujte z on-line dokumentace.
3.2.6. Podmíněný výraz V Javě existuje ternární8 operátor, který slouží k vytvoření podmíněného výrazu, syntaxe je: podmínka ? výraz1 : výraz2
Můžeme zapsat např. následující podmíněný příkaz: cislo1 = cislo1 < 5 ? cislo1 + 1 : 0;
Pokud je hodnota proměnné cislo1 menší než 5 (podmínka je splněna), zvýší se její hodnota o 1. Pokud je hodnota proměnné cislo1 větší nebo rovna 5 (tedy podmínka není splněna), přiřadí se do této proměnné hodnota 0. Zápis podmíněného výrazu je málo přehledný a proto se dává přednost podmíněnému příkazu if (viz kapitola 4).
3.2.7. Kulaté závorky Kulaté závorky ( ) se v Javě používají na následujících místech: ♦ ve složitějších výrazech pro vyjádření priority operací, ♦ jako operátor přetypování, ♦ v deklaraci metod pro uzavření seznamu formálních parametrů, ♦ při volání metod pro uvedení seznamu skutečných parametrů, ♦ v příkazech selekce a iterace pro uvedení podmínky, ♦ při odchytávání výjimek. 8
Pojem ternární znamená, že operátor používá tři operandy. Obvykle se používají dva operandy (např. při sčítání) či jeden operand (např. logická negace či operátor ++) – používá se označení binární a unární operátory.
Datové typy
strana 35
3.3. Referenční datové typy 3.3.1. Rozdíl mezi primitivními a referenčními datovými typy Jak již bylo uvedeno v úvodní kapitole, na každou instanci, kterou budeme v našem programu využívat, si musíme uložit odkaz (referenci) do identifikátoru odpovídajícího typu. Samotný obsah jednotlivých datových atributů je poté dostupný přes tuto referenci. U primitivních datových typů se do identifikátoru neukládá odkaz, ale přímo hodnota. Zjednodušeně je to znázorněno na obrázku 3.1. Rozdíl mezi hodnotou a odkazem se projevuje v přiřazovacím příkazu či v předávání parametrů metodám – viz přiřazovací příkaz dále. Na jednu instanci referenčního typu může odkazovat více identifikátorů. U primitivních typů může jednu hodnotu obsahovat právě jeden identifikátor, druhý identifikátor může odkazovat pouze na kopii hodnoty. Identifikátor referenčního typu nemusí odkazovat na žádnou instanci, identifikátor primitivního typu vždy obsahuje nějakou hodnotu, byť někdy může být náhodná (deklarace lokální proměnné).
Obrázek 3.1 Znázornění rozdílu mezi referenčními a primitivními datovými typy
3.3.2. Konstanta null S referenčními typy souvisí speciální konstanta null, která popisuje situaci, kdy identifikátor referenčního typu neodkazuje na žádnou instanci. Konstantu null lze použít v přiřazovacích příkazech či při porovnávání identifikátorů referenčního typu. Datové atributy referenčního typu nemají při deklaraci přiřazenu žádnou instanci – obsahují konstantu null (pokud není součástí deklarace i inicializace). Pokud identifikátor obsahuje hodnotu null a zavoláme nějakou metodu, vznikne výjimka NullPointerException (výjimky jsou popsány v kapitole 12). Příklad vzniku takovéto výjimky následuje: String retezec; retezec.toUpperCase(); Student student = null; student.getSemestr();
// vznikne NullPointerException // vznikne NullPointerException
3.3.3. Přetypování referenčních typů Podobně jako primitivní typy lze přetypovávat referenční typy. Automatické přetypování probíhá směrem k předkovi v dědičné hierarchii, pokud se má přetypovávat v opačném směru, musí být
Datové typy
strana 36
explicitně uvedeno. Při přetypování referenčních typů se nikdy nemění vlastní instance, což je rozdíl od primitivních číselných typů, u kterých může dojít ke ztrátě číslic. Přetypovávat lze též na implementovaná rozhraní. String retezec = "řetězec"; Object o = retezec; String retezec2 = (String)o;
// automatické přetypování // explicitní přetypování
O přetypování referenčních typů bude uvedeno více podrobností v kapitole 11 věnované dědičnosti.
3.3.4. Operátor přiřazení (přiřazovací příkaz) Operátorem přiřazení = se u referenčních typů přiřazují (kopírují) odkazy (reference). Při přiřazení referenčního typu vznikne kopie odkazu, instance zůstane v paměti pouze jednou. Překladač kontroluje typovou správnost přiřazení. Kopie instancí lze vytvářet pomocí metody clone() při splnění dalších podmínek. Výsledek následujícího kódu je zobrazen na obrázku 3.2. String text = "ahoj"; int cislo1 = 5; int cislo2 = cislo1; String text2 = text;
Obrázek 3.2 Znázornění rozdílu mezi přiřazováním primitivních a referenčních typů Pro parametry metod platí stejná pravidla jako pro přiřazovací příkaz, neboť při volání metody se přiřadí (zkopíruje) skutečný parametr do formálního parametru. U primitivních typů se zkopíruje hodnota, u referenčních typů se zkopíruje odkaz. Zevnitř metody tudíž nelze změnit obsah původní primitivní proměnné, která byla použita jako parametr při volání metody. Nelze ani změnit odkaz na instanci, na kterou odkazuje identifikátor použitý při volání metody. U referenčních typů je však možné posílat zprávy příslušné instanci (volat metody instance). Tím se může změnit obsah datových atributů v instanci, na kterou ukazuje původní odkaz i kopie odkazu v parametru.
3.3.5. Relační operátory == a != Relační operátory == a != je možné použít u referenčních datových typů pro porovnání odkazů. Lze pomocí nich zjistit, zda dva identifikátory ukazují na stejnou instanci, popř. zda identifikátor obsahuje hodnotu null. Tyto operátory nemohou sloužit pro logické porovnání (porovnání obsahu) dvou instancí (např. zda dva řetězce obsahují stejný text). Pro porovnání obsahu slouží v Javě metoda equals(), viz kapitola 6. V následujícím příkladu jsou použity instance třídy Integer, která bude popsána dále v kapitole.
Datové typy
strana 37
Integer cislo1 = new Integer(10); // vytvoření instance Integer cislo2 = cislo1; // zkopírování odkazu na instanci Integer cislo3 = new Integer(10); // vytvoření druhé instance System.out.print(cislo1 == cislo2); //výsledek je true System.out.print(cislo1 == cislo3); //výsledek je false System.out.print(cislo1.equals(cislo3)); //výsledek je true
3.3.6. Relační operátor instanceof Relační operátor instanceof se používá pro zjištění, zda lze instanci přetypovat na nějakou třídu či na nějaké rozhraní. Formální zápis vypadá následovně: identifikátor instanceof referenčníTyp
Operátor vrátí true, pokud je možné přetypovat na uvedený referenční typ instanci, na kterou odkazuje uvedený identifikátor. Operátor instanceof je použit např. v metodě equals() ve třídě Mistnost v projektu Adventura (v příkladu je vidět i přetypování referenčního typu): public boolean equals (Object o) { if (o instanceof Mistnost) { Mistnost druha = (Mistnost)o; return nazev.equals(druha.nazev); } else { return false; } }
3.3.7. Volání metod Volání metod je základní způsob provádění operací s referenčními typy – metody jsme si popsali již v kapitole 2.7.
3.4. Obalové třídy pro primitivní typy Ke každému primitivnímu datovému typu v Javě existuje třída zapouzdřující tento typ na referenční typ. Tyto třídy se označují jako obalové typy – představují obal (box) kolem primitivní hodnoty. V tabulce 3.6 jsou uvedeny obalové třídy pro jednotlivé primitivní typy Obalové třídy mají následující význam: ♦ obsahují další metody pro práci s těmito typy (např. převod řetězce na číslo), ♦ obalové třídy číselných typů mají definovány konstanty pro určení maximální a minimální hodnoty daného typu např. Integer.MAX_VALUE, ♦ jsou v nich definovány další konstanty spojené s primitivními typy jako Double.NEGATIV_INFINITY, Boolean.TRUE, Double.NaN, ♦ v některých situacích nelze použít primitivní typ (např. při ukládání do seznamu), poté se použijí instance obalových tříd.
Datové typy
strana 38
primitivní datový typ obalová třída byte
Byte
short
Short
int
Integer
long
Long
float
Float
double
Double
char
Character
boolean
Boolean
Tabulka 3.6 Přehled obalových tříd a jejich přiřazení k primitivním datovým typům S hodnotami v obalovém typu (stejně jako u všech tříd) lze pracovat pouze pomocí metod. Dalším rysem těchto tříd je to, že jsou read only9 – hodnotu, kterou obalují, již není možné změnit (lze samozřejmě vytvořit novou instanci s novou hodnotu). V Javě 5.0 probíhají potřebné převody mezi primitivním datovým typem a odpovídající obalovou třídou automaticky (autoboxing), takže se může zdát, že mezi primitivním typem a jeho obalovou třídou není rozdíl. Lze např. napsat následující kód. Integer cislo = 5; cislo += 2;
Ve skutečnosti je však na prvním řádku vytvořena instance třídy Integer obalující hodnotu 5. Pro výpočet na dalším řádku je proveden převod na primitivní datový typ int, provedena operace sčítání a vytvořena nová instance třídy Integer obalující hodnotu 7. Ve starších verzích Javy musel tyto převody napsat programátor následovně: Integer cislo = new Integer (5); int pomocna = cislo.intValue(); // převod na primitivní typ pomocna += 2; cislo = new Integer(pomocna);
Je třeba si uvědomit, že takto napsaný kód je také mnohem pomalejší než kdybychom pro proměnnou cislo použili typ int. Pokud se pro uložení číselných hodnot použijí primitivní datové typy, jsou prováděné operace (matematické, porovnávání) 5x až 50x rychlejší. Doporučení: – dávejte přednost primitivním datovým typům, – pokud je aplikace (část aplikace) pomalá, je vhodné zkusit optimalizovat obalové typy a jejich automatické konverze do primitivních datových typů.
Překladač se při automatických konverzích snaží optimalizovat kód – někdy to vede k nečekaným výsledkům. Např. při deklaraci dvou proměnných Integer cislo1 = 5; Integer cislo2 = 5;
vrací porovnání (cislo1 == cislo2) hodnotu true (porovnávají se hodnoty), při následující deklaraci však vrací hodnotu false (porovnávají se odkazy): Integer cislo1 = new Integer(5); Integer cislo2 = 5;
9
Read-only třída je taková, která neposkytuje žádné metody, pomocí kterých by bylo možné měnit hodnoty datových atributů.
Datové typy
strana 39
Využití obalové třídy pro konverzi řetězce na číslo si ukážeme na příkladě. Z grafiky dostaneme údaj od uživatele jako String (má to být např. počet kusů). Bude tedy třeba provést převod na číslo typu int. Kód využívající metodu parseInt() třídy Integer bude vypadat následovně: String text = vstupniPole.getText(); int cislo = Integer.parseInt(text);
V této ukázce chybí ošetření chybného vstupu (např. uživatel zadá místo čísla písmena) – tento postup si ukážeme v kapitole 12 o výjimkách.
Datové typy
strana 40
Základní konstrukce metod
strana 41
4. Základní konstrukce metod V těle metody se mohou vyskytnout následující typy příkazů: ♦ volání metody, ♦ přiřazení, ♦ příkaz return, ♦ sekvence (posloupnost, blok příkazů), ♦ selekce (rozhodování, větvení), ♦ iterace (cyklus, opakování), včetně příkazů skoku z cyklu, ♦ prázdný příkaz, ♦ vyvolání a obsluha výjimek, ♦ příkaz assert pro testování. Tři první uvedené typy příkazů jsme objasnili v kapitole 2. V této kapitole popíšeme další čtyři příkazy, výjimkami se budeme zabývat v kapitole 12, příkazu assert se v těchto skriptech nebudeme věnovat.
4.1. Sekvence Posloupnost příkazů v Javě zapíšeme tak, že jednotlivé příkazy a deklarace oddělíme středníkem. Ucet mujUcet = new Ucet (1,"Jarmila",10000); mujUcet.vloz(100) ; double stavMehoUctu = mujUcet.getStav() ;
Příkazy lze napsat i takto: Ucet mujUcet = new Ucet (1, "Jarmila", 10000); mujUcet.vloz(100) ;double stavMehoUctu = mujUcet.getStav();
ale vzhledem k nepřehlednosti doporučujeme zapisovat na každý řádek jeden příkaz či deklaraci. Řadu po sobě jdoucích příkazů a deklarací můžeme (a v mnoha případech musíme) spojit do bloku pomocí složených závorek. {
}
Ucet mujUcet = new Ucet (1, "Jarmila", 10000); mujUcet.vloz(100) ; double stavMehoUctu = mujUcet.getStav() ;
Po složené závorce už neuvádíme středník.
4.1.1. Rozsah platnosti lokálních proměnných Každá dvojice složených závorek v metodě představuje blok, který může obsahovat deklaraci lokálních proměnných a příkazy (sekvenci příkazů). S tím souvisí i rozsah platnosti lokální proměnné, tj. kde se lze odkazovat na lokální proměnnou. Pro rozsah platnosti platí dvě základní pravidla: ♦ lokální proměnnou lze používat od místa, kde se deklaruje, až po uzavírací závorku bloku, ve kterém je deklarována, ♦ pokud je proměnná či formální parametr deklarovaný v kulatých závorkách těsně před začátkem bloku (před složenými závorkami), tak je lze používat v rámci celého bloku. Pravidla si ukážeme na následujícím kódu (příkazy if a for jsou vysvětleny dál v této kapitole):
Základní konstrukce metod
strana 42
1 public void metoda(int cislo) { 2 int promenna1 = 0; 3 for (int i = 0; i < 10; i++) { 4 // nějaké příkazy 5 } 6 int promenna2 = 0; 7 if (promenna1 > 0) { 8 int promenna3 = promenna2; 9 // další příkazy 10 } 11 // další příkazy 12 }
Formální parametr cislo se může používat v celé metodě. Lokální proměnná promenna1 se může používat v celé metodě, neboť je deklarována na prvním řádku a blok končí na posledním řádku metody. Proměnná i (řádek 3) se může používat až po konec bloku příkazu for na řádku 5. Proměnná promenna2 se může používat od své deklarace až po konec metody. Proměnná promenna3 se může používat od místa své deklarace po konec bloku if na řádku 10. Datové atributy a metody mají rozsah platnosti po celé třídě. Na rozdíl od lokálních proměnných je lze nejdříve používat a poté deklarovat.
4.2. Selekce Pro větvení má Java dva příkazy: if a switch.
4.2.1. Příkaz if Příkaz if má podmínku a jednu nebo dvě větve. Výsledkem podmínky musí být hodnota typu boolean. Podmínka je vždy uvedena v kulatých závorkách. Syntaxe příkazu if s jednou větví je následující: if (podmínka) { příkaz; }
Příkaz se provede pouze v případě, že podmínka platí, tj. byla vyhodnocena jako true. Pokud podmínka byla vyhodnocena jako false, neprovede se nic, příkaz if skončí a kód pokračuje dalším příkazem. Syntaxe příkazu if s větví else: if (podmínka) { příkaz1; } else { příkaz2; }
Pokud je výsledek vyhodnocení podmínky hodnota true, provede se příkaz1, pokud je výsledkem false, provede se příkaz2. Následující diagramy zobrazují obě varianty příkazu if – bez větve else a s větví else.
Základní konstrukce metod
strana 43
Obrázek 4.1 Diagram průběhu příkazu if s jednou a dvěma větvemi Každá větev příkazu if může obsahovat libovolný počet příkazů (jsou seskupeny ve složených závorkách). Jako příklad použití příkazu if si můžeme znovu uvést kód metody pro výběr z účtu z kapitoly 0. public boolean vyber (double castka){ if ((stav – castka) >= 0) { stav = stav – castka; return true; } else { return false; } }
Při zápisu příkazu if do kódu je vhodné dodržovat následující pravidla: ♦ větev if i else vždy uvádět se složenými závorkami, a to i v případě, že větev obsahuje jen jeden příkaz, ♦ odsazovat kód a párovat závorky kvůli přehlednosti.
4.2.2. Příkaz switch Méně používaný příkaz switch na základě hodnoty výrazu provádí příslušnou větev příkazu (větví obvykle bývá několik). Výraz musí být typu char, byte, short nebo int nebo výčtový typ. Syntaxe je následující: switch (výraz) { case konstanta1: příkazy1; [break;] case konstanta2: příkazy2; [break;] case konstanta3: příkazy3; [break;] case konstanta4: příkazy4; [break;] // větví může být libovolný počet default: příkazy; }
V bloku může být umístěno několik větví začínajících klíčovým slovem case, konstantou a dvojtečkou (v Javě nelze použít např. podmínku i > 5). Za dvojtečkou může být uvedeno více příkazů, obvykle se uvádí dva – vlastní příkaz (např. volání nějaké metody) a příkaz break. Jako poslední se obvykle uvádí větev uvozena slovem default následovaným dvojtečkou. Při provádění příkazu switch se
Základní konstrukce metod
strana 44
nejprve vyhodnotí výraz a poté se provedou příkazy ve větvi case s odpovídající hodnotou. Pokud se odpovídající hodnota nenajde, provádí se větev default. Pokud není větev case ukončena příkazem break, pokračuje se v provádění příkazů na následující větvi i když je u nich uvedena jiná hodnota než jakou má výraz (tj. záleží na pořadí uvedení větví!). Diagram na obrázku 4.2 zobrazuje průběh zpracování příkazu switch ve dvou variantách – s break na konci příkazu a bez break. Podobný význam jako break mají ve větvi case příkazy return (ukončí se celá metoda) a throw (vznikne výjimka a přejde se na zpracování výjimky obvykle v jiné metodě).
výraz == konstanta1
true
příkazy1
break
výraz == konstanta1
false výraz == konstanta2
true
true
příkazy2
break
výraz == konstanta2
true
true
příkazy2
false příkazy3
break
výraz == konstanta3
false
výraz == konstantaN
příkazy1
false
false výraz == konstanta3
true
true
příkazy3
false
příkazyN
false default příkazy
break
výraz == konstantaN
true
příkazyN
false default příkazy
Obrázek 4.2 Diagram variant průběhu příkazu switch s break na konci větve a bez break Následující metoda převádí řetězec na malá písmena a „maže“ diakritická znaménka u českých samohlásek (je to součást řešení domácího úkolu z projektu HadaniSlov). private String upravitSlovo(String slovo) { char [] znaky = slovo.toLowerCase().toCharArray(); for (int i = 0; i < znaky.length; i++) { switch (znaky[i]) { case 'á': znaky[i]='a'; break; case 'ä': znaky[i]='a'; break; case 'é': znaky[i]='e'; break; case 'ě': znaky[i]='e'; break; case 'ë': znaky[i]='e'; break; case 'í': znaky[i]='i'; break; case 'ó': znaky[i]='o'; break; case 'ö': znaky[i]='o'; break; case 'ú': znaky[i]='u'; break; case 'ů': znaky[i]='u'; break;
Základní konstrukce metod
strana 45
case 'ü': znaky[i]='u'; break; case 'ý': znaky[i]='y'; break; default:
}
} } return new String(znaky);
V verzi 5.0 byl příkaz switch rozšířen o podporu výčtového typu (enum) – popis bude v kapitole 8 věnované tomuto typu.
4.3. Iterace (cykly) V Javě jsou definovány tři druhy cyklů – příkazy while, do-while a for. S nimi souvisí i dva příkazy pro skok z cyklu – break a continue.
4.3.1. Příkaz while Nejčastěji se používá cyklus while s následující syntaxí: while ( podmínka ){ příkaz; }
vyhodnocení podmínky false true provedení příkazu (těla cyklu)
Obrázek 4.3 Diagram průběhu příkazu while Provádění příkazu začíná vždy testem podmínky. Pokud je podmínka splněna (výsledkem je hodnota true), provede se příkaz a znovu se přejde na test podmínky. Při nesplnění podmínky provádění cyklu končí. Pokud podmínka není na začátku splněna, neprovede se příkaz v cyklu while ani jednou. Cyklus while se nejčastěji používá v situaci, kdy předem neznáme počet opakování. V následující ukázce se generují náhodná celá čísla v intervalu <0;10) do té doby, než se vygeneruje číslo 0. Pro generování náhodných čísel se používá třída Random z balíčku java.util. java.util.Random generator = new java.util.Random(); int nahoda = generator.nextInt(10); while ( nahoda != 0 ) { System.out.println(nahoda); nahoda = generator.nextInt(10); }
Základní konstrukce metod
strana 46
Cyklus while lze použít v situaci, kdy známe počet opakování – většinou se však dává v této situaci přednost příkazu for. Následující cyklus proběhne 10x. int i = 1; while ( i <= 10 ) { // příkazy i++; }
Pomocí while lze nekonečný cyklus zapsat následovně10: while (true) { // příkazy }
Nekonečný cyklus lze ukončit pomocí příkazů break (ukončení cyklu), return (ukončení celé metody), throw (vyvolání výjimky) či voláním některých metod (ukončení aplikace, ukončení vlákna atd.). Nekonečné cykly používejte pouze v odůvodněných případech – pokud má cyklus skončit po splnění nějaké podmínky, je vždy přehlednější tuto podmínku uvést hned ve while.
4.3.2. Příkaz do-while Nejméně používaným typem cyklu je cyklus do-while s následující syntaxí: do příkaz while ( podmínka );
provedení příkazu (těla cyklu) vyhodnocení podmínky true
false
Obrázek 4.4 Diagram průběhu cyklu do-while Tento cyklus začíná provedením příkazu a až poté se testuje podmínka. Dle výsledku se provádí znovu příkaz (hodnota true podmínky) nebo se končí (hodnota false). V cyklu do-while se příkaz provede vždy alespoň jednou.
4.3.3. Příkaz for Posledním typem cyklu je cyklus for, který má od verze 5.0 dvě varianty – klasickou variantu s inicializací, podmínkou a krokem a „for each“ variantu, která se používá pro procházení datových struktur. Zde si popíšeme první variantu, druhá varianta cyklu for bude popsána v kapitole věnované datovým strukturám (kapitola 11). Klasický cyklus for používáme tam, kde známe počet iterací. Syntaxe je následující: 10
Toto je jeden z mála případů, kdy se v praxi používají booleovské konstanty true či false.
Základní konstrukce metod
strana 47
for (inicializace; podmínka; krok) { příkazy; }
Zpracování klasického cyklu for je zobrazeno na následujícím diagramu:
inicializace vyhodnocení podmínky false krok
true
provedení příkazu (těla cyklu) Obrázek 4.5 Diagram průběhu příkazu for Inicializací určujeme řídící proměnnou cyklu a její vstupní hodnotu. Pokud je splněna podmínka, provede se příkaz následovaný operací s řídící proměnnou cyklu uvedenou v části krok. Poté se opět přejde na vyhodnocení podmínky. V následujícím příkladě se zopakuje obsah cyklu 10x (příklad odpovídá příkladu u cyklu while): for (int i = 1; i <= 10; i++) { // příkazy }
Následující cyklus se provede 50x s tím, že řídící proměnná cyklu bude nabývat pouze sudých hodnot: for (int i = 2; i <= 100; i += 2) { // příkazy }
Příkaz for lze přepsat do příkazu while následujícím způsobem: inicializace; while (podmínka) { příkaz; krok; } V hlavičce cyklu for lze vynechat jednotlivé části a používat cyklus for podobně jako cyklus while. Toto však vede k nepřehlednému kódu a doporučujeme se tomuto použití vyhýbat.
4.3.4. Příkazy break a continue Jazyk Java zná příkazy break a continue, které ovlivňují průběh zpracování příkazů v rámci cyklu (příkaz break se používá i v příkazu switch). Jestliže v těle cyklu použijeme break, výsledkem bude skok za konec tohoto cyklu.
Základní konstrukce metod
strana 48
for (int i = 0; i <= 5; i++) { if (i == 3) break; System.out.println(i); } System.out.println("Konec programu");
Tato část programu bude mít tento výstup: 0 1 2 Konec programu
Příkaz continue způsobí, že je přeskočen zbytek těla cyklu a znovu se testuje podmínka cyklu (v případě cyklu for se provede ještě krok řídící proměnné cyklu). Pokud ve výše uvedeném příkladu použijeme místo break příkaz continue, bude výstup vypadat takto: 0 1 2 4 5 Konec programu
Za příkazy Break i continue lze uvést návěstí, na které tyto příkazy skočí – tj. lze opustit i několik do sebe vnořených cyklů. Návěstí ukončené dvojtečkou se uvádí v programu před začátkem vnějšího cyklu.
4.4. Prázdný příkaz Prázdný příkaz, jak již název napovídá, nic nedělá. Prázdný příkaz vznikne po uvedení samostatného středníku. Výjimečně se použije úmyslně, často je použit neúmyslně. Občas se úmyslně používá v příkazu if. Je to situace, kdy by negativní podmínka byla nepřehledná či se obtížně vytváří: if (složitá_podmínka) { ; } else { příkazy; }
Neopatrné umístění prázdného příkazu někdy může mít nepříjemné důsledky. Následující příklad skončí chybou při překladu, neboť větev else je od příkazu if oddělena prázdným příkazem (přebývajícím středníkem za ukončující složenou závorkou): if (i > 0) { // příkazy }; else { // příkazy }
V následujícím příkladu se programátor snaží vypsat čísla od 1 do 10: int i=1; while (i <= 10); { System.out.println(i); i++; }
Program však skončí nekonečným cyklem (tj. nikdy neskončí), neboť za podmínkou while je prázdný příkaz – přebývající středník mezi podmínkou a otevírací složenou závorkou.
Řetězce (třída String)
strana 49
5. Řetězce (třída String) Pro práci s řetězci (tj. s posloupností znaků) se v jazyce Java používá třída String. Třída String slouží k ukládání konstantních řetězců, jejichž hodnota se během činnosti programu nezmění (jedná se o příklad read-only třídy) – toto však neznamená, že stejný identifikátor nemůže v průběhu programu ukazovat na různé hodnoty (různé instance). Instanci třídy String lze vytvořit třemi způsoby: ♦ explicitně následující definicí (není vhodné tento způsob vůbec používat, neboť druhá varianta je kratší, rychlejší a přehlednější): String text = new String("abcd"); ♦ implicitně, kdy překladač automaticky doplní potřebný kód pro vytvoření instance typu String: String text = "abcd"; ♦ implicitně, kdy se nedefinuje ani identifikátor (odpovídá konstantě primitivních datových typů): "abcd"; Pro práci s řetězci jsou definovány operátory + a += pro spojení dvou řetězců (instancí třídy String) do nové instance třídy String (do nového řetězce)11. Při použití operátoru += (např. retezec1 += retezec2) se odkaz na novou instanci přiřadí k identifikátoru uvedenému na levé straně výrazu. Spojování řetězců si ukážeme v následujícím příkladu: int cislo = 10; String retezec1 = "Výsledek je"; System.out.println(retezec1 + " " + cislo + " korun");
Příklad vypíše následující řádek: Výsledek je 10 korun
Pokud se k řetězci připojuje operátorem + proměnná primitivní typu (int, long a další), překladač automaticky zajistí její převod na typ String (viz předchozí příklad s metodou println()). Pokud se k řetězci připojuje instance objektu, překladač automaticky doplní volání metody toString(), která převede objekt na řetězec. Pozor na posloupnost operací viz následující příklady: System.out.println ("Výsledek je " + 5 + 7);
Tento řádek vypíše následující text: Výsledek je 57
Pokud chceme čísla sečíst, musíme sčítání uzavřít do kulatých závorek. System.out.println ("Výsledek je " + (5 + 7));
A tento řádek kódu vypíše toto: Výsledek je 12
5.1. Porovnávání řetězců Při práci s řetězci v programu je třeba si uvědomit, že řetězce nejsou primitivní datový typ, ale instance třídy String, tj. referenční datové typy. Pro porovnání obsahu dvou řetězců se používá metoda equals(). Syntaxe porovnání řetězců retezec1 a retezec2 je následující: retezec1.equals(retezec2)
a výsledek je typu boolean. Od verze 1.4 Java optimalizuje ukládání řetězců v paměti – cílem je ukládat stejné řetězce v paměti pouze jednou. Při následující deklaraci se v paměti vytvoří pouze jedna instance, na kterou budou odkazovat oba identifikátory. 11
Jedná se o jediné případy přetížení (overloading) operátorů v Javě.
Řetězce (třída String)
strana 50
String retezec1 = "textik"; String retezec2 = "textik";
Proto i při porovnání pomocí operátoru == je výsledkem hodnota true, i když to na první pohled odporuje tomu, co jsme si říkali o relačních operátorech pro referenční typy (při porovnání dvou proměnných referenčního typu pomocí operátoru == je výsledkem hodnota true, pokud obě proměnné odkazují na stejnou instanci). Optimalizace ukládání řetězců probíhá v době překladu a při zavádění tříd do paměti. Pokud se však řetězec vytvoří až za běhu, tak se neoptimalizuje uložení a porovnání pomocí operátoru == proto nefunguje. V následujícím kódu metoda println() vypíše hodnotu false: String retezec1 = "text"; System.out.println( (retezec1 + "ik") == "textik));
Doporučujeme vždy používat pro porovnání metodu equals() – je pouze nepatrně pomalejší, ale garantuje porovnávání dle obsahu za všech situací.
5.2. Další operace s řetězci Pro převody primitivních datových typů na řetězce je ve třídě String definována metoda třídy valueOf(). Například: String retezec1 = String.valueOf(3.7629);
uloží do objektu retezec1 hodnotu 3.7629 jako řetězec12. Stejného výsledku lze dosáhnout i následujícím výrazem String retezec1 = "" + 3.7629;
kdy se využije vlastnosti automatické konverze proměnných na řetězec. Tato druhá varianta je však pomalejší, neboť zde se vedle konverze vytvoří objekt typu String s prázdným řetězcem a oba řetězce se spojí do další instance třídy String. Metoda String.valueOf s parametrem typu objekt vrací na výstupu výsledek metody toString() příslušného objektu. Třída String poskytuje velké množství dalších metod pro práci s řetězci. Mezi základní patří následující: metoda
popis
int length()
vrací délku uloženého řetězce
boolean equals(String str)
porovnání obsahu dvou instancí třídy String
boolean equalsIgnoreCase(String str)
porovnání obsahu dvou instancí třídy String s tím, že se nerozlišují malá/velká písmena
boolean endsWith(String koncovka)
zjišťuje, zda uložený řetězec končí zadanou koncovkou
boolean startsWith(String str)
zjišťuje, zda uložený řetězec začíná uvedeným parametrem
String toLowerCase()
vrací instanci třídy String se všemi znaky převedenými na malá písmena
String toUpperCase()
vrací instancí třídy String se všemi znaky převedenými na velká písmena
12
Při prohlížení tohoto výrazu mohou u některých čtenářů vzniknout pochybnosti, proč zde není operátor new pro vytvoření nového objektu. Metoda valueOf() sama uvnitř vytvoří novou instanci třídy String a na výstupu vrací pouze odkaz na tuto instanci, který se přiřadí k příslušnému identifikátoru.
Řetězce (třída String)
strana 51
metoda
popis
String substring(int beginIndex)
vrací instanci třídy String obsahující část řetězce začínající na zadaném indexu
String substring(int beginIndex, int endIndex)
vrací instanci třídy String obsahující část řetězce začínající na zadaném indexu a končící druhým indexem
int indexOf(String str)
vrací pozici v rámci uloženého řetězce, na které začíná řetězec uvedený jako parametr; pokud neobsahuje zadaný řetězec, vrací hodnotu –1
char[] toCharArray()
převede uložený řetězec do pole znaků
static String valueOf(Object o)
vrátí textovou reprezentaci objektu – pokud je parametrem hodnota null, vrátí řetězec „null“, jinak vrátí výsledek metody o.toString()
Tabulka 5.1 Přehled nejpoužívanějších metod třídy String
5.3. Speciální (escape) znaky v řetězcích Při psaní textových řetězců i znakových konstant ve zdrojovém kódu programu lze používat speciální (escape) znaky uvozené zpětným lomítkem – viz tabulka 5.2. escape znak popis \t
tabulátor
\n
nový řádek, používá se při výstupu do konzole či do souboru
\"
uvozovky
\’
apostrof
\\
zpětné lomítko
Tabulka 5.2 Přehled escape znaků používaných třídou String Do řetězců lze vkládat znaky z tabulky Unicode13 pomocí zápisu \uxxxx, kde na místě xxxx jsou uvedeny hexadecimální znaky. Např. zápis \u20ac odpovídá znaku € (euro). Dále lze vkládat znaky pomocí oktálových čísel (prvních 256 znaků z tabulky Unicode \u0000 až \u00ff) – za zpětným lomítkem se uvedou oktalová čísla 0 – 377.
5.4. Formátování řetězců Od verze 5 Java podporuje formátování řetězců podobné funkcím sprintf() a printf() v jazyce C. Ve třídě String je k dispozici statická metoda format(), která se obvykle používá pro formátování řetězců. Toto formátování se dále používá v metodě printf() ve třídách PrintWriter a PrintStream. V těchto metodách se jako první parametr zadává předpis pro formátování výstupu. Poté následují parametry, které se doplní do příslušné části předpisu. Vlastní formátování zajišťuje třída Formatter, u které jsou podrobně popsána pravidla pro formátování. Při volání metod používajících formátování lze jako první parametr uvést odkaz na příslušné národní prostředí (Locale), které ovlivňuje např. formátování data a času či znaky použité na místě desetinné čárky. Předpis pro formátování obsahuje texty/znaky, které mají být součástí každého výstupu a formáty pro jednotlivé parametry uvozené znakem procento (%). Následují příklady použití: 13
Od verze 5.0 Java podporuje rozšířené znaky Unicode – znaky, které jsou mimo základní rozsah 2 bytů. Podrobnosti o jejich používání najdete v dokumentaci Javy u firmy Sun.
Řetězce (třída String)
strana 52
String vystup = String.format("strana: %d/%d", strana, pocetStran); System.out.printf("úhly – alfa: %f6.4, beta: %f6.4, gama: %f6.4%n", alfa, beta, gama); System.out.printf("%-30s %2d %f4.2%n", prijmeni, semestr, prumer);
V prvním příkladu se na místo prvního řetězce %d doplní obsah proměnné strana, místo druhého řetězce %d se doplní obsah proměnné pocetStran. Obecná specifikace formátu vypadá následovně: %[argument_index$][příznaky][šířka][.přesnost]konverze
Z jednotlivých částí je nutný pouze úvodní znak procenta a určení konverze parametru. Číslo argument_index odkazuje na pořadí parametrů (počítají se od 1) a používá se při formátování data. Šířka udává minimální počet výstupních znaků pro formátování výstupu. Pokud se parametr do uvedeného počtu znaků nevejde, tak se vypisuje ve skutečné velikosti. Pokud je počet znaků delší než vlastní výstup, tak se výstup zarovná vpravo (pokud není uveden příznak pro zarovnání vlevo). Přesnost omezuje počet výstupních znaků, přesný význam závisí na konkrétní konverzi. U desetinných čísel znamená počet číslic za desetinnou tečkou (zaokrouhluje se), u formátu %s určuje maximální počet znaků. Pro ostatní konverze nemá přesnost smysl. V následující tabulce jsou uvedeny možné konverze. Pokud se místo malého písmene označujícího formát použije velké písmeno, tak se výstup na konci převede na velká písmena. konverze
typ parametru
popis
'b', 'B'
libovolný
Pokud je parametr typu boolean či Boolean, tak vloží true či false. Pokud je parametr jiného typu, výsledkem je true. Pokud je parametrem hodnota null, vloží se false.
'h', 'H'
libovolný
Pokud je parametrem hodnota null, vloží se null. Jinak se vloží výsledek metody Integer.toHexString(arg.hashCode()).
's', 'S'
libovolný
Pokud je parametrem hodnota null, vloží se null. Pokud parametr implementuje rozhraní Formattable, vloží se výsledek metody arg.formatTo(). Jinak se vloží výsledek metody arg.toString().
'c', 'C'
znak
Vloží se znak v Unicode.
'd'
celé číslo
Vloží se celé číslo v dekadickém tvaru.
'o'
celé číslo
Celé číslo se vloží v oktalové notaci.
'x', 'X'
celé číslo
Vloží se celé číslo v hexadecimální notaci.
'e', 'E'
desetinné číslo
Vloží se desetinné místo ve vědecké notaci.
'f'
desetinné číslo
Vloží se desetinné číslo.
'g', 'G'
desetinné číslo
V závislosti na velikosti čísla a požadované přesnosti se vloží desetinné číslo v normální či ve vědecké notaci.
'a', 'A'
desetinné číslo
Vloží se číslo v hexadecimálním tvaru.
't', 'T'
datum/čas
Prefix pro konverzi data a času – následuje upřesňující znak, bližší popis viz dokumentace třídy Formatter.
'%'
Vloží se znak '%'.
'n'
Vloží se správný oddělovač řádků pro konkrétní platformu.
Tabulka 5.3 Přehled možných konverzí používaných v metodách format() a printf()
Řetězce (třída String)
strana 53
Při specifikaci formátu lze použít příznaky uvedené v následující tabulce. příznak
význam
'-'
pokud bude výstup kratší, než uvedená délka, zarovná se doleva
'0'
u čísel budou uvedeny úvodní nuly
'+'
u čísel bude vždy uvedeno znaménko
','
u čísel budou použity oddělovače řádů dle národního prostředí (Locale)
Tabulka 5.4 Přehled specifikací formátu Při nesprávně zapsaném formátu či při neodpovídajícím datovém typu vznikají výjimky, které by měl programátor odchytávat.
5.5. Regulární výrazy Regulární výrazy umožňují zjistit, zda zadaný řetězec či jeho část odpovídá zadanému vzoru. Dále je možné nahradit část řetězce jiným a dle vzoru rozdělit řetězec na jednotlivé části. Regulární výrazy jsou součástí Javy od verze 1.4, kdy byl doplněn balíček java.util.regex a třída String byla rozšířena o několik metod pracujících s regulárními výrazy – viz následující tabulka. hlavička metody třídy String
popis metody
public boolean matches(String regex)
zjišťuje, zda celý uložený řetězec v instanci odpovídá celému vzoru
public String replaceFirst(String regex, String replacement)
metoda nahradí první výskyt odpovídající zadanému vzoru druhým řetězcem
public String replaceAll(String regex, String replacement)
metoda nahradí všechny výskyty odpovídající zadanému vzoru druhým řetězcem
public String[] split(String regex)
metoda rozdělí řetězec na základě zadaného vzoru na jednotlivé části a vrátí jako pole řetězců
public String[] split(String regex, int limit)
metoda rozdělí řetězec na základě zadaného vzoru na jednotlivé části a vrátí jako pole řetězců; proti předchozí variantě je omezen maximální počet částí, na které se řetězec rozdělí
Tabulka 5.5 Metody třídy String, které pracují s regulárními výrazy V případě vícenásobného použití stejného regulárního výrazu (např. v cyklu) je efektivnější použít třídy Pattern a Matcher, které obsahují i další metody rozšiřující možnosti regulárních výrazů. Vzory jsou základní prvky pro vytváření regulárních výrazů – vzorem se popisuje, jak má přípustný řetězec vypadat. Nejjednodušším vzorem je řetězec, který chceme vyhledat. Následuje přehled základních pravidel pro vytváření vzorů, další možnosti jsou uvedeny v dokumentaci. speciální symbol v regulárním výrazu
popis
. (tečka)
libovolný znak
+ (plus)
minimálně jedno opakování předchozího znaku/výrazu
* (hvězdička)
0 až nekonečno opakování předchozího znaku/výrazu
[ ] (hranaté závorky)
množina, tj. libovolný znak z množiny znaků uvnitř závorek
Řetězce (třída String)
strana 54
speciální symbol v regulárním výrazu
popis
\ (zpětné lomítko)
potlačuje speciální význam následujícího znaku či uvozuje speciální skupinu znaků (pokud je za lomítkem písmeno či číslice)
\\
zpětné lomítko
\b
hranice slova
\d
číslice
\s
„netisknutelné“ znaky – mezera, tabulátor, konec řádku
X|Y
buď znak X nebo znak Y
(X)
označení skupiny
Tabulka 5.6 Přehled nejčastěji používaných speciálních symbolů pro regulární výrazy Příklady V příkladech jsou vzory zadány jako řetězce ve zdrojovém kódu programu – v této poměrně obvyklé situaci se uplatňuje zpětné lomítko též jako speciální znak pro řetězcovou konstantu (viz tabulka 5.7) a proto je potřeba pro vložení zpětného lomítka vzoru uvést dvě zpětná lomítka. příklad regulárního výrazu a popis retezec.matches("ahoj"); True, pokud řetězec obsahuje pouze "ahoj" – vhodnější je použít metodu equals(), neboť je rychlejší. retezec.matches(".*\\bahoj\\b.*"); True, pokud řetězec slovo ahoj (jsou zde uvedeny hranice slova). retezec.matches("\\s*"); True, pokud řetězec je „prázdný“, tj. má nulovou délku, či obsahuje pouze mezery nebo tabulátory. retezec.matches(".*\\s+"); True, pokud na konci řetězce je minimálně jedna mezera a tabulátor. retezec.matches("[a-wyz].*"); True, pokud řetězec začíná písmenem a až z s výjimkou písmene x. retezec.matches(".*\\\\.*"); Zjišťuje se, zda řetězec obsahuje aspoň jedno zpětné lomítko. retezec.matches(".*\\s(try)|(catch)\\s.*"); True, pokud řetězec obsahuje slovo try či catch (nebo oboje) oddělené od okolí mezerou či tabulátorem. radek = radek.replaceFirst("^\\s+",""); Zrušení mezer na začátku řádku. radek = radek.replaceFirst("\\s+$",""); Zrušení mezer na konci řádku. radek = radek.replaceAll("\\s+",""); Zrušení všech mezer v řádku.
Řetězce (třída String)
strana 55
příklad regulárního výrazu a popis String radek = "xabcd01:1:IN:Student Testovací"; String [] polozky = radek.split(":"); Řádek se rozdělí na jednotlivé části, oddělovačem je znak dvojtečky. Vznikne pole se čtyřmi prvky. String radek = "xabcd01 1 IN Student Testovací"; String [] polozky = radek.split("\\s+",4); Řádek se rozdělí na jednotlivé části s tím, že oddělovačem je posloupnost mezer a tabulátorů. Výstup je omezen na maximálně čtyři položky, které i v našem případě vzniknou. Tabulka 5.7 Příklady použití regulárních výrazů V následující ukázce se zpracovávají řádky z textového souboru o evidovaných počítačích. Řádek vstupního souboru vypadá následovně (IP adresa, jméno, umístění): 146.102.33.1:s019h01:019sb
Program ze vstupního souboru vybere řádky z místnosti 019sb (na začátku se provádějí kontroly vstupního řádku), jeho rozdělení na části pomocí metody split() a nahrazení části textu jiným (v našem případě se nahradí prázdným řetězcem, tj. v podstatě se začátek řetězce zruší). Pro výše uvedený ukázkový řádek bude výsledkem hodnota "33.1". String radek = vstup.readLine(); while (radek != null) { if (radek.matches("#.*")) { // komentar next; } if (radek.matches("[ \t]*")) { // prázdný řádek ci //pouze mezery a tabulátory next; } if (radek.matches("[0-9\\.]+:.+:.+")) { // řádek je O.K. if (radek.matches(".*:019sb")) { //z místnosti 019sb? String [] prvky = radek.split(":"); // rozdělení řádku // na části dle : String ipAdresa=prvky[0].replaceFirst("146.102.",""); // cast IP adresy ... } } radek = vstup.readLine(); }
Čtení ze souboru je podrobně popsáno v kapitole věnované vstupně/výstupním operacím.
5.6. Třídy StringBuffer a StringBuilder Ke třídě String existuje alternativní třída StringBuffer, která na rozdíl od třídy String není read-only – při přidávání/rušení řetězců se nevytváří nové instance, čímž lze dosáhnout větší efektivity programu. Od Javy 5.0 existuje ještě třída StringBuilder, která má stejné metody jako StringBuffer. Hlavní rozdíl je v tom, že třídy StringBuilder není synchronizovaná – tj. nelze ji používat ve vláknech. Pokud je použita v jednom vlákně, tak je rychlejší než StringBuffer. Použití třídy StringBuffer má význam v situaci, kdy potřebujeme spojit více řetězců, vkládat řetězce dovnitř existujícího řetězce či střídavě vkládat/rušit části řetězce. V následující tabulce je přehled vybraných metod tříd StringBuffer a StringBuilder.
Řetězce (třída String)
strana 56
metoda
popis
int length()
vrací délku uloženého řetězce
StringBuffer append(String str)
na konec uloženého řetězce vloží obsah instance třídy String
StringBuffer append(Object o)
k uloženému řetězci přidá textovou reprezentaci objektu (výsledek operace String.valueOf(o))
StringBuffer insert(int pozice, String str)
na zadanou pozici vloží řetězec
StringBuffer insert(int pozice, Object o)
na zadanou pozici vloží textovou reprezentaci objektu (výsledek operace String.valueOf(o))
StringBuffer delete(int zacatek, int konec)
smaže znaky z pozice zacatek po pozici konec
String toString()
vrátí obsah jako instanci třídy String
String substring(int beginIndex)
vrací instanci třídy String obsahující část řetězce začínající na zadaném indexu
String substring(int beginIndex, int endIndex)
vrací instanci třídy String obsahující část řetězce začínající na zadaném indexu a končící druhým indexem
Tabulka 5.8 Přehled vybraných metod třídy StringBuffer, třída StringBuilder má stejné metody V následujícím příkladu se prochází v cyklu mapa s druhy zvířat a jejich počty. Pro každý druh se vytvoří řetězec s popisem. for (String klic: mapa.keySet()) { StringBuffer sb = new StringBuffer(); sb.append("zvire "); sb.append(klic); sb.append(", pocet kusu "); sb.append(mapa.get(klic)).toString(); String radek = sb.toString(); }
Tento kód je asi o třetinu rychlejší (ale méně přehledný), než následující kód pracující s instancemi třídy String. for (String klic: mapa.keySet()) { String radek = "zvire "+klic+", pocet kusu "+mapa.get(klic); }
Třídy StringBuffer a StringBuilder jsou navrženy tak, že po zavolání metod append(), insert() a delete() vrací odkaz na sebe sama. To umožňuje přehlednější zápis řetězením těchto příkazů, viz následující kód. Tento kód je nejrychlejší – používá se instance třídy StringBuilder, využívá se pouze jedna instance této třídy a na začátku je nastavena předpokládaná velikost řetězce. StringBuilder sb = new StringBuilder(60); for (String klic: mapa.keySet()) { sb.delete(0, sb.length()); sb.append("zvire ").append(klic); sb.append(", pocet kusu ").append(mapa.get(klic)); String radek = sb.toString(); }
Třída Object
strana 57
6. Třída Object Třída Object je v Javě předkem všech tříd (včetně speciálních typů – polí (array) a výčtového typu (enum)) a jako jediná žádného předka nemá. Pokud při deklaraci své třídy neuvedete slovo extends a jméno předka, bude jako předek vaší třídy automaticky dosazena třída Object. Třída Object neobsahuje žádné zvnějšku viditelné datové atributy, je zde však definováno několik metod, které se dědí a je možno je používat či překrývat. Koncepcí Javy je též zaručeno, že všechny třídy mají k dispozici tyto metody – zděděné nebo překryté.
6.1. Metoda toString() Tato metoda vrací textovou reprezentaci objektu, která je snadno čitelná. Metoda se používá automaticky v mnoha situacích, např. pokud je instance parametrem metody System.out.println(), metoda println() vypíše výsledek metody toString(). Pokud se při spojování řetězců (použití operátoru + pro spojování řetězců) jako parametr zadá instance, též se na její místo doplní výsledek metody toString(). Standardní formát výpisu metody toString() třídy Object však není příliš vhodný (vypisuje se jméno třídy a výsledek metody hashcode()), proto se doporučuje tuto metodu překrýt, pokud ji chceme využívat např. pro kontrolní výpisy. V projektu Skola je ve třídě Osoba překryta metoda toString() následovně: public String toString() { return titulPred+" "+jmeno+" "+titulZa; }
6.2. Metoda equals() Metoda equals() se používá v případě, že chceme zjistit, zda se dva objekty rovnají. Ve třídě Object je napsána nejpřísnějším možným způsobem, tj. porovnávané objekty jsou si rovny, pokud oba názvy odkazují na stejnou instanci (na stejné místo v paměti). Vrací tedy stejný výsledek jako použití operátoru ==. Tento způsob porovnávání u věcných tříd obvykle nevyhovuje, a proto je vhodné metodu equals() překrýt a porovnávat obsahy. Je potřeba si uvědomit, že se tato metoda používá automaticky v mnoha situacích – např. při ukládání instancí do množin (Set), při používání instance jako klíče v mapách (Map). Metoda equals() je překryta ve většině základních tříd, např. ve třídách String, Integer. Při překrývání metody equals() musíme dodržet následující pravidla: ♦ musí být reflexivní, tj. x.equals(x) == true, ♦ musí být symetrická, tj. x.equals(y)== true právě tehdy, když y.equals(x)==true, ♦ musí být tranzitivní, tj. když x.equals(y)==true a y.equals(z)==true, tak x.equals(z) musí vrátit také true, ♦ musí být konzistentní, tj. pro nezměněné instance vrací vždy stejnou hodnotu, ♦ pro parametr null vracet hodnotu false, tj. x.equals(null) vrátí false. Tato pravidla najdete v dokumentaci metody equals() ve třídě Object. Dále je vhodné, aby se pro porovnání instancí používaly ty datové atributy, které se v průběhu života instance nemění. Metoda equals() ve třídě Mistnost v projektu Adventura (viz kapitola19) vypadá takto:
Třída Object
strana 58
public boolean equals (Object o) { if (o instanceof Mistnost) { Mistnost druha = (Mistnost)o; return nazev.equals(druha.nazev); } else { return false; } }
Z kódu vyplývá, že dvě instance třídy Mistnost si jsou rovny, pokud mají stejný název (stejnou hodnotu v datovém atributu nazev typu String).
6.3. Metoda hashCode() Metoda hashCode() vrací pro každou instanci číslo typu int. Využívá se pro optimalizaci ukládání do dynamických datových struktur HashSet nebo HashTable. Pro metodu hashCode() jsou v dokumentaci Javy stanovena tato pravidla: ♦ je konzistentní, tj. pro nezměněné instance vrací vždy stejnou hodnotu, ♦ pro dvě instance, které jsou si rovny na základě použití metody equals() vrací stejnou hodnotu, ♦ pro dvě instance, u kterých metoda hashCode() vrací stejnou hodnotu, ještě nemusí platit, že jsou si rovny na základě použití metody equals(). Z toho vyplývá, že když překryjete ve své třídě metodu equals(), měly byste překrýt i metodu hashCode(). Pro třídu Mistnost v projektu Adventura je metoda hashCode() jednoduchá, neboť využívá metodu hashCode() třídy String: public int hashCode() { return nazev.hashCode(); }
6.4. Metoda getClass() Tato metoda nám za běhu programu může vrátit informace o objektu. Vrací je jako instanci třídy Class, která je potomkem třídy Object. Následujícím kódem za běhu zjistíme jméno třídy pro instanci, na kterou odkazuje proměnná moje: String jmenoTridy = moje.getClass().getName();
Metoda getClass() má modifikátor final, tj. nemůžeme ji překrýt.
6.5. Metoda clone() Metoda clone() se používá pro vytváření identické kopie jedné instance (je potřeba rozlišovat kopii instance od vytvoření dalšího odkazu na stejný objekt, který se vytváří přiřazovacím příkazem). Po provedení metody clone() jsou obě instance, původní i nová, na sobě nezávislé. Pokud chceme použít ve vlastní třídě klonování, je nutné metodu clone() překrýt a navíc musí naše třída implementovat rozhraní Cloneable, jinak bude vždy vyhozena výjimka CloneNotSupportedException. Rozhraní Cloneable neosahuje žádnou metodu, není tedy třeba implementovat jinou metodu než clone().
6.6. Metoda finalize() Tuto metodu spouští Garbage Collector (GC) při čištění paměti od neplatných objektů. Pokud GC zjistí, že na konkrétní instanci nevede žádný odkaz, spustí tuto metodu. Není ale garantováno, že tato
Třída Object
strana 59
metoda proběhne – pokud končí aplikace, tak se okamžitě uvolní paměť a metody finalize() se nevolají. Z tohoto důvodu nelze metodu finalize() používat pro operace, které musí proběhnout, např. pro uzavření souborů.
6.7. Metody notify(), notifyAll(), wait() Tyto metody se vztahují k použití více vláken (thread) v programu. Problematika vláken je mimo rozsah těchto skript.
Třída Object
strana 60
Statické prvky třídy
strana 61
7. Statické prvky třídy V úvodních kapitolách jsme popsali deklaraci a používání datových atributů a metod instance. Jsou to nejčastěji používané součásti třídy, nicméně třída v Javě může obsahovat ještě další prvky. Nyní si popíšeme význam a použití statických datových atributů a statických metod.
7.1. Statické datové atributy (statické proměnné, proměnné třídy) Statický datový atribut (též statická proměnná či proměnná třídy) je společný pro všechny instance dané třídy. V deklaraci je za modifikátorem přístupu uveden modifikátor static. Při vysvětlování statických proměnných budeme pokračovat v našem jednoduchém příkladě s účty. Budeme mít úrok z uložených peněz, který bude pro všechny účty stejný. Můžeme do každé instance přidat datový atribut urok, který bude mít vždy stejnou hodnotu. Při změně úroku však budeme muset zavolat metodu pro změnu úroku u každé instance. Druhým a lepším řešením je použití statické proměnné, která bude v paměti pouze jednou a o které budou „vědět“ všechny instance. Tato proměnná vznikne při natažení kódu třídy Ucet do paměti, tj. ještě před vytvořením první instance. Deklaraci všech datových atributů včetně statického je v následujícím kódu. public class private private private private }
Ucet { static double urok = 2.5; int cisloUctu; String jmenoVlastnika; double castka = 0;
//zde následuje další kód třídy
Z jiných tříd se na statickou proměnnou odkazujeme se jménem třídy např. Ucet.urok (v našem případě tuto konstrukci nelze použít, protože proměnná třídy byla deklarována private). Uvnitř kódu třídy Ucet se na ni odkazujeme pouze pomocí identifikátoru.
Obrázek 7.1 Znázornění vztahu mezi instancemi třídy a statickým datovým atributem Jak již bylo řečeno, statická proměnná existuje v paměti jen jednou a „vědí“ o ní všechny instance. Zjednodušeně je vztah instancí ke statické proměnné znázorněn na obrázku 7.1. Statické proměnné se též používají pro vytváření pojmenovaných konstant, které jsou uvozeny modifikátory public static final (je to jedna z mála situací, kdy datové atributy nejsou private). Hodnotu pojmenovaných konstant nelze po přiřazení změnit (modifikátor final), proto mohou být veřejné. Použití modifikátoru static znamená, že tyto konstanty jsou v paměti uloženy pouze jednou. Ukázka deklarace takové konstanty vypadá následovně: public static final double PI = 3.14;
Statické prvky třídy
strana 62
Dalším příkladem pojmenovaných konstant mohou být konstanty ve třídě Math – hodnota pí (π) či Eulerova konstanta.
7.2. Statická metoda (metoda třídy) Statická metoda (též se používá pojem metoda třídy) je metoda společná pro všechny instance, která má při deklaraci uveden modifikátor static. Statické metody jsou nezávislé na jakékoliv instanci třídy, tj.: ♦ pro použití není potřeba vytvořit žádnou instanci, ♦ statická metoda se nemůže přímo odkazovat na datové atributy instance či metody instance, ve statické metodě lze ale vytvořit instanci třídy a poslat ji zprávu, ♦ z instance lze přímo volat statické metody, ♦ statická metoda může přímo použít statické proměnné. V našem příkladu vytvoříme statickou metodu na změnu úroku. Pro všechny účty najednou změníme výši úročení vkladů. Metoda se jmenuje setUrok() a má parametr typu double. Pojmenování setUrok() opět souvisí s konvencemi v Javě, kdy metoda, která slouží k nastavení nebo změně hodnoty datového atributu se pojmenovává setJmenoAtributu. Kromě metody pro změnu úroku bychom mohli napsat i statickou metodu getUrok(), která by vracela aktuální hodnotu úroku. I tato metoda by měla v hlavičce modifikátor static. Kód metody setUrok() vypadá takto: public static void setUrok (double novyUrok) { urok = novyUrok; }
Pro volání metody setUrok() není nutné mít vytvořenou instanci. Mimo kód třídy Ucet se metoda volá pomocí jména třídy a tečky tedy: Ucet.setUrok(3.0);
Ve třídě Ucet (tj. v jiné metodě stejné třídy) se lze na tuto metodu odkazovat také celým jménem včetně názvu třídy, či pouze jménem metody – překladač jméno třídy doplní. Statické metody se používají ve více situacích: ♦ pro změnu statického datového atributu (viz předchozí příklad), pro získání hodnoty statického datového atributu (pokud je private). ♦ pro provedení operace, u které není potřeba instance – příkladem mohou být matematické operace, např. sin, cos, druhá odmocnina. Třída Math a další příklady tohoto použití jsou v další části této kapitoly. Dalším příkladem těchto statických metod může být např. metoda parseInt() ve třídě Integer, která převede řetězec na proměnnou typu int. ♦ pro získání instance třídy v situaci, kdy pomocí přetížení konstruktoru nelze odlišit jednotlivé varianty vstupních parametrů. Příkladem může být konstruktor pro vytvoření trojúhelníka – nejsme schopni pomocí parametrů odlišit variantu, kdy jsou zadány tři strany od varianty, kdy zadáme dvě strany a jeden úhel (v obou případech budou zadána 3 reálná čísla). Proto použijeme statické metody, které se liší názvem. Bližší popis je v projektu Trojuhelniky. ♦ pro získání instance třídy v situaci, kdy na základě parametrů vrací statická metoda různé instance – toto souvisí s dědičností a polymorfismem. Příkladem může být získání ovladače k SQL databázi pomocí metody forName() ze třídy Class. Podrobnější popis této problematiky je mimo zaměření skript.
7.3. Statické datové atributy a metody ve třídách Math a System 7.3.1. Třída Math Třída Math ze standardu Javy obsahuje pouze statické konstanty a statické metody. Třída nemá veřejný konstruktor, nelze tedy vytvořit instanci a ani to není smysluplné. Takto navržená třída se označuje jako utilita.
Statické prvky třídy
strana 63
Dvě konstanty ze třídy Math jsou uvedeny v tabulce 7.1, vybrané statické metody ze třídy Math jsou v tabulce 7.2. konstanty třídy
význam
Math.PI
hodnota pí (π)
Math.E
Eulerova konstanta přirozeného logaritmu
Tabulka 7.1 Konstanty třídy Math statické metody ve třídě
popis metody
double Math.abs(double a)
absolutní hodnota
double Math.ceil(double a)
zaokrouhlení na nejbližší vyšší celé číslo
double Math.floor(double a)
zaokrouhlení na nejbližší nižší celé číslo
double Math.rint(double a)
zaokrouhlení na nejbližší celé číslo
double Math.sin(double a)
sin úhlu v radiánech
double Math.cos(double a)
cos úhlu v radiánech
double Math.tan(double a)
tangens úhlu v radiánech
double Math.toDegrees(double a)
převod úhlu v radiánech do stupňů
double Math.toRadians(double a)
převod úhlu ve stupních do radiánů
double Math.pow(double a, double b)
mocnina ab
double Math.sqrt(double a)
druhá odmocnina
double Math.log(double a)
přirozený logaritmus
double Math.log10(double a)
desítkový logaritmus
double Math.exp(double a)
přirozená mocnina ea
Tabulka 7.2 Přehled vybraných metod třídy Math
7.3.2. Třída System Třída System z balíčku java.lang poskytuje tři statické proměnné sloužící pro vstup a výstup z/na konzoly. statická proměnná
popis
InputStream System.in
vstup z konzoly
PrintStream System.out
výstup na konzolu
PrintStream System.err
chybový výstup
Tabulka 7.3 Přehled statických proměnných třídy System Podrobný popis typů InputStream a PrintStream je uveden v kapitole věnované vstupu a výstupu. Zde si jenom uvedeme, že u třídy PrintStream se nejčastěji používají následující metody: void println (String text) void print (String text) void printf(String format, Object ... args)
První metoda vypíše text a odřádkuje, metoda print nedoplňuje na konec výpisu odřádkování. Metoda printf umožňuje formátovat výstup – bližší popis je v kapitole věnované třídě String.
Statické prvky třídy
strana 64
Pomocí statických metod setIn(), setOut() a setErr() mohou být tyto statické proměnné přesměrovány jinam. Např. v BlueJ jsou proměnné System.in, System.out a System.err přesměrovány do samostatného grafického okna pojmenovaného Terminal. Další statické metody třídy System jsou uvedeny v tabulce 7.4. hlavička metody
popis
void System.exit(int status)
ukončí aplikaci, jako parametr se zadává návratový kód (nula při úspěšném ukončení, kladná čísla pro označení chyby)
String System.getEnv(String name)
získání hodnoty environment proměnné (proměnné operačního systému), závisí na operačním systému
long System.currentTimeMillis()
získání aktuálního času v milisekundách, přesnost závisí na operačním systému
long System.nanoTime()
získání aktuálního času v nanosekundách, přesnost závisí na operačním systému
void System.setSecurityManager (SecurityManager s)
nastavení správce zabezpečení, toto téma je mimo rozsah těchto skript
Properties System.getProperties()
získání všech proměnných/vlastností JVM, mezi tyto vlastnosti patří verze JVM, identifikace operačního systému, domovský adresář uživatele
String System.getProperty(String name)
získání hodnoty konkrétní proměnné/vlastnosti JVM
String System.setProperty(String name, String value)
nastavení hodnoty konkrétní proměnné/vlastnosti JVM
Tabulka 7.4 Přehled vybraných metod třídy System
7.4. Standardní spouštění aplikace – metoda main Aby bylo možno spustit Java aplikaci, musí existovat aspoň jeden vstupní bod do aplikace. V případě apletů se vytvoří instance hlavní třídy apletu a poté se volají metody init(), start(), paint(), stop() a destroy() v závislosti na stavu stránky s apletem. V případě servletů se vytvoří instance třídy a volá se metoda doGet(), doPost(), doPut(), doDelete() v závislosti na typu požadavku od klienta. Při spouštění Java aplikace v operačním systému se nevytváří automaticky instance, bylo zvoleno řešení s využitím statické metody main(). Při spouštění JVM (příkaz java v operačním systému) říkáme, jaká třída má být natažena do paměti první: java MojeTrida
JVM hledá v kódu této třídy statickou metodu main() a tu spustí (pokud metoda main() neexistuje, tak JVM vypíše chybové hlášení). Metoda main() musí splňovat následující pravidla: ♦ metoda musí být veřejná (public), jinak by ji nebylo možné spustit, ♦ metoda musí být statická, není vytvořena žádná instance, pro kterou by bylo možné spustit metodu instance, ♦ metoda nevrací žádnou hodnotu (není možné nastavit žádnou proměnnou, do které by se uložil výsledek), ♦ metoda má jako vstupní parametr pole prvků typu String (jméno args je jediné, co lze v hlavičce metody změnit např. na String parametry [], ale obvykle se název pole parametrů nemění), do tohoto pole se ukládají parametry příkazové řádky.
Statické prvky třídy
strana 65
Hlavička metody main() je následující: public static void main (String [] args)
Aplikace provede kód v metodě main() a skončí (pokud není aplikace ukončena dříve výskytem výjimky nebo metodou System.exit()). Pokud při zpracování metody main() vznikly vlákna, čeká se na jejich dokončení (vlákna vznikají např. při spuštění grafické aplikace). Následuje příklad metody main() v projektu Trojuhelniky (blíže viz kapitola 17): public static void main (String [] args) { Trojuhelniky troj = new Trojuhelniky(); troj.zakladniCyklus(); }
Při spouštění aplikace lze též zadávat parametry příkazové řádky. Jednotlivé parametry jsou odděleny mezerou, v následující příkladu jsou při spuštění aplikace tří parametry na příkazové řádce: java Pocitej 3 + 5
Tyto parametry jsou dostupné v poli, které předá JVM metodě main() při spuštění. V našem případě bude parametrem pole o třech prvcích. Parametry jsou uloženy jako řetězce (String). Od verze 5.0 lze zapsat metodu main() s použitím proměnlivého počtu parametrů (viz kapitola 10.5):
public static void main (String... args)
7.5. Počítání vytvořených instancí Hezkým příkladem na používání statických datových atributů a statických metod je počítání vytvořených instancí nějaké třídy. public class Pokus { private static long pocetInstanci = 0; public Pokus () { pocetInstanci++; // další příkazy konstruktoru } public static long getPocetInstanci() { return pocetInstanci; } // další části třídy }
Je nutno zdůraznit, že se počítají vytvořené instance, ne instance existující14. Počítání existujících instancí je problematické, neboť není přesně určen okamžik, kdy instance přestane existovat. Pro účely počítání instancí můžeme za okamžik zrušení považovat chvíli, kdy Garbage Collector zavolá metodu finalize() – popis metody finalize() je v kapitole 6.6. Pro počítání existujících instancí je tudíž ještě potřeba překrýt metodu finalize() – celý kód bude vypadat následovně (klíčové slovo super je vysvětleno v kapitole 11.1 věnované dědičnosti): public class Pokus { private static long pocetInstanci = 0; public Pokus () { pocetInstanci++; // další příkazy konstruktoru } 14
Ani počet vytvořených instancí nemusí být přesný – pokud by třída Pokus měla potomky, tak i vytvoření instance potomka se započte mezi vytvořené instance třídy Pokus (v konstruktoru potomka se vždy volá konstruktor předka). Tj. počítáme vytvořené instance, které lze přetypovat na typ Pokus.
Statické prvky třídy
}
strana 66
public void finalize() { super.finalize(); pocetInstanci--; } public static long getPocetInstanci() { return pocetInstanci; } // další části třídy Uvedené kódy počítání instancí nefungují správně ve vláknech či při serializaci objektů – obě témata jsou však mimo rozsah těchto skript.
Výčtový typ
strana 67
8. Výčtový typ V této kapitole si ukážeme, jak implementovat v Javě „statické“ seznamy konstant (hodnot). Příkladem mohou být dny v týdnu, měsíce v roce, planety obíhající kolem slunce či přípustné parametry na příkazové řádce. Jedná se o seznamy, které se nemění za běhu aplikace – pokud se seznam změní (např. přibude další parametr příkazové řádky), musí se program znovu přeložit. Nebudeme se zde zabývat seznamy, kterým se za běhu programu počet prvků mění (např. seznamy studentů, seznamy místností, seznamy věcí). Nejdříve si ukážeme implementaci seznamu pomocí celočíselných pojmenovaných konstant, většina kapitoly je věnována výčtovému typu, který je novinkou ve verzi Javy 5.0.
8.1. Pojmenované konstanty Pro vyjádření seznamu přípustných hodnot lze použít celočíselné pojmenované konstanty. U pojmenovaných konstant se uvádějí modifikátory final (hodnotu konstanty nelze změnit), static (konstanty jsou nezávislé na instancích) a obvykle i public – konstanty bývají veřejně dostupné, což opět souvisí s jejich neměnitelností. Ukážeme si je na příkladu se dny v týdnu: public public public public public public public
static static static static static static static
final final final final final final final
int int int int int int int
DEN_NEDELE=0; DEN_PONDELI=1; DEN_UTERY=2; DEN_STREDA=3; DEN_CTVRTEK=4; DEN_PATEK=5; DEN_SOBOTA=6;
Tyto konstanty použijeme při vkládání termínů do rozvrhu. Hlavička metody by mohla vypadat takto: public void pridejTermin(int den, String odKdy, String doKdy, String popis) { // obsah metody }
Vlastní přidávání termínů může vypadat takto: rozvrh.pridejTermin(DEN_PONDELI, "14:30", "16:00", "konzultační hodiny"); rozvrh.pridejTermin(DEN_UTERY, "12:45", "14:15", "konzultační hodiny");
Použití pojmenovaných konstant pro vyjádření seznamu hodnot má ale několik nevýhod: ♦ řešení není typově bezpečné – bez upozornění se přeloží i následující kód: rozvrh.pridejTermin(999, "14:30", "16:00", "konzultační hodiny");
♦ řešení nepodporuje vkládání konstant – pokud se do seznamu konstant vloží nová konstanta či pokud se změní pořadí konstant, musí se znovu přeložit všechny třídy, které konstanty používají, ♦ při tisku mají malou vypovídací hodnotu – v našem příkladu se vytiskne číslo a ne např. „pondělí“ či „DEN_PONDELI“. Pojmenované konstanty se používají i v jiných situacích, např. pro vyjádření matematických konstant. V těchto případech mají své opodstatnění a samozřejmě nelze hovořit o výše uvedených nevýhodách.
Výčtový typ
strana 68
8.2. Výčtový typ (enum) V Javě 5.0 byl zaveden výčtový typ – enum, který řeší většinu nevýhod pojmenovaných konstant pro vytvoření uzavřené množiny neměnných hodnot. Základní deklarace výčtového typu je velmi jednoduchá: [public] enum název { hodnota1, hodnota2, ... }
Výčtový typ se obvykle deklaruje v samostatném souboru podobně jako třídy či rozhraní. Soubor se musí jmenovat stejně jako výčtový typ. Jméno výčtového typu by mělo začínat velkým písmenem, jména vlastních hodnot by měly být velkými písmeny obdobně jako pojmenované konstanty. V deklaraci výčtového typu se též obvykle používá i modifikátor přístupu public. Následuje příklad deklarace výčtového typu pro jednotlivé dny v týdnu: public enum DenVTydnu { NEDELE, PONDELI, UTERY, STREDA, CTVRTEK, PATEK, SOBOTA }
Při použití tohoto výčtového typu v rozvrhu musíme upravit hlavičku a částečně asi i obsah metody pridejTermin(). V hlavičce metody bude první parametr výčtového typu DenVTydnu: public void pridejTermin(DenVTydnu den, String odKdy, String doKdy, String popis) { // obsah metody }
Vlastní přidávání termínů bude nyní vypadat takto: rozvrh.pridejTermin(DenVTydnu.PONDELI, "14:30", "16:00", "konzultační hodiny"); rozvrh.pridejTermin(DenVTydnu.UTERY, "12:45", "14:15", "konzultační hodiny");
Řešení s výčtovým typem: ♦ je typově bezpečné – nelze za den v týdnu doplnit hodnotu, která není ve výčtovém typu, ♦ je odolnější vůči změnám výčtového typu – pokud se změní pořadí hodnot ve výčtovém typu či se dovnitř seznamu vloží další hodnota, není potřeba překládat třídy, které výčtový typ používají. ♦ při tisku se vytiskne smysluplnější hodnota, vytiskne se vlastní konstanta (např. „PONDELI“). V diagramu tříd se vyznačí výčtový typ s použitím stereotypu:
Obrázek 8.1 Diagram tříd s výčtovým typem
Výčtový typ
strana 69
8.3. Metody výčtového typu Výčtový typ patří mezi referenční typy, konkrétně je speciálním potomkem třídy Object, hodně se podobá třídám. Na rozdíl od tříd a rozhraní však nepodporuje dědičnost – nelze vytvořit výčtový typ, který by byl potomkem jiného výčtového typu. Jako potomek třídy Object obsahuje výčtový typ též všechny metody třídy Object – metody toString(), equals(), hashCode() a další. Výčtový typ má několik dalších metod. Statická metoda values() vrací výčet všech hodnot výčtového typu. Používá se pro procházení výčtového typu pomocí cyklu for. Následující kód vypíše všechny hodnoty výčtového typu DenVTydnu (vypíše všechny dny): for (DenVTydnu den : DenVTydnu.values() ) { System.out.println(den); }
U výčtových typů lze používat statickou metodu valueOf(), která vrátí prvek výčtového typu odpovídající zadanému řetězci. Pokud se nenajde prvek výčtového typu pro zadaný řetězec, vznikne výjimka IllegalArgumentException. Při vyhledávání prvku se kontroluje velikost písmen. V následujícím kódu vznikne na druhém řádku výjimka: DenVTydnu sobota = DenVTydnu.valueOf("SOBOTA"); DenVTydnu nedele = DenVTydnu.valueOf("nedele"); // výjimka !!!
Výčtový typ též implementuje rozhraní Comparable a tudíž obsahuje metodu compareTo() – dle výčtového typu je možné třídit. Prvky se řadí dle pořadí, v jakém jsou uvedeny ve výčtovém typu. V našem příkladu se dny by se rozvrh řadil od neděle do soboty.
8.4. Rozšíření příkazu switch V souvislosti s výčtovým typem byl též rozšířen příkaz switch – lze v něm uvést vedle celočíselných primitivních typů a typu char i výčtový typ. I u výčtového typu je vhodné vždy uvádět větev default a to i v případě, že vyjmenujeme všechny přípustné hodnoty – je to určitá ochrana pro případ přidání další hodnoty do výčtového typu. V následujícím příkazu switch se odlišuje víkend od pracovních dní (je to i jedna z mála situací, kdy ve větvích case není vždy uveden příkaz brek): // proměnná den je typu DenVTydnu switch (den) { case NEDELE: case SOBOTA: System.out.println("víkend"); break; case PONDELI: case UTERY: case STREDA: case CTVRTEK: case PATEK: System.out.println("pracovní den"); break; default : System.out.println("takový den neznám"); }
Všimněte si, že za slovem case se píší konkrétní konstanty výčtového typu bez odkazu na vlastní výčtový typ.
8.5. Výčtový typ uvnitř třídy V některých situacích potřebujeme seznam přípustných hodnot pouze uvnitř nějaké třídy – nechceme, aby výčtový typ byl veřejně dostupný. Řešením je definice výčtového typu uvnitř třídy. Ukážeme si to na příkladu se znaménky pro matematické operace:
Výčtový typ
strana 70
1 public class Vypocet { 2 3 private enum Operace { PLUS, MINUS, NASOBENO, DELENO } 4 5 public double vypocet (Operace operace, double prvni, 6 double druhy) { 7 switch (operace) { 8 case PLUS: return prvni + druhy; 9 case MINUS: return prvni - druhy; 10 case NASOBENO: return prvni * druhy; 11 case DELENO: return prvni/druhy; 12 default : return 0; 13 } 14 } 15 public static void main (String ... args) { 16 Vypocet spocti = new Vypocet(); 17 System.out.println("3 PLUS 5 = " + 18 spocti.vypocet(Operace.PLUS, 3, 5)); 19 } 20 }
Pokud je výčtový typ definován uvnitř třídy, je možné v deklaraci uvést všechny modifikátory přístupu (tedy i private a protected) – v tomto případě se chovají stejně, jako u datových atributů. V závislosti na modifikátoru lze deklarovaný výčtový typ používat i v potomcích hlavní třídy (v našem případě v potomcích třídy Vypocet). Pokud chceme, aby výčtový typ byl veřejně dostupný, měl by být definovaný v rámci samostatného souboru a ne uvnitř nějaké třídy.
8.6. Výčtový typ a datové struktury Pokud chceme ukládat výčtový typ do množin či ho používat jako klíč v mapách (viz kapitola 10), poskytuje Java v balíčku java.util speciální rychlé implementace množiny a mapy – třídy EnumSet a EnumMap. Třída EnumSet má navíc metodu range(), kterou lze použít pro procházení pouze části seznamu prvků ve výčtovém typu. V následujícím příkladu se vypíší pouze pracovní dny: for (Den den : EnumSet.range(Den.PONDELI, Den.PATEK)) { System.out.println(den); }
Třída EnumSet dále podporuje některé základní operace s množinami výčtových typů.
8.7. Rozšiřování výčtového typu Výčtové typy nemusí být tak jednoduché, jak jsme si doposud ukazovali. K hodnotám ve výčtovém typu lze přiřazovat další parametry (např. k planetám lze přiřadit vzdálenost od slunce, k názvům barev lze doplnit jejich označení pomocí RGB), výčtový typ lze rozšiřovat o další metody či překrývat již existující metody. Tato problematika je však již poměrně komplikovaná a proto doporučujeme ji nastudovat z dokumentace u firmy Sun. My si zde ukážeme pouze jednoduchý příklad, ve kterém překryjeme metodu toString(), aby vypisovala dny v týdnu malými písmeny:
Výčtový typ public enum Den { NEDELE, PONDELI, UTERY, STREDA, CTRVTEK, PATEK, SOBOTA;
}
public String toString() { return name().toLowerCase(); }
strana 71
Výčtový typ
strana 72
Polymorfismus a rozhraní
strana 73
9. Polymorfismus a rozhraní Tato kapitola navazuje na základní informace o objektech v kapitole 2, zde se budeme zabývat přetěžováním metod, polymorfismem a rozhraními.
9.1. Přetěžování metod a polymorfismus Pojem polymorfismus pochází z řečtiny a znamená mnohotvarost. V objektovém programování vyjadřuje situaci, kdy se při stejném volání provádí různý kód. Která konkrétní metoda se provede, závisí na: ♦ předávaných parametrech, ♦ objektu, kterému je zpráva předána. První varianta polymorfismu15 je v Javě realizována přes přetěžování metod. Druhá varianta polymorfismu je v Javě závislá na dědičnosti a pozdní vazbě, realizuje se přes překrývání metod. Nyní si vysvětlíme přetěžování metod, překrývání metod bude popsáno v kapitole 11 věnované dědičnosti.
9.1.1. Přetěžování metod Přetěžování metod označuje situaci, kdy nějaký objekt má více metod stejného jména. Při volání metody se konkrétní varianta metody vybere na základě typu a počtu předávaných parametrů. V Javě může existovat ve třídě více metod stejného jména, musí se však lišit typem a počtem parametrů. Správná metoda se přiřadí při překladu – vybere se na základě počtu a typu parametrů. Příkladem přetížení metod lišící se počtem parametrů mohou být dvě metody substring() pro vybrání podřetězce ze třídy String. Jejich hlavičky následují: String substring (int beginIndex) String substring (int beginIndex, int endIndex)
Přetížení na základě typu parametru si ukážeme na statických metodách valueOf() opět ze třídy String, které převádějí hodnoty v primitivních typech na řetězce. Následují hlavičky některých variant: static static static static
String String String String
valueOf(double d) valueOf(int i) valueOf(boolean b) valueOf(char c)
V následující ukázce použití překladač přiřadí správnou metodu valueOf() na základě typu parametru: String String String String
text1 text2 text3 text4
= = = =
"reálné číslo " + String.valueOf(2.52); "celé číslo " + String.valueOf(178); "logická hodnota " + String.valueOf(true); "znak tabulátor " + String.valueOf('\t');
Teprve při použití překrytých metod se hovoří o polymorfismu – při použití metody valueOf() programátorem není hned vidět, že se pod ní schovávají čtyři různé metody. Tj. při stejném volání se provádí různý kód. V situaci, kdy při vytváření třídy potřebujeme dvě významově podobné metody, je vhodné obě metody pojmenovat stejně a rozlišit je počtem či typem parametrů – vede to ke snadnějšímu použití třídy. Pokud metody nelze rozlišit počtem či typem parametrů, musí se napsat dvě metody s různým jménem. Příkladem takové situace ve třídě String jsou metody replaceAll() a replaceFirst(), které nahrazují části řetězce jiným. První metoda nahradí všechny výskyty, které odpovídají vzoru, druhá metoda nahradí pouze první výskyt.
15
Někteří teoretici objektového programování do polymorfismu zahrnují pouze překrývání.
Polymorfismus a rozhraní
strana 74
Pokud se metody liší typem parametru a mezi typy je vztah dědičnosti, tak se při překladu vybírá metoda s konkrétnějším typem. V Javě je možné též přetěžovat konstruktory, tj. jedna třída může mít více konstruktorů – ukázku můžete vidět ve třídě Ucet, která je popsána v kapitole 2.6.
9.1.2. Příklad polymorfismu s přetěžováním metod Polymorfismus s přetížením metody si ukážeme na příkladu louky s květinami, nad kterou létají včely (instance třídy Vcela) a motýli (instance třídy Motyl). Naším úkolem je ve třídě Louka dopsat metody pro vkládání motýlů a včel na louku16. Pro včely i pro motýly si vytvoříme samostatné seznamy (seznamy jsou popsány v kapitole 10.3): private List
vcely; private List<Motyl> motyli;
Nyní si ukážeme tři různé varianty řešení tohoto úkolu. Čtvrtá varianta bude uvedena v části věnované rozhraním. Varianta se dvěmi metodami různých názvů public void pridejMotyla (Motyl motyl) { motyli.add(motyl); } public void pridejVcelu (Vcela vcela) { vcely.add(vcela); }
Zmiňovaná varianta má tyto nevýhody: ♦ duplikuje se informace o tom, zda se předává motýl či včela – je to napsáno nejen v názvu metody, ale i jako typ parametru metody, ♦ vytváří se tím velké množství metod s různými názvy, které komplikují použití. Ve třídě Louka bude počet metod stejný, ale ten, kdo bude chtít používat tuto třídu, si bude muset pamatovat větší počet metod (místo jedné metody pridej() dvě metody pridejVcelu() a pridejMotyla()), Varianta s jednou metodou s rozskokem dle typu parametru public void pridej (Object o) { if (o instanceof Vcela) { Vcela vcela = (Vcela)o; vcely.add(vcela); } else { if (o instanceof Motyl) { Motyl motyl = (Motyl)o; motyli.add(motyl); } else { throw new InvallidArgumentException( "lze vkládat pouze motýly a včely"); } } }
16
Příznivcům bojových her doporučujeme si představovat, že programují zbrojnici, do které se vkládají meče, luky a další zbraně.
Polymorfismus a rozhraní
strana 75
Tato varianta má následující nevýhody: ♦ Metoda je poměrně složitá, je to více řádků kódu, než varianta s přetížením metod, struktura metody je složitější (přibývá rozhodování ohledně typu). ♦ Kontrola přípustných typů se přesouvá z doby překladu do běhu aplikace. Překladač povolí, aby parametrem byla instance libovolné třídy (např. String), při běhu však v tomto případě vznikne výjimka. ♦ Při použití (tj. při vkládání objektů na louku) by se měl ošetřovat vznik výjimky, tj. vzniká složitější kód i při použití této metody. Varianta s přetížením metody Varianta přidávání včel a motýlů na louku s přetížením metody bude vypadat následovně: public void pridej (Vcela vcela) { vcely.add(vcela); } public void pridej (Motyl motyl) { motyli.add(motyl); }
Všimněte si, že jsou zde dvě metody stejného jména s různým typem parametrů. Volba správné metody se provádí při překladu na základě typu parametru. Kód pro vkládání včel a motýlů by mohl vypadat následovně (nejsou uvedeny parametry konstruktoru instancí tříd Vcela a Motyl): louka.pridej(new louka.pridej(new louka.pridej(new louka.pridej(new
Vcela( Vcela( Motyl( Motyl(
...... ...... ...... ......
)); )); )); ));
Pokud si porovnáte tuto variantu s předchozími, zjistíte, že přetěžování metod zjednodušuje psaní kódu na straně používání metod i na straně třídy, která metodu poskytuje (přijímá zprávy). V diagramu na obrázku 9.1 je zobrazen vztah mezi třídou Louka a třídami Motyl a Vcela.
Obrázek 9.1 Diagram tříd zobrazující motýly a včely na louce Přetěžování metod též umožňuje omezit počet selekcí (příkazů if a switch) v programu – je to zřetelné při porovnání s předchozí variantou.
9.2. Rozhraní Pojem rozhraní se v programování používá v různých významech, nejčastější jsou následující: ♦ uživatelské rozhraní (user interface), které definuje způsob komunikace mezi uživatelem a programem. Tato problematika je velmi důležitá, ale mimo zaměření těchto skript. ♦ aplikační programové rozhraní (application programming interface, API), což je množina definicí, která určuje, jak program (část programu) může komunikovat s jiným. Je to určitá abstraktní vrstva, která je k dispozici programátorům při psaní zdrojového kódu dalších programů. Příkladem API může být rozhraní pro programátory ve Windows (Microsoft Win32
Polymorfismus a rozhraní
strana 76
API, viz příslušné části http://msdn.microsoft.com/) či rozhraní pro využívání služeb Google (Google API, viz http://www.google.com/apis/). Java má také svá API – programátor aplikace může využívat jak standardní knihovny (např. pro práci s datovými strukturami, se soubory, pro vytváření grafického rozhraní), tak velké množství knihoven a tříd, která dávají za určitých podmínek k dispozici různí programátoři a různé firmy. V Javě jsou tato aplikační rozhraní obvykle popsána v dokumentaci vygenerované programem javadoc. ♦ rozhraní (interface) jako jazyková konstrukce, která podporuje vytváření abstraktní vrstvy mezi konkrétními implementacemi a programy, které je používají. To je náplň této části kapitoly. ♦ „vzdálené rozhraní“ (remote interface) – rozhraní z programovacího jazyka na jiné systémy. V tomto smyslu se zavedla rozhraní např. do Pascalu jako rozhraní pro přístup ke komponentní technologii CORBA. V Javě se toto označení příliš nepoužívá, byť zde existují rozhraní pro vzdálené volání procedur (RMI) či rozhraní do komponentní technologie CORBA.
9.2.1. Deklarace rozhraní Deklarace rozhraní je podobná deklaraci rozhraní třídy s tím, že v rozhraní se definují pouze hlavičky veřejných metod a případně veřejné konstanty. Deklarace rozhraní se obvykle zapisuje do samostatného souboru s koncovkou .java, který se jmenuje stejně jako uvnitř uvedené rozhraní (stejné pravidlo jako pro třídy či pro výčtový typ). Obecná deklarace rozhraní vypadá takto: [public] interface Identifikátor [extends rozhraní1 [, rozhraní2 ...]] { }
Dovnitř rozhraní se zapisují pouze veřejné metody – může a nemusí být u nich uveden modifikátor public. Pokud není uveden, doplní tento modifikátor překladač, tj. nelze uvést jiné modifikátory přístupu. Následuje příklad deklarace rozhraní ObyvatelLouky, které by mělo být zapsáno v souboru ObyvatelLouky.java. Rozhraní obsahuje pouze jednu metodu. public interface ObyvatelLouky { public void jednaAkce(); }
Třída, která implementuje rozhraní, musí tuto skutečnost vyjádřit v hlavičce třídy pomocí klíčového slova implements. Za tímto klíčovým slovem se uvádějí jednotlivá rozhraní, která třída implementuje (může jich implementovat více). Třída (pokud není abstraktní, viz kapitola 11.4) musí deklarovat všechny metody předepsané v rozhraní – musí odpovídat modifikátory (tj. musí být public), musí odpovídat jméno a hlavička metody. Pokud implementujete nějaké rozhraní a neuvedete některou požadovanou metodu (např. metodu xxx()), tak překladač uvede méně srozumitelné chybové hlášení. Nevypíše, že chybí nějaká metoda požadovaná rozhraním, ale oznámí, že máte svoji třídu označit jako abstraktní, neboť není implementována metoda xxx().
Třída Motyl může schématicky vypadat následovně: public class Motyl implements ObyvatelLouky { public void jednaAkce () { if (naKvetineSNektarem()) { // sbirej nektar } else { preletni(); } } }
Polymorfismus a rozhraní
strana 77
Třída Vcela by vypadala schématicky takto: public class Vcela implements ObyvatelLouky { public void jednaAkce () { if (vUlu() && maNektar()) { // odevzdat nektar } else { if (plnyKosicek()) { // let k úlu } else { if (naKvetineSNektarem()) { // sbirej nektar } else { preletni(); } } } } }
V diagramu tříd se obvykle rozhraní zobrazuje jako třída s tím, že se před jméno třídy uvede stereotyp <>. Implementace rozhraní se zobrazuje pomocí přerušované čáry s plnou šipkou na konci.
Obrázek 9.2 Zobrazení rozhraní a implementace rozhraní v diagramu tříd V rozhraní je možné vedle metod definovat i pojmenované konstanty. Tato možnost má pouze jednu problematickou výhodu – pokud nějaká třída implementuje rozhraní s konstantami, nemusí se v ní uvádět názvy konstant společně s názvem třídy, ve které jsou konstanty definovány. Vhodnější je definovat konstanty v samostatné třídě či ještě lépe za pomocí výčtového typu.
9.2.2. Rozhraní umožňuje volbu implementace Tuto možnost využití rozhraní si ukážeme na seznamech pro ukládání prvků z balíčku java.util (blíže jsou popsány v kapitole 10.1). Pro seznamy jsou zde dvě základní implementace ArrayList a LinkedList. Třída ArrayList je rychlejší pro přístup k prvkům, třída LinkedList je rychlejší, pokud je časté vkládání prvků dovnitř seznamu či přesun/rušení prvků uvnitř seznamu. Obě dvě třídy implementují rozhraní List. Při deklaraci seznamu v programu je vhodné jako typ použít příslušné rozhraní: private List <Motyl> motyli;
a v konstruktoru potom inicializovat seznam příslušným typem: motyli = new ArrayList<Motyl>();
Použití rozhraní v typu datového atributu omezuje programátora na používání pouze těch metod, které jsou deklarovány v rozhraní. Výhodou však je, že změnou pouze na jednom řádku můžeme změnit
Polymorfismus a rozhraní
strana 78
implementaci. Pokud bychom chtěli použít LinkedList, tak stačí upravit řádek při vytváření instance seznamu: motyli = new LinkedList<Motyl>();
Tento výběr implementace samozřejmě nemusí být pouze statický v době překladu, na základě podmínky lze vybrat různé implementace za běhu aplikace: if (podmínka) { motyli = new ArrayList<Motyl>(); } else { motyli = new LinkedList<Motyl>();
9.2.3. Rozhraní a polymorfismus Pokud rozhraní implementuje více tříd, je možné ho použít jako zástupce těchto tříd. Ukážeme si to opět na příkladu s motýly a včelami. Třídy Motyl a Vcela nyní implementují rozhraní Obyvatel a je tudíž možné ve třídě popisující louku vytvořit společný seznam pro instance obou tříd: private List obyvateleLouky;
Vytvoření instance bude vypadat takto: obyvateleLouky = new ArrayList();
Nyní v této třídě stačí jedna metoda pro vkládání motýlů, včel: public void pridej (Obyvatel obyvatel) { obyvateleLouky.add(obyvatel); }
Při porovnání této metody s variantami uvedenými na začátku kapitoly je zřejmé, že toto je nejkratší a typově bezpečné řešení. Je zde ještě jedna výhoda – pokud se přidá další obyvatel louky, není potřeba upravovat zdrojový kód třídy popisující louku. Použití této varianty má také jednu nevýhodu – ve třídě popisující louku můžeme používat pro obyvatele louky pouze ty metody, které jsou definovány v rozhraní Obyvatel, tj. v našem případě volat metodu jednaAkce(). Následuje ukázka metody, která simulaci louky posune o jeden krok: public void jedenKrok() { for (Obyvatel obyvatel : obyvateleLouky) { obyvatel.jednaAkce(); } pocetKroku++; }
Tato metoda je ukázkou polymorfismu – při volání metody jednaAkce() se volá různý kód v závislosti na tom, zda konkrétní obyvatel je instance třídy Vcela či instance třídy Motyl. Využívá se zde toho, že třídy implementují stejné rozhraní, které jim předepisuje existenci metod. Pokud v našem příkladu třída implementuje rozhraní Obyvatel, musí mít metodu jednaAkce(). Pokud nějaká třída toto rozhraní neimplementuje, tak překladač zabrání umístění instance této třídy do seznamu obyvatel louky (do datového atributu obyvateleLouky). Následuje diagram tříd zobrazující vztahy mezi třídami Louka, Motyl, Vcela a rozhraním Obyvatel. Porovnejte ho s diagramem na obrázku 9.1, popisujícím obdobnou situaci bez polymorfismu. Na tomto diagramu není vztah mezi třídou Louka a konkrétními třídami Motyl a Vcela, ale kreslí se asociace od Louky k rozhraní Obyvatel.
Polymorfismus a rozhraní
strana 79
Obrázek 9.3 Diagram tříd zobrazující motýly a včely na louce s využitím polymorfismu Stejný typ polymorfismu zajišťuje i dědičná hierarchie – to si vysvětlíme v kapitole 11.5.
9.2.4. Rozhraní umožňuje předávat metody jako parametr Existence rozhraní též umožňuje předávat metodu (funkci) jako parametr či ji přiřazovat do datového atributu. Příkladem může být třídění – existuje obecný algoritmus pro třídění seznamu prvků, ale ten potřebuje vědět, jak se porovnají dva prvky (např. zda se mají dvě instance třídy Ucet porovnat dle čísla účtu či dle stavu na účtu). Tj. algoritmus pro třídění potřebuje metodu pro porovnání dvou instancí třídy Ucet. V Javě je to řešeno pomocí rozhraní – existuje rozhraní Comparator, které definuje metodu int compare(E prvni, E druhy). Programátor vytvoří pomocnou třídu, která implementuje rozhraní Comparable, ve které napíše metodu compare() na porovnání dvou účtů. Při třídění se předají statické metodě na třídění (Collections.sort()) dva parametry: vlastní seznam a instance pomocné třídy. Tím algoritmus pro třídění získá metodu, pomocí které bude porovnávat vždy dvě instance třídy Ucet. Podrobnější popis třídění a včetně příkladů je v kapitole 10.3.
9.2.5. Rozhraní a vícenásobná dědičnost Někteří autoři přistupují k rozhraní jako ke specifické verzi vícenásobné dědičnosti, neboť na rozhraní se lze dívat jako specifickou variantu abstraktní třídy – na abstraktní třídu, která definuje pouze veřejné abstraktní metody. Např. jazyk C++ nepodporuje rozhraní, ale podporuje vícenásobnou dědičnost. Ve prospěch rozhraní zaznívají tyto argumenty: ♦ S využitím rozhraní je návrh jednodušší, neboť je menší riziko vzniku kolizí jmen, kdy dva různí předci (dvě různá implementovaná rozhraní) definují metody stejného jména s různým významem. Je to dáno tím, že v abstraktních třídách může být více prvků, které mohou kolidovat – např. datové atributy či metody podporující implementaci. ♦ Jazykový element rozhraní více podporuje správný návrh API – zapouzdření a znovupoužitelnost – neboť programátor má menší možnost se v této fázi zabývat implementací. Použití abstraktních tříd programátora více svádí zabývat se implementací, neboť může do nich zapisovat datové atributy či privátní metody, které patří do implementace API. Preference rozhraní před abstraktními třídami v Javě se prosazovala postupně – je to vidět při porovnání nejstarších částí, které preferují abstraktní třídy (např. třídy pro vstup/výstup v balíčku java.io) s novějšími (např. dynamické datové struktury v balíčku java.util).
9.2.6. Dědičnost mezi rozhraními Existuje i dědičnost mezi rozhraními – jedno rozhraní může dědit definice od jiného, popř. jiných rozhraní. V hlavičce rozhraní se dědičnost rozhraní vyznačí klíčovým slovem extends, za kterým se zapisuje jedno či více rozhraní. Ukážeme si to na příkladu třídy AbstractList, jejíž předci, potomci a implementovaná rozhraní jsou zobrazeny na obrázku 9.4. Můžete zde vidět dědičnost mezi rozhraními – rozhraní List je potomkem rozhraní Collection (které je potomkem rozhraní Iterable). Hlavička rozhraní List vypadá následovně (nejsou uvedeny generické typy): public interface List extends Collection {
Polymorfismus a rozhraní
strana 80
Obrázek 9.4 Vztah mezi třídami a rozhraními v části balíčku java.util – AbstractList a potomci Na obrázku není z důvodů přehlednosti vyznačeno, že třídy implementují též rozhraní Cloneable a Serializable.
Datové struktury
strana 81
10.Datové struktury Tato kapitola popisuje, jak uchovávat více instancí stejného typu (stejné třídy) nebo jeho podtypů. Vzhledem k tomu, že je to poměrně častá úloha, většina programovacích jazyků podporuje seskupování dat stejného typu. V Javě jsou k dispozici tyto základní datové struktury: ♦ kolekce (collections) – se používají pro uchování více prvků stejného typu. Podporují snadné přidávání a ubírání prvků. Kolekce se dělí do následujících tří skupin: * seznamy (lists) – se nejčastěji používají pro uložení prvků. Prvky se stejným obsahem (hodnotou) mohou být v seznamu vícekrát, seznamy udržují pořadí vkládání prvků. Příkladem může být seznam tiskáren, seznam studentů či seznam věcí v místnosti. * množiny (sets) – mají proti seznamům dvě odlišnosti: každý prvek lze do seznamu uložit pouze jednou a množiny neudržují pořadí prvků. Výhodou je rychlejší zjišťování, zda prvek již v seznamu je. Množiny lze použít např. pro udržování seznamu typů tiskáren či v nějaké karetní hře pro udržování seznamu karet v ruce hráče. * fronty (queues) – jsou určeny pro ukládání prvků před dalším zpracováním. Vedle klasických front FIFO Java podporuje i fronty LIFO, fronty s prioritou, fronty se zpožděním či fronty s omezeným přístupem. Fronty nebudou v těchto skriptech dále popisovány. ♦ mapy (maps) – na rozdíl od seznamů se pracuje s dvojicí prvků. První prvek se nazývá klíč a musí být jedinečný, druhý prvek se nazývá hodnota. Výhodou mapy je, že vyhledávání dle klíče je rychlé. Příkladem je frekvenční analýza slov v textu – klíčem bude jednotlivé slovo, hodnotou bude počet výskytů. Dalším příkladem může být seznam bankovních účtů, ke kterým chceme přistupovat rychle pomocí čísla účtu – číslo účtu bude klíč, vlastní účet bude hodnotou. Složitějším příkladem může být přehled známek studentů, kde klíčem bude student a hodnotou je seznam (list) obsahující jednotlivé známky. ♦ pole (array) – představuje seznam s pevným počtem prvků. Podobá se seznamům, ale proti seznamům je však do pole mnohem obtížnější prvky přidávat či ubírat. Pole má i své výhody – je trochu rychlejší a podporuje vkládání primitivních datových typů bez obalování. Používá se v situacích, kdy se nepředpokládá přidávání/ubírání prvků seznamu – seznam parametrů na příkazové řádce, slovo převedené na jednotlivé znaky, seznam nějakých hodnot pro jednotlivé měsíce v roce, atd. Java podporuje i vícerozměrná pole – proto se používá např. pro vyjádření šachovnice či tabulek pevné velikosti. ♦ výčtový typ (enum) – používá se pro uložení pevného seznamů konstantních (neměnných) prvků, každá změna v seznamu znamená nový překlad aplikace. Příkladem může být seznam dnů v týdnu, seznam planet ve sluneční soustavě, seznam barev v kartách. Výčtový typ byl popsán v kapitole 8. Vedle těchto základních struktur je možné najít v knihovnách Javy další datové struktury, uživatel si může také vytvářet vlastní struktury instancí. V projektu Škola je ukázána možnost, jak vytvořit stromovou datovou strukturu pro uložení hierarchické organizační struktury.
10.1. Collections framework V knihovnách Javy je k dispozici skupina tříd a rozhraní, která je označována jako Collections Framework. Jsou součástí standardního API a jsou umístěny v balíčku java.util. Framework se skládá z následujících součástí: ♦ rozhraní – abstraktní datové typy představující jednotlivé druhy kontejnerů. Umožňují manipulovat s kontejnery nezávisle na konkrétní implementaci. ♦ konkrétní implementace – konkrétní implementace rozhraní, připravené k použití. ♦ algoritmy – konkrétní statické metody např. pro setřídění jednotlivých struktur. Tyto metody jsou soustředěny ve třídách Collections a Arrays (tato třída je určena pro práci s poli).
Datové struktury
strana 82
Ve verzi 5.0 Javy se datové struktury deklarují s využitím tzv. generických datových typů, pomocí kterých se určuje typ objektů, které lze vkládat do vytvořených instancí datových struktur. Teorií a deklarací generických datových struktur se v těchto skriptech nebudeme zabývat, v této kapitole si ukážeme používání generických typů v souvislosti s datovými strukturami.
10.2. Základní rozhraní Následující obrázek obsahuje základní přehled rozhraní pro datové struktury (jsou vynechány fronty) a typické implementace těchto rozhraní.
Obrázek 10.1 Přehled základních rozhraní a konkrétních implementací kolekcí a map
10.3. Kolekce Rozhraní Collection reprezentuje skupinu objektů (instancí) označovaných jako prvky. Jsou zde deklarovány základní operace jako je vložení prvku, zrušení prvku či zjištění počtu prvků. Z variant seskupení prvků budeme popisovat seznamy definované rozhraním List a množiny definované rozhraním Set. V následující tabulce jsou uvedeny nejpoužívanější metody rozhraní Collection (tyto metody mají všechny konkrétní implementace rozhraní, tj. seznamy i množiny). metoda
užití
boolean add(E element)
Přidá do kolekce prvek typu E. Pokud se operace nepodaří, vrací metoda hodnotu false.
void clear()
Zruší všechny prvky v kolekci.
boolean contains(Object o)
Vrací true, jestliže kolekce obsahuje prvek uvedený jako argument metody.
boolean isEmpty()
Vrací true, jestliže je kolekce prázdná.
Iterator <E>iterator()
Vrací instanci rozhraní Iterator, pomocí které je možno procházet kolekci
boolean remove(Object o)
Jestliže kolekce obsahuje argument, je odpovídající prvek zrušen. Vrací true, když je prvek zrušen.
int size()
Vrací počet prvků uložených v kolekci.
Tabulka 10.1 Přehled nejpoužívanějších metod rozhraní Collection
Datové struktury
10.3.1.
strana 83
Seznamy (List)
Rozhraní List reprezentuje seznamy, které mohou obsahovat libovolný počet prvků zadaného typu. Seznamy udržují pořadí vkládaných prvků, umožňují vložit stejný prvek vícekrát. Seznamy umožňují přístup k jednotlivým položkám také pomocí indexů. Tyto indexy jsou typu int a jsou v rozsahu 0 až n-1, kde n je aktuální počet prvků seznamu. Při vkládání lze prvky přidávat na konec nebo vkládat na zadanou pozici – přitom se prvek na této pozici a všechny následující posunou o jedno místo. Prvky lze rušit buď ze zadané pozice, nebo na základě rovnosti se zadaným parametrem (tj. lze zadat instanci, která se má zrušit). Při zrušení prvku se za ním uložené prvky posouvají o jedno místo doleva. Java nabízí dvě základní implementace seznamů a to třídy ArrayList a LinkedList. Většinou se používá ArrayList, v případě větších změn uvnitř seznamu (vkládání dovnitř seznamu, rušení prvků ze seznamu) je výhodnější používat LinkedList. Všechny implementace rozhraní List mají metody definované pro Collection (předchozí tabulka) a navíc metody využívající indexy, uvedené v následující tabulce. metoda
užití
void add(int index, E element)
Přidá do kolekce na zadanou pozici prvek typu E.
E get(int index)
Získání prvku na zadané pozici v seznamu.
E remove(int index)
Zruší prvek ze zadané pozice seznamu, tento prvek vrací jako návratovou hodnotu.
Tabulka 10.2 Přehled nejpoužívanějších metod rozhraní List Při vytváření kolekce je třeba určit, jakého typu budou vkládané prvky. Typ prvků se v Javě 5.0 uvádí v ostrých závorkách. Pro typ prvků je jedno omezení – lze vkládat pouze referenční typy, nelze tedy vytvořit kolekci obsahující prvky typu int. Pokud chceme do kolekce ukládat prvky primitivního typu, musíme použít obalový typ např. Integer. Při vkládání jednotlivých hodnot a při práci s nimi se využijí automatické konverze mezi primitivními typy a jejich obalovými třídami. Pokud pro seznam hodnot typu String použijeme třídu ArrayList, může deklarace a inicializace vypadat takto: List<String> seznam = new ArrayList<String>();
Jednotlivé prvky se vkládají pomocí metody add(), překladač kontroluje, zda vkládaný prvek je požadovaného typu. Následující kód ukazuje, jak do výše deklarovaného seznamu přidat tři prvky: seznam.add("pes"); seznam.add("kočka"); seznam.add("pes");
Prvky jsou v seznamu uloženy v tom pořadí, v jakém se vkládaly. Mají též přiřazeny indexy, určující jejich pozici – pozice se číslují od nuly. List seznam 0 1 2
String “pes”
String “kočka”
String “pes”
Obrázek 10.2 Objekty uložené v seznamu
Datové struktury
strana 84
Ve třídě ArrayList, stejně jako v ostatních kontejnerech, je překryta metoda toString(). Pro jednoduchý výpis obsahu seznamu je tedy možné použít následující příkaz: System.out.println(seznam);
Výsledkem tohoto příkazu je následující výpis: [pes, kočka, pes]
10.3.2.
Množiny (Set)
Rozhraní Set popisuje datové struktury, které mohou obsahovat libovolný počet prvků zadaného typu, a které (na rozdíl od seznamu) neumožňují vkládat duplicity (vložit dvakrát stejnou instanci či vložit dvě instance, které jsou si rovny). Existují dvě základní implementace, a to HashSet a TreeSet. Při ukládání prvků do HashSet nelze ovlivnit pořadí prvků – neplatí, že posledně vložený prvek je uložen nakonec. TreeSet udržuje své prvky trvale setříděny. Objekty vkládané do instance třídy TreeSet musí mít naimplementováno rozhraní Comparable (viz dále v této kapitole), neboť metoda compareTo() tohoto rozhraní se používá pro zjištění duplicit a při zatřiďování prvků17. Rozhraní Set nerozšiřuje množství metod, které jsou určeny rozhraním Collection. Při vytváření množin postupujeme obdobně jako u seznamů – při ukládání se navíc kontroluje duplicita vkládaného prvku. Při vložení duplicitní hodnoty nevznikne žádná chyba (nevznikne výjimka), ale ani se duplicitní hodnota nevloží. Pokud se nepodaří objekt vložit, vrátí metoda add() hodnotu false. Ukážeme si to v následující části kódu: Set<String> mnozina = new HashSet<String>(); mnozina.add("pes"); mnozina.add("kočka"); mnozina.add("pes"); mnozina.add("morče"); System.out.println(mnozina);
Výsledkem tohoto kódu je následující výpis: [pes, morče, kočka]
Obrázek 10.3 Objekty uložené v množině (HashSet) Každá implementace rozhraní Set musí zajistit, aby se nevkládaly duplicity. U častěji používané množiny HashSet se rovnost objektů zjišťuje pomocí metod hashCode() a equals(), které jsou popsány v kapitole 6. Pokud u vkládané instance metoda hashCode() vrátí číslo odlišné od kódů již vložených instancí, považuje se za jedinečnou. Pokud již v množině existuje instance vracející stejný hashCode(), porovnává se s nimi vkládaná instance pomocí metody equals(). Následuje 17
Pokud je v konstruktoru třídy TreeSet uvedena implementace rozhraní Comparator, tak vkládané prvky nemusí implementovat rozhraní Comparable, ale musí být konzistentní s použitou implementací rozhraní Comparator.
Datové struktury
strana 85
schématické zobrazení uložení prvků v HashSet – pokud prvky vracejí stejný hashCode(), uloží se do spojitého seznamu. 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
Alena
Drahomir
Dušan
Hana Iva Jan
Helena Jarmila
Jaroslav
Jiří Josef
Libor Martina Norbert Ota
Luboš
Renata
Renáta
Obrázek 10.4 Schématické zobrazení HashSetu – pro stejnou hodnotu hashCode() se prvky ukládají do seznamu Ze způsobu uložení vyplývají tyto pravidla: ♦ Jednotlivé objekty by měly vracet pokud možno odlišný hashCode(), neboť dle hashCode() se vyhledává mnohem rychleji než poté následně pomocí equals() ve spojitém seznamu. Z tohoto hlediska není algoritmus pro výpočet hashCode() v příkladu na obrázku zvolen příliš vhodně – seznam pro hashCode() 74 je poměrně dlouhý. ♦ Dvě instance, které jsou si rovny dle metody equals(), musí vracet stejný hashCode() – jinak hrozí, že se uloží duplicitně. ♦ Metoda hashCode() by měla vracet stejnou hodnotu po celou dobu existence instance, obdobně metoda equals() by pro porovnání měla používat datové atributy, které se v průběhu existence instance nemění. Vkládané objekty by měly mít dobře naprogramované metody hashCode() a equals().
10.3.3.
Procházení kolekce (rozšířený příkaz for)
Pro postupné procházení jednotlivých prvků kolekce se používá speciální syntaxe cyklu for, která bývá označována jako „for each“. Syntaxe této varianty cyklu for je následující: for (Typ identifikátor : kolekce) { příkazy; }
První parametr specifikuje typ a identifikátor proměnné, do které budou postupně přiřazovány jednotlivé prvky kolekce, která je uvedena jako druhý parametr. Oddělovačem je v tomto případě dvojtečka. První parametr by měl být toho typu, s jakým je deklarována kolekce.
Datové struktury
strana 86
V následujícím příkladě si ukážeme jak vypsat jednotlivé prvky seznamu na samostatné řádky. Použijeme opět náš seznam zvířat. for (String zvire : seznam){ System.out.println(zvire); }
Cyklus vypíše následující údaje: pes kočka pes
Kód na procházení množiny je obdobný: for (String zvire : mnozina){ System.out.println(zvire); }
Výsledek bude: pes kočka morče
Procházení kolekcí pomocí cyklu for skrývá jednu záludnost – při procházení (tj. v těle cyklu) nelze mazat prvky kolekce. Pokud je potřeba při procházení kolekce mazat prvky, musí se použít iterátor, který popíšeme v kapitole 10.3.5.
10.3.4.
Používání indexů u seznamů
Jak již bylo uvedeno, seznamy (třídy implementující rozhraní List) mají možnost vyžívat indexy jednotlivých položek. Indexy začínají od nuly a postupně se zvyšují do velikosti seznamu zmenšeného o jedničku (seznam.size() – 1).
Obrázek 10.5 Struktura objektů v seznamu s vyznačením indexů Ze seznamu zobrazeného na obrázku 10.5 získáme druhý prvek seznamu (tj. prvek s indexem 1) příkazem: String zvire = seznam.get(1);
Pro vložení prvku do seznamu můžeme použít i variantu metody add(), která vloží prvek na zadanou pozici v seznamu. Následující kód ukazuje, jak na první místo v seznamu vložit další prvek. seznam.add(0, "morče");
Při vložení prvku na první místo se ostatní prvky posunou – zvýší se jejich indexy o jedničku.
Datové struktury
strana 87
Obrázek 10.6 Struktura objektů v seznamu po přidání morčete Podobně jako lze vkládat instance na určené místo v seznamu, umožňuje metoda remove() rušit prvky na místě určeném konkrétním indexem. Metoda remove() vrací rušený prvek jako návratovou hodnotu metody. Následující kód zruší prvek s indexem 1 (rušený prvek dále nepotřebujeme, tudíž vracenou hodnotu nepřiřazujeme do žádné proměnné): seznam.remove(1);
Obrázek 10.7 Struktura objektů v seznamu po smazání prvku s indexem 1 Vzhledem k tomu, že List poskytuje přístup k položkám i prostřednictvím indexu, je možno použít také standardní podobu cyklu for. Použití tohoto cyklu má smysl v situaci, kdy chceme pracovat i s indexem prvku v seznamu. V následujícím příkladě vypisujeme jednotlivé prvky seznamu na samostatné řádky včetně čísla indexu (používá se formátování pomocí metody printf() – viz popis třídy String). for (int i = 0; i < seznam.size(); i++) { System.out.printf ("%2d. %s",i,seznam.get(i)); }
Výstup bude následující: 0. morče 1. kočka 2. pes
Ani při použití klasického cyklu for a indexů nelze při procházení seznamu (uvnitř těla cyklu for) mazat prvky seznamu – je potřeba použít iterátor.
10.3.5.
Používání iterátoru
V Javě 5.0 se pro procházení kolekcí obvykle používá příkaz for(obě jeho varianty). Lze však použít i starší způsob – procházet kolekci pomocí iterátoru, instance implementující rozhraní Iterator. Proti příkazu for má jedinou výhodu – při procházení pomocí iterátoru lze rušit položky v příslušné kolekci.
Datové struktury
strana 88
Iterátor si můžeme představit jako ukazovátko, pomocí kterého se postupně ukazuje na prvky seznamu. Instanci iterátoru získáme pomocí metody iterator() příslušné kolekce. Iterátor má tři operace: metoda
užití
boolean hasNext()
Vrací true, pokud iterátor ukazuje na prvek v kolekci. Pokud se dojde na konec kolekce, vrací hodnotu false.
E next()
Vrátí prvek, na který ukazuje iterátor a posune iterátor na další prvek v seznamu.
void remove()
V kolekci zruší prvek, který byl naposledy vrácen metodou next(). Po next() může být volána metoda remove() pouze jednou.
Tabulka 10.3 Přehled metod rozhraní Iterator Při deklaraci iterátoru je potřeba uvést typ prvků v kolekci, která se bude procházet. Deklarace a vlastní procházení může vypadat takto: Iterator<String> iter = seznam.iterator(); while (iter.hasNext()) { System.out.println(iter.next()); }
Na první pohled je zřetelné, že procházení pomocí cyklu for je přehlednější. Iterátor má ve verzi 5.0 jedinou výhodu – umožňuje mazat prvky v průběhu procházení kolekce. Následuje schématický příklad, který ze seznamu vymaže zvířata, která nemám doma. Iterator<String> iter = seznam.iterator(); while (iter.hasNext()) { String zvire = iter.next(); if (! mamDoma(zvire) { iter.remove(); } }
10.3.6.
Prohledávání kolekce
Všechny kolekce mají metodu contains(), která zjišťuje, zda je v kolekci obsažen aspoň jeden prvek shodný s parametrem. Porovnávání probíhá na základě metody equals(), tj. instance vkládané do kolekce musí mít správně implementovánu metodu equals(). Mnoho standardních tříd má metodu equals() již implementovánu, pro seznam instancí třídy String by vyhledávání v kolekci mohlo vypadat takto: if (seznam.contains("morče")) { // příkazy }
Pokud potřebujeme rychlé vyhledávání a instance se neopakují, je nejvhodnější použít pro ukládání instancí třídu HashSet. Někdy je vyhledávání složitější – příkladem může být vyhledávání účtu dle čísla účtu. Abychom mohli ukládat účty do HashSet, doplníme do třídy Ucet metodu equals() – dva účty si budou rovny, pokud budou mít stejné číslo účtu:
Datové struktury
strana 89
public class Ucet { // dosavadní obsah třídy public boolean equals (Object o) { if (o instanceof Ucet) { Ucet druhy = (Ucet)o; return cislo == druhy.cislo; } else { return false; } } }
Nyní v jiné třídě vytvoříme kolekci seznamUctu, do které budeme vkládat jednotlivé účty. Tuto třídu rozšíříme i o metodu existujeUcet(), která bude vracet true, pokud v seznamu účtů existuje účet se zadaným číslem. Pro vyhledání je potřeba vytvořit pomocnou instanci třídy Ucet, s jejíž pomocí budeme vyhledávat. Ukázka kódu obsahuje také metodu getUcet(), která vrátí účet se zadaným číslem: private Collection seznamUctu = new HashSet(); public boolean existujeUcet(int cisloUctu) { Ucet pomocny = new Ucet(cisloUctu, "pomocny"); return seznamUctu.contains(pomocny); } public Ucet vratUcet(int cisloUctu) { for (Ucet ucet : seznamUctu) { if (ucet.getCislo() == cisloUctu) { return ucet; } } return null; }
V těchto situacích bývá výhodnější použít pro uložení místo kolekce mapu – to si ukážeme v podkapitole 10.4.2.
10.3.7.
Třídění kolekcí
Častou operací je třídění kolekcí podle různých kritérii. K tomu, abychom mohli třídit, musíme splnit dva předpoklady: ♦ mít k dispozici metodu, která porovná dva prvky seznamu a zjistí, který prvek je větší a který menší (který má být dříve v seznamu, který později). Existují dvě základní možnosti: * tzv. přirozené řazení – třída, jejíž instance se mají třídit, musí implementovat rozhraní Comparable, * vytvořit speciální třídu implementující rozhraní Comparator. Tato třída je „nezávislá“ na třídě, jejíž instance se mají třídit. Toto řešení umožňuje mít více řadících pravidel pro stejné instance (jednou se seznam účtů setřídí dle čísla účtu, podruhé dle hodnoty uložených peněz), ♦ mít k dispozici třídící algoritmus. Pro jednotlivé typy datových struktur jsou k dispozici různé možnosti: * pro setřídění seznamů (implementací List) se používá statická metoda Collections.sort(),
Datové struktury
strana 90
*
pro setřídění polí (array – budou vysvětleny na konci kapitoly) se používá Arrays.sort(), * množiny (Set) nelze obecně třídit, TreeSet jako jedna z implementací udržuje prvky automaticky setříděné, * mapy (Map) též nelze třídit, obdobně jako u množin existuje implementace TreeMap, která automaticky udržuje vložené prvky setříděné, * ve třídě Collections jsou též metody max() a min(), které vyhledají největší/nejmenší hodnotu v kolekci (v seznamech i v množinách), * obdobně pro pole (array) jsou ve třídě Arrays metody max() a min() pro vyhledání největší/nejmenší hodnoty. Pokud se má použít přirozené řazení (natural ordering), musí třída, jejíž instance se mají třídit, implementovat rozhraní Comparable, které předepisuje jedinou metodu, a to public int compareTo (T o). V této metodě se porovnává aktuální instance s instancí, předanou jako parametr. Metoda by měla vracet nulu v případě, že jsou si instance rovny (tj. na pořadí těchto dvou instancí v setříděném seznamu nezáleží). Záporné číslo je vráceno v případě, že aktuální instance je „menší“ než instance uvedená jako parametr a kladné číslo se vrací v opačném případě. Výsledky metody compareTo() by měly být konzistentní s metodou equals() – jsou-li si dvě instance této třídy rovny na základě volání metody equals(), měla by metoda compareTo() vrátit hodnotu nula. V mnoha standardních třídách je již rozhraní Comparable implementováno – instance třídy String se řadí vzestupně dle abecedy (anglické), instance obalových tříd se řadí od nejmenšího čísla k největšímu. My si ukážeme implementaci přirozeného řazení na příkladu s účty – účty se budou řadit vzestupně dle čísla účtu. V deklaraci třídy se musí uvést, že třída implementuje rozhraní Comparable a současně typ instancí, se kterými se budou instance této třídy porovnávat (obvykle se porovnává pouze s instancemi stejné třídy). V následujícím kódu vidíte hlavičku třídy a implementaci metody compareTo(). public class Ucet implements Comparable{ private int cisloUctu; private String vlastnik; private double stav = 0; // část kódu není uvedena public int compareTo(Ucet druhyUcet){ if (this.cisloUctu == druhyUcet.cisloUctu){ return 0; } else { if (this.cisloUctu < druhyUcet.cisloUctu){ return -1; } else { return 1; } } } // další metody třídy Ucet
Vlastní setřídění instancí uložených v seznamu (List) je nyní již velmi jednoduché – zavolá se statická metoda sort() ze třídy Collections: Collections.sort(seznamUctu);
Datové struktury
strana 91
Pokud používáme množiny (implementace rozhraní Set) a chceme prvky setřídit, musíme použít implementaci TreeSet, která automaticky vkládané prvky třídí. Zde platí i obrácené omezení, do TreeSet lze ukládat pouze instance, které implementují rozhraní Comparable nebo které jsou konzistentní s implementací rozhraní Comparator použitou při vytváření instance TreeSet. Obdobně je to s ukládáním klíčů do TreeMap. Implementací rozhraní Comparable můžeme řadit instance pouze dle jednoho kritéria. Když budeme chtít setřídit účty podle jmen vlastníků (podle abecedy), musíme napsat řazení s využitím rozhraní Comparator. Pro třídění instancí jedné třídy lze vytvořit libovolný počet „pomocných tříd“, které implementují rozhraní Comparator. Každá z těchto implementací může řadit instance podle jiného kritéria. Nyní si ukážeme, jak může vypadat pomocná třída implementující rozhraní Comparator, která bude řadit účty dle vlastníka účtu („podle abecedy“). import java.util.Comparator; class PorovnavaniUctuDleAbecedy implements Comparator {
}
public int compare (Ucet prvni, Ucet druhy){ String vlastnikPrvni = prvni.getVlastnik(); String vlastnikDruhy = druhy.getVlastnik(); return vlastnikPrvni.compareTo(vlastnikDruhy); }
Pro vlastní setřídění seznamu účtů (typu List) zavoláme metodu sort() ze třídy Collections takto: Collections.sort(seznamUctu, new PorovnavaniUctuDleAbecedy());
Při porovnávání dvou instancí třídy Ucet se volá metoda compare() uvedená ve třídě PorovnavaniUctuDleAbecedy. Námi vytvořenou implementaci rozhraní Comparator je možné použít i při řazení prvků v množině TreeSet. Množinu účtu trvale setříděnou abecedně podle vlastníka vytvoříme takto: Set mnozinaUctu = new TreeSet(new PorovnavaniUctuDleAbecedy());
Použití tohoto řazení u TreeSet má však jeden nepříjemný vedlejší důsledek – do takto definované množiny nelze vložit účty dvou vlastníků stejného jména (či dva účty stejného vlastníka). TreeSet patří mezi množiny a již z definice množiny nelze vložit duplicity. U TreeSet se při přirozeném řazení rovnost zjišťuje pomocí metody compareTo()18, při použití implementace rozhraní Comparator se rovnost zjišťuje pomocí metody compare(). Přirozené řazení i řazení přes implementaci rozhraní Comparator je možno využít v dalších metodách třídy Collections. Jsou zde např. definovány metody max() a min(), které vrátí prvek kolekce s největší či nejmenší hodnotou. Pokud je při volání metod max() nebo min() jako parametr uvedena pouze kolekce, pro vyhledání extrému se použije přirozené řazení pomocí metody compareTo(). Vyhledání účtu s nejvyšším číslem účtu je jednoduché: Ucet ucetSNejvyssimCislem = Collections.max(seznamUctu);
Metody max() a min() jsou přetížené, jako druhý parametr je možno uvést implementaci rozhraní Comparator. Pokud budeme chtít vybrat ze seznamu účtů účet s nejvyšší uloženou částkou, potřebujeme následující implementaci rozhraní Comparator:
18
Toto je hlavní důvod, proč by metody equals() a compareTo() měly být konzistentní – aby při ukládání instancí do množiny byla identifikace duplicit stejná pro implementace HashSet() i TreeSet().
Datové struktury
strana 92
import java.util.Comparator; class PorovnavaniUctuDleStavu implements Comparator {
}
public int compare (Ucet prvni, Ucet druhy){ double stavPrvni = prvni.getStav(); double stavDruhy = druhy.getStav(); if (stavPrvni == stavDruhy){ return 0; } else { if (stavPrvni > stavDruhy){ return 1; } else { return -1; } } }
Spuštění metody pro vyhledání účtu s nejvyšší uloženou částkou bude vypadat následovně: Ucet ucetSNejvyssimStavem = Collections.max(seznamUctu, new PorovnavaniUctuDleStavu());
Pokud existuje více účtů se stejným nejvyšším stavem, vrátí se pouze jeden z nich. metoda
užití
static T max(Collection col)
Vrátí maximální (nejvyšší) prvek z kolekce dle přirozeného řazení. Instance v kolekci musí implementovat rozhraní Comparable.
static T max(Collection col, Comparator comp)
Vrací maximální (nejvyšší) prvek z kolekce v řazení dle komparátoru. Komparátor musí řadit typ instancí uložených v kolekci.
static T min(Collection col)
Vrátí minimální (nejmenší) prvek z kolekce dle přirozeného řazení. Instance v kolekci musí implementovat rozhraní Comparable.
static T min(Collection col, Comparator comp)
Vrátí minimální (nejmenší) prvek z kolekce v řazení dle komparátoru. Komparátor musí řadit typ instancí uložených v kolekci.
static int frequency(Collection col, Object o)
Zjišťuje, kolik je v kolekci shodných prvků (dle metody equals()) se zadanou instancí.
static void reverse(List list)
Obrátí pořadí prvků v seznamu.
static void shuffle(List list)
Náhodně zamíchá prvky v zadaném seznamu.
static void sort(List list)
Setřídí prvky v seznamu dle přirozeného řazení. Instance v seznamu musí implementovat rozhraní Comparable.
static void sort(List list, Comparator comp)
Setřídí prvky v seznamu dle zadaného komparátoru. Komparátor musí řadit typ instancí uložených v seznamu.
static void swap (List list, int i, int j)
Zamění v seznamu prvky na indexu i a na indexu j.
Datové struktury
strana 93
metoda
užití
static Comparator reverseOrder()
Vrátí komparátor, který bude řadit obráceně vzhledem k přirozenému řazení.
static Comparator reverseOrder(Comparator comp)
K zadanému komparátoru vrátí komparátor, který bude třídit v obráceném pořadí.
static Collection synchronizedCollection(Collection col)
K zadané kolekci vrátí kolekci, kterou je bezpečné používat ve vláknech. Existují obdobné metody pro implementace List, Set, Map a SortedMap.
static Collection unmodifiableCollection(Collection col)
K zadané kolekci vrátí kolekci, kterou nelze již upravovat (přidávat/ubírat prvky). Existují obdobné metody pro implementace List, Set, Map a SortedMap.
Tabulka 10.4 Metody třídy Collections
10.4. Mapy (Map) Do mapy se ukládá dvojice objektů: klíč a k němu přiřazená hodnota. Příkladem mapy může být telefonní seznam, ve kterém bude klíčem tel. číslo (jedinečné) a k němu jméno osoby19. V aplikaci počítající četnost slov v textu bude klíčem slovo a hodnotou počet výskytů. Často se mapa používá i v situaci, kdy chceme zajistit rychlý přístup k prvkům seznamu dle klíče – např. můžeme vytvořit mapu, kde klíčem bude číslo účtu a hodnotou bude instance třídy Ucet. Použití map má jedno logické omezení – v mapách nemohou být duplicitní klíče. Mapy se používají i v jiných jazycích, používá se ale často odlišné označení: asociativní pole, slovník (dictionary), hašovací tabulka (ve verzi 1.0 Javy byla k dispozici mapa pojmenovaná HashTable, která je stále k dispozici z důvodu zpětné kompatibility). Základní funkčnost mapy je definována v rozhraní Map, v následující tabulce jsou popsány základní metody: metoda
užití
V get(Object key)
Vrací hodnotu odpovídající zadanému klíči.
V put(K key, V value)
Vloží klíč a hodnotu do mapy. Pokud již klíč v mapě je, přepíše se pouze hodnota.
V remove(Object k)
Vrací hodnotu odpovídající zadanému klíči a zároveň v mapě ruší odpovídající záznam.
int size()
Vrací počet prvků uložených v mapě.
boolean isEmpty()
Vrací true, jestliže je mapa prázdná.
void clear()
Zruší všechny prvky v mapě.
boolean containsKey(Object key)
Pokud je klíč obsažen v mapě, vrací true.
boolean containsValue(Object value)
Vrací true, jestliže mapa obsahuje hodnotu uvedenou jako parametr metody.
Set keySet()
Vrací množinu (Set) obsahující klíče.
19
Pokud na jednom tel. čísle bude více osob, tak se buď jejich jména spojí do jednoho řetězce či se použije deklarace mapy, která bude mít jako hodnotu seznam (List) obsahující jednotlivé osoby.
Datové struktury
strana 94
metoda
užití
Collection values()
Vrací kolekci (Collection) hodnot.
Set<Map.Entry> entrySet()
Vrací množinu (Set) s prvky Map.Entry.
Tabulka 10.5 Přehled nejpoužívanějších metod rozhraní Map V Javě jsou dvě univerzální implementace mapy – HashMap a TreeMap – a několik speciálních implementací. HashMap je rychlejší, TreeMap automaticky třídí klíče. Při deklaraci mapy a při volání konstruktoru je možné (a velmi vhodné) určit typy klíčů a typy hodnot. Následuje příklad mapy, kde klíčem bude řetězec a hodnotou počet výskytů – při deklaraci čísel musíme použít obalovou třídu. private Map <String, Long> pocetSlov = new HashMap<String, Long>();
Mapa s telefonním seznamem, kde klíčem je telefonní číslo a hodnotou informace o osobě (instance třídy Osoba), může být deklarována a inicializována takto: private Map telSeznam = new HashMap();
Třída, jejíž instance se vkládají jako klíče, by měla implementovat metody hashCode() a equals(), platí stejná pravidla jako pro instance vkládané do množiny HashSet. Pro ilustraci používání map si uvedeme jednoduchou mapu, jejímiž prvky budou údaje o domácích zvířatech, jejich druh a počet jedinců tohoto druhu. Deklaraci takové mapy provedeme následovně: HashMap <String, Integer> mapa = new HashMap<String, Integer>();
Mapu naplníme následujícími údaji: mapa.put("pes", 3); mapa.put("pes", 2); mapa.put("kočka", 1); mapa.put("morče", 1);
Stejně jako u kolekcí i u map je možné pro jednoduchý výpis obsahu mapy použít příkaz: System.out.println(mapa);
Výsledkem bude následující výpis: {pes=2, morče=1, kočka=1}
Jak si můžete všimnout, při vkládání dvojic se shodnými klíči dochází k přepsání původní hodnoty. V naší mapě to znamená, že původní hodnota 3 ke klíči „pes“ byla přepsána později vkládanou hodnotou 2. Uvědomte si též, že při vkládání čísel se uplatňuje autoboxing – automatický převod primitivních čísel na obalovou třídu Integer. I v následujícím kódu na zvětšení počtu psů o jednoho se několikrát uplatní autoboxing20: mapa.put("pes", mapa.get("pes") + 1);
Pro zjišťování jedinečnosti klíče se v HashMap využívají metody hashCode() a equals(). V TreeMap se pro porovnání a zatřídění klíčů používá metoda compareTo() – instance vkládané jako klíče musí implementovat rozhraní Comparable. Mapy poskytují pro vyhledávání dvě metody: containsKey() pro vyhledávání v klíčích a containsValue() pro vyhledávání mezi uloženými hodnotami. Hledání přes klíče je ve většině situací rychlejší. 20
Díky autoboxingu je tento kód relativně pomalý. Pokud by vkládání do mapy a přičítání hodnot bylo kritickým místem aplikace, lze tyto operace optimalizovat za cenu menší přehlednosti kódu.
Datové struktury
strana 95
String “morče“
Integer “1“
String “kočka”
Integer “1“
String “pes”
Integer “2“
klíče
hodnoty
Obrázek 10.8 Zobrazení mapy – vztah klíčů a hodnot
10.4.1.
Procházení mapy
Mapa se obvykle prochází přes množinu klíčů, ke které získáme přístup pomocí metody keySet(). Hodnotu ke klíči získáme pomocí metody get(). Následuje ukázka procházení naší mapy obsahující údaje o zvířatech pomocí cyklu for: public void vypisMapy(){ Set<String> mnozinaKlicu = mapa.keySet(); for (String klic : mnozinaKlicu){ System.out.println(klic+"\t"+mapa.get(klic)); } }
Výsledek výpisu metody bude vypadat takto: pes morče kočka
3 1 1
Mapu lze procházet též pomocí iterátoru – smysl to má však pouze v situaci, kdy chceme při průchodu rušit prvky v mapě. V následujícím kódu zrušíme z mapy všechny druhy, od kterých nemáme doma alespoň jednoho jedince: public void ruseniNeexistujicich(){ Iterator<String> ukazovatko = mapa.keySet().iterator(); while(ukazovatko.hasNext()) { String klic = ukazovatko.next(); if (mapa.get(klic) < 1) { System.out.println ("ruším z mapy " + klic); ukazovatko.remove(); } } }
Datové struktury
strana 96
Mapu lze procházet ještě dvěmi způsoby – přes seznam hodnot, který získáme metodou values(), a přes množinu dvojic, kterou vrátí metoda entrySet(). Procházení přes seznam hodnot se používá výjimečně, neboť k hodnotě nelze dohledat klíč. Procházení přes dvojice se též mnoho nepoužívá, neboť je většinou pomalejší, než procházení přes množinu klíčů. Metoda entrySet() vytvoří množinu speciálních prvků typu Map.Entry, každý prvek vznikne spojením klíče a příslušné hodnoty do jednoho objektu. Prvek typu Map.Entry poskytuje metody getKey() a getValue(), které vracejí jednotlivé části (tj. klíč a jemu příslušnou hodnotu) odpovídajících typů. Následující metoda vypisMapy3() ukazuje možné použití těchto metod pro vypsání obsahu mapy. public void vypisMapy3(){ Set<Map.Entry<String,Integer>> mnozinaDvojic = mapa.entrySet(); for(Map.Entry <String, Integer> dvojice : mnozinaDvojic) { System.out.println(dvojice.getKey()+"\t" +dvojice.getValue()); } }
10.4.2.
Mapa pro urychlení vyhledávání
Mapu lze někdy výhodně použít pro urychlení vyhledávání v seznamu – hodnotu, dle které chceme vyhledávat uložíme jako klíč. Ukážeme si to na seznamu účtů, které chceme vyhledávat dle čísla účtu. Klíčem bude číslo účtu (Integer), hodnotou bude instance třídy Ucet. Při prohledávání mapy dle klíče se využívá automatická konverze typu int na obalový typ Integer. private Map seznamUctu = new HashMap(); public boolean existujeUcet(int cisloUctu) { return seznamUctu.containsKey(cisloUctu); } public Ucet vratUcet(int cisloUctu) { return seznamUctu.get(cisloUctu); }
Použití mapy při vyhledávání má proti kolekcím tyto výhody21: ♦ bývá rychlejší, pokud v instanci je více datových atributů, než jen klíč, ♦ vlastní kód psaný programátorem je o něco kratší. Mapa má však i své nevýhody: ♦ zabírá více místa v paměti (klíče jsou uloženy dvakrát), ♦ klíč musí být jedinečný.
10.4.3.
Mapy obsahující seznamy
Někdy potřebujeme ke klíči vložit do mapy více hodnot – příkladem může být telefonní seznam firmy, kdy k některému tel. číslu chceme vložit více osob. Telefonní čísla máme uvedena v tabulce 10.6. Pro uložení telefonních čísel použijeme mapu, kde klíčem bude teefonní. číslo a hodnotou bude seznam (List) osob, které jsou na tomto telefonním čísle. Deklarace této mapy bude vypadat následovně: Map > telSeznam; telSeznam = new HashMap> ();
21
Porovnejte si zde uvedený kód s vyhledáváním uvedeným u kolekcí v kapitole 10.3.6.
Datové struktury
strana 97
telefonní číslo
osoby
101
Petra
102
Jan
103
- volné -
104
Tomáš, Prokop
105
Jana, Eva
106
Petr, Antonín, Petra
Tabulka 10.6 Telefonní seznam, který chceme vyjádřit pomocí mapy Následuje kód metody na přidání další položky do telefonního seznamu – pokud číslo v seznamu ještě neexistuje, tak se vloží. Pokud číslo již v seznamu existuje, přidá se další jméno mezi osoby na tomto telefonním čísle. void pridejPrvek(int telCislo, String jmeno) { if (mapa.containsKey(telCislo)){ List <String> seznamJmen = mapa.get(telCislo); seznamJmen.add(jmeno); } else { List <String> seznamJmen = new ArrayList<String>(); seznamJmen.add(jmeno); mapa.put(telCislo, seznamJmen); } }
Metoda vypisSeznam() vypíše telefonní seznam, ke každému číslu vypíše jednotlivé osoby. public void vypisSeznam (){ Set seznamKlicu = mapa.keySet(); for (Integer telCislo :seznamKlicu){ List <String> seznamJmen = mapa.get(telCislo); System.out.print(telCislo + "\t\t"); for(String jmeno : seznamJmen) { System.out.print(jmeno +", "); } System.out.println(); } }
10.5. Pole (array) Pole je struktura jednoduchá na používání, do které lze vkládat větší množství hodnot stejného typu. Pole má následující výhody: ♦ pokud potřebujeme uložit předem daný počet prvků, je pole ze všech datových struktur nejefektivnější, ♦ pole umožňuje vkládat přímo primitivní datové typy, u ostatních datových struktur se musí převést na objekty, ♦ je možné vytvářet jednorozměrná i vícerozměrná pole. Pole má také nevýhody: ♦ je třeba předem znát počet prvků, které do něj budete ukládat, ♦ nepodporuje některé složitější způsoby práce s objekty (např. vkládání prvků doprostřed pole či vytváření asociativních polí),
Datové struktury
strana 98
♦ špatně se v něm vyjadřuje neexistence prvku – pokud máme pole pro 20 čísel a zatím máme vloženo pouze 10 čísel, potřebujeme pomocnou proměnnou pro vyjádření, která políčka pole jsou neobsazena22, ♦ pro pole nejsou definovány žádné speciální metody, lze použít pouze metody třídy Object a ty se většinou nevyužívají.
10.5.1.
Jednorozměrné pole
Jednorozměrné pole si lze představit jako řadu stejných hodnot. Pole má jeden identifikátor (jméno), pro práci s jednotlivými položkami používáme indexy. Následuje příklad pole se jmény dnů v týdnu. pondělí 0
úterý 1
středa
čtvrtek
2
pátek
3
sobota
4
5
neděle 6
← hodnota ← index
Pole v Javě patří mezi referenční proměnné a podobně jako u objektů se rozlišuje deklarace pole od vytvoření instance pole (inicializace nebo také alokace pole). Pole se pozná podle hranatých závorek [ ], které se uvádějí při deklaraci, inicializaci i při přístupu k prvkům pole. Jednorozměrné pole lze deklarovat oběma následujícími způsoby, na umístění závorek nezáleží: typ [] jmenoPole typ jmenoPole []
Typ určuje položky pole (např. čísla typu int nebo řetězce typu String) – všechny prvky pole jsou stejného typu. Rozsah pole při deklaraci neuvádíme, deklarací pouze vytváříme identifikátor pole. Následuje několik příkladů deklarace pole: int [] prvocisla; String [] dnyVTydnu; String [] args;
Pole inicializujeme stejně jako ostatní referenční proměnné pomocí new, za kterým se uvede typ prvků pole a počet položek v hranatých závorkách: jmenoPole = new typ [pocetPoložek];
Příklady inicializace (vytváření) pole: prvocisla = new int [5]; dnyVTydnu = new String [7];
Deklaraci a inicializaci lze spojit dohromady, vytvoření pole pro pět prvočísel zapíšeme takto: int prvocisla [] = new int [5];
Na jednotlivé prvky pole se odkazujeme indexy, které se číslují od nuly. Z toho vyplývá, že poslední index je o jedničku menší než velikost pole (počet prvků uvedený při inicializaci). Na první položku námi definovaného pole se odkážeme výrazem prvocisla[0], na druhou prvocisla[1] a na poslední prvocisla[4]. Java striktně kontroluje překročení mezí pole – pokud použijeme index 5 nebo jiný mimo interval 0 až 4, bude ohlášena chyba za běhu programu (výjimka ArrayIndexOutOfBoundException). Po vytvoření jsou v jednotlivých položkách pole jejich inicializační hodnoty, tj. např. pole celých čísel obsahuje ve všech položkách nuly. Lze vytvořit i pole s již naplněnými hodnotami, pro pole dnyVTydnu by deklarace a inicializace vypadala takto (všimněte si, že se zde neuvádí new): String dnyVTydnu[] = { "pondělí", "úterý", "středa", "čtvrtek", "pátek", "sobota","neděle"}; 22
Můžeme také říct, že pokud je v nějakém políčku hodnota nula, tak políčko není obsazeno. V tomto řešení ale nemůžeme do pole vložit hodnotu nula.
Datové struktury
strana 99
Práce s tímto polem je naprosto stejná jako s ostatními poli vytvořenými pomocí new, prvky takto inicializovaného pole nejsou konstanty (tj. lze je měnit). Jediný rozdíl je v tom, že pokud chceme inicializovat pole tímto způsobem, musíme spojit deklaraci a inicializaci do jednoho příkazu. Každé pole má proměnnou length přístupnou pouze pro čtení, ve které je uložen počet prvků pole. Příklady použití najdete v následující podkapitole.
10.5.2.
Procházení jednorozměrného pole
K procházení pole je možno využít dva způsoby. První způsob spočívá v použití klasického cyklu for a indexů. Použijeme jej v příkladu jednoduché metody pro součet hodnot prvků pole čísel typu int. Pro zjištění délky pole, které je předáno jako parametr, použijeme proměnnou length. public static int secti(int [] cisla) { int soucet=0; for (int i = 0; i < cisla.length ; i++) { soucet += cisla[i]; } return soucet; }
Druhý způsob procházení pole využívá cyklu „for each“. Jako příklad nám opět poslouží metoda secti(). public static int secti(int [] cisla) { int soucet=0; for (int cislo: cisla) { soucet += cislo; } return soucet; }
10.5.3.
Proměnlivý počet parametrů metody
Pokud potřebujete metodě předat větší počet parametrů stejného typu, je vhodné deklarovat příslušný parametr jako pole. Příkladem může být metoda secti() z minulé podkapitoly. Při volání metody můžeme přímo zadat čísla k sečtení do hlavičky metody: int vysl = secti(new int [] {4, 6, 10, 20});
Od verze 5.0 zavádí Java podporu tzv. proměnlivého počtu parametrů metody, který zjednodušuje hlavně zápis při volání metody s větším počtem parametrů stejného typu. Deklarace metody je téměř stejná jako v předchozím případě, pouze místo hranatých závorek se v hlavičce uvedou tři tečky. Parametry se opět předávají jako pole. public static int secti(int ... cisla) { int soucet=0; for (int cislo: cisla) { soucet += cislo; } return soucet; }
Volání metody s proměnlivým počtem parametrů je mnohem jednodušší a přehlednější – prvky pole parametrů uvedeme přímo v kulatých závorkách metody (první řádek následujícího příkladu). Na místě proměnlivého počtu parametrů lze též uvést pole parametrů (třetí řádek příkladu). int vysl = secti(4, 6, 10, 20); int [] cisla = { 5, 10, 20 }; int vysl2 = secti(cisla);
Datové struktury
strana 100
Není obtížné odvodit, že proměnlivý počet parametrů může být uveden v hlavičce pouze jednou a že musí být poslední. Proměnlivý počet parametrů je použit např. v metodě format() třídy String (viz kapitola 5), jejíž deklarace vypadá takto: public static String format(String format, Object ... args)
Deklaraci s proměnlivým počtem parametrů můžeme použít i u metody main(): public static void main (String ... args) { // obsah metody }
10.5.4.
Vícerozměrná pole
V Javě lze vytvářet i vícerozměrná pole, počet dvojic hranatých závorek odpovídá počtu rozměrů. Deklarace vypadají takto: int poleDvojrozmerne [] [];
Inicializovat toto pole lze několika způsoby. Na následujícím řádku: int poleDvojrozmerne [] [] = new int [2] [3];
vznikne pole se dvěma řádky a třemi sloupci, položky jsou naplněny nulami. Lze použít i inicializaci výčtem – následuje příklad vytvoření pole se dvěma řádky a třemi sloupci, prvky jsou naplněny zadanými hodnotami: int poleDvojrozmerne [ ] [ ] = { { 1, 2, 3, }, { 4, 5, 6, }, }
V Javě je možná i postupná inicializace jednotlivých rozměrů pole: int poleDvojrozmerne [ ] [ ] = new int [2] [ ];
U tohoto pole je již inicializován počet řádků, není ale určen počet sloupců. Tento způsob postupné inicializace umožňuje vytvářet poněkud nezvyklá pole, která budou mít v každém řádku jiný počet prvků23. poleDvojrozmerne [0] = new int [3]; poleDvojrozmerne [1] = new int [5];
Nyní má pole v prvním řádku tři a ve druhém pět prvků. Všechny prvky mají hodnotu nula. Pro zjištění počtu prvků lze opět použít proměnnou length. Počet řádků našeho dvojrozměrného pole zjistíme výrazem poleDvojrozmerne.length, počet prvků prvního řádku poleDvojrozmerne[0].length a pro další řádky analogicky. Lze vytvářet i pole tří a více rozměrná. Při jejich deklaraci a inicializaci platí stejná pravidla jako u dvojrozměrných polí. V případě, že pole vytváříme po částech, nesmíme přeskakovat rozměry, nelze tedy napsat int poleTroj [] [] [] = new int [5] [ ] [5];
Pro procházení vícerozměrných polí můžeme použít obě varianty cyklu for. V následujícím kódu si to ukážeme na výpisu obsahu dvourozměrného pole čísel typu int.
23
V Javě se vytvářejí pole polí, ne klasická dvourozměrná pole známá např. z Pascalu.
Datové struktury
strana 101
public static void vypisPole(int [][]pole){ for (int[] radek : pole){ for (int prvek : radek) { System.out.print(prvek + ", "); } System.out.println(); } } public static void vypisPole2(int[][]pole){ for (int i = 0; i < pole.length; i++){ for (int j = 0; j < pole[i].length; j++) { System.out.print(pole[i][j] + ", "); } System.out.println(); } }
10.5.5.
Parametry vstupní řádky
Pokud má třída poskytnout možnost spuštění z příkazové řádky, musí obsahovat metodu main deklarovanou s hlavičkou public static void main (String [] args). Parametrem metody je tedy jednorozměrné pole řetězců. Při spuštění programu můžeme za příkaz java a jméno třídy uvést libovolný počet parametrů oddělených mezerou, které budou při spuštění metody main uloženy do pole, na které odkazujeme identifikátorem args. Pole args má samozřejmě proměnnou length, která udává počet zadaných parametrů. Pokud nejsou zadány žádné parametry, má proměnná args.length hodnotu nula. V případě, že některý parametr má obsahovat mezery, je nutné ho zadat na příkazové řádce v uvozovkách, např. "Dobrý den". Následující příklad vezme z příkazové řádky desetinné číslo představující poloměr kruhu a na konzole vypíše jeho obvod a obsah. Parametr je předán jako String – před prováděním operací je tedy nutný převod na číslo. Bližší informace ke konstrukci try catch najdete v kapitole 12 věnované výjimkám. 1 public class ObsahKruhu { 2 3 public static void main (String [] args) { 4 double polomer = 0; 5 if (args.length == 1) { 6 try { 7 polomer = Double.valueOf(args[0]).doubleValue(); 8 double obvod = 2*Math.PI*polomer; 9 double obsah = Math.PI*polomer*polomer; 10 System.out.println("Kruh s poloměrem "+polomer 11 +" má obsah "+obsah+" a obvod "+ obvod); 12 } 13 catch (NumberFormatException e) { 14 System.out.println("Zadaný parametr není číslo"); 15 System.exit(1); 16 } 17 } 18 else { 19 System.out.println("Nebyl zadán poloměr kruhu, nelze" + "spočítat obsah "); 20 } 21 } 22 }
Datové struktury
strana 102
Dědičnost
strana 103
11.Dědičnost V této kapitole si vysvětlíme jeden ze základních pojmů objektově orientovaného programování – dědičnost (inheritance). S ní souvisejí i následující témata: ♦ předek a potomek třídy, ♦ klíčová slova extends a super, ♦ přetypování referenčních typů, ♦ abstraktní třídy, ♦ abstraktní metody, ♦ překrývání metod, ♦ pozdní vazba. Tato kapitola navazuje na základní informace o objektech v kapitole 2, zde se však nebudeme zabývat jednou třídou, ale vztahy mezi třídami a objekty. Dědičnost je jednou z forem znovupoužitelnosti – vytvářená třída (potomek) do sebe absorbuje datové atributy a dědí metody z jiné třídy (předek) a dále je rozšiřuje a upravuje. V diagramu tříd se dědičnost vyznačuje pomocí šipky, u které trojúhelník na konci směřuje k předkovi.
A
třída A je předkem třídy B
B
třída B je potomkem třídy A
Obrázek 11.1 Dědičnost, předek a potomek Dědičnost není pouze jednoúrovňová – potomek nějaké třídy může mít dále své potomky. Tito potomci dědí metody od všech tříd na vyšší úrovni. Takto vzniká hierarchie tříd, ve které není omezen počet úrovní. Na obrázku 11.2. je část dědičné hierarchie balíčku java.io (viz kapitola 13) – je zahrnuta pouze abstraktní třída InputStream a její potomci. Zobrazena je i dědičnost ze třídy Object, která se u aplikací v Javě obvykle nekreslí. Java podporuje jednonásobnou dědičnost – každá třída může a musí mít právě jednoho předka. Stromová hierarchie tříd začíná třídou Object (tato jediná třída nemá předka), všechny třídy jsou přímým či nepřímým potomkem této třídy. Většina programovacích jazyků podporuje jednonásobnou dědičnost, některé (např. C++ či Eifel) podporují vícenásobnou dědičnost – třída může mít více předků současně.
Dědičnost
strana 104
Obrázek 11.2 Dědičná hierarchie části balíčku java.io – InputStream a potomci Příklad dědičnosti Pro vysvětlení dědičnosti použijeme příklad s účty z kapitoly 2. Třída Ucet vypadala takto: public class Ucet { private int cisloUctu; private String vlastnik; private double stav = 0; public Ucet (int cisloUctu, String vlastnik){ this(cisloUctu, vlastnik, 0); // volání druhého konstruktoru } public Ucet (int cisloUctu, String vlastnik, double pocatecniVklad){ this.cisloUctu = cisloUctu; this.vlastnik = vlastnik; stav = pocatecniVklad; } public double getStav(){ return stav; }
Dědičnost
strana 105
public void vloz (double castka){ stav = stav + castka; }
}
public boolean vyber (double castka){ if ((stav – castka) >= 0) { stav = stav – castka; return true; } else { return false; } }
Nyní vytvoříme další typ účtu – účet s kontokorentem (žirový účet), u kterého lze vybírat do předem stanoveného zůstatku. Vzhledem k tomu, že základní datové atributy a metody jsou podobné, použijeme dědičnosti – v hlavičce třídy uvedeme klíčové slovo extends a za ním jméno třídy předka24: public class ZiroUcet extends Ucet {
Žirový účet bude mít navíc jeden datový atribut – limit kontokorentu. Metody getStav() a vloz() se nemění – tj. je možné je zdědit, nemusí se tudíž psát do kódu potomka. Metoda vyber() bude odlišná – musí se povolit výběr až do limitu kontokorentu. Uvedení metody se stejnou hlavičkou v potomkovi se nazývá překrytí metody (overriding). Pokud má metoda v potomkovi jinou hlavičku (liší se počtem či typy parametrů), nejedná se o překrytí metody, ale o přetížení metody – viz kapitola 9. V příkladu je komplikace i s datovým atributem stav – tento datový atribut je private ve třídě Ucet a tudíž není dostupný ani pro potomky. Řešením je vytvořit ve třídě Ucet metodu setStav() s modifikátorem přístupu protected: protected void setStav(double castka) { stav = castka; }
Metoda vyber() by poté ve třídě ZiroUcet vypadala takto (doplněna je též metoda pro zjištění limitu, naopak chybí konstruktory, které si popíšeme později): public class ZiroUcet extends Ucet { private double limit = 0; // konstruktor(y) public boolean vyber (double castka) { if ((getStav()+limit – castka) >= 0) { setStav(getStav()– castka); return true; } else { return false; } } public double getLimit() { return limit; } }
24
Pokud je předkem třídy třída Object, nemusí se extends uvádět – tuto dědičnost automaticky doplní překladač.
Dědičnost
strana 106
Pokud bychom nemohli zasahovat do kódu předka, tj. pokud bychom do předka nemohli doplnit metodu setStav(), museli bychom si vybrat některou z následujících variant: ♦ nastavovat stav pomocí metody vyber() s tím, že se použijí záporné hodnoty, ♦ v potomkovi si udržovat samostatný datový atribut, který by obsahoval částku vybranou z kontokorentu. V této variantě je potřeba upravit i další metody.
11.1. Dědičnost a konstruktory, klíčové slovo super Ještě je potřeba do třídy ZiroUcet doplnit konstruktory. Vytvoříme tři, první umožní nastavit všechny čtyři datové atributy (číslo účtu, vlastníka, počáteční vklad a limit). Druhý a třetí konstruktor odpovídají přetíženým konstruktorům z předka – cílem je, aby i z hlediska vytváření se mohl potomek používat stejně jako třída předka. I tyto dva konstruktory je nutné napsat, neboť v Javě se konstruktory nedědí. Z konstruktoru potřebujeme volat konstruktor předka – pro volání konstruktoru předka se používá klíčové slovo super. Super se používá podobně jako this při volání jiného konstruktoru ve stejné třídě, opět musí být prvním příkazem v konstruktoru. public class ZiroUcet extends Ucet { private double limit = 0; public ZiroUcet (int cisloUctu, String vlastnik, double pocatecniVklad, double limit){ super(cisloUctu, vlastnik, pocatecniVklad); this.limit = limit; } public ZiroUcet (int cisloUctu, String vlastnik, double pocatecniVklad){ this(cisloUctu, vlastnik, pocatecniVklad, 0); } public ZiroUcet (int cisloUctu, String vlastnik){ this(cisloUctu, vlastnik, 0, 0); //volání prvního konstruktoru }
V prvním konstruktoru se nejdříve pomocí super volá konstruktor předka – předávají se mu tři parametry. Na dalším řádku se nastavuje datový atribut limit. Druhý a třetí konstruktor je napsán s využitím prvního konstruktoru – doplní se defaultní hodnoty na místě chybějících parametrů25. Vztah mezi třídami Ucet a ZiroUcet je na obrázku 11.3. Pokud v konstruktoru potomka neuvedeme volání konstruktoru předka (super) či volání jiného konstruktoru stejné třídy (this), překladač automaticky na první řádek konstruktoru doplní volání:
super ();
Tj. volá se konstruktor předka bez parametrů. Pokud předek takový konstruktor nemá, vznikne chyba při překladu. Třída Object (předek všech tříd) má konstruktor bez parametrů.
25
Některé objektové programovací jazyky podporují psaní defaultních hodnot již přímo do deklarace hlavičky metody. Tím se lze někdy vyhnout psaní přetěžovaných metod a konstruktorů. Příkladem takových jazyků je Python či Ruby.
Dědičnost
strana 107
Obrázek 11.3 Vztah tříd Ucet a ZiroUcet (diagram tříd) Klíčové slovo super má vedle volání konstruktoru předka ještě dvě použití: ♦ lze se pomocí něho odkazovat na datový atribut předka, pokud není private (vhodnější je mít všechny datové atributy private a přistupovat k nim pomocí metod), např. super.stav
♦ lze se pomocí super odkázat na metodu předka. Používá se v situaci, kdy v předkovi a v potomkovi dochází k překrytí metod a potřebujeme zavolat z potomka metodu předka. V metodě vyber() ve třídě ZiroUcet by se mohlo nejdříve zkusit vybrat pomocí metody vyber() z předka, která má stejnou hlavičku: public boolean vyber (double castka) { if (super.vyber(castka)) { // pokračování metody } }
11.2. Vytvoření instance při dědičnosti Je potřeba odlišovat dědičnost v době psaní kódu (při překladu) a při vlastním vytváření instancí. Při psaní kódu máme z potomka přístup k datovým atributům a metodám předka, pokud nejsou private. Pomocí super lze přistupovat i k datovým atributům předka, které mají stejné jméno jako datové atributy v potomkovi. Obdobně lze přistupovat i k metodám předka se stejnou hlavičkou. Odlišná situace je u vytvořené instance. Každá instance má: ♦ Přiřazen paměťový prostor pro všechny datové atributy definované ve třídě i ve všech předcích (včetně privátních datových atributů v předcích, ke kterým není přímý přístup). ♦ Přiřazeny všechny své konstruktory. ♦ Konstruktory předků nejsou přímo dostupné, jsou ve stavu, kdy je lze volat pouze z potomka.
Dědičnost
strana 108
♦ Přiřazeny všechny své metody a metody předků, které nejsou v potomkovi překryty (v rámci dědičné hierarchie). Pokud je v předkovi a v potomkovi metoda se stejnou hlavičkou, tak je přiřazena pouze ta z potomka. Každá instance třídy ZiroUcet z našeho příkladu s účty bude mít: ♦ datové atributy cisloUctu, vlastnik, stav a limit, ♦ tři konstruktory ze třídy ZiroUcet, ♦ metodu vyber(double castka) ze třídy ZiroUcet, ♦ tři metody ze třídy Ucet – getStav(), setStav() a vloz(), ♦ metody ze třídy Object – equals(), hashCode(), finalize(), toString(), clone(), getClass(), notify(), notifyAll() a tři varianty metody wait(), ♦ možná nějaké privátní datové třídy ze třídy Object – třída Object nemá žádné přístupné datové atributy, může mít však privátní.
11.3. Modifikátor přístupu protected Modifikátor přístupu protected se používá v situaci, kdy chceme nějakou metodu zpřístupnit pouze pro potomky (tj. není veřejně dostupná). Používá se v situaci, kdy není vhodné, aby se metoda veřejně používala, ale současně předpokládáme, že tuto metodu využije více potomků. Modifikátor protected lze používat i u datových atributů – v tomto případě je však nutno zvážit, zda není vhodnější deklarovat datové atributy private a z potomků k nim přistupovat pomocí vhodných metod (get/set metody), které budou chráněné (protected).
11.4. Abstraktní třídy Abstraktní třída se používá jako předek v dědičné hierarchii a představuje třídu, od které nemá smysl vytvářet instance. Abstraktní třída může mít vedle konkrétních datových atributů a metod i abstraktní metody (datové atributy nemohou být abstraktní). Abstraktní metoda obsahuje pouze hlavičku, potomek této abstraktní třídy musí tuto metodu implementovat (nebo musí být potomek též abstraktní). Abstraktní třídy si vysvětlíme na příkladu se včelami a motýly, které létají po louce. Třídy Motyl a Vcela by mohly vypadat následovně (většina kódu je pouze naznačena): public class Vcela { private Pozice pozice; public void jedenPohyb () { if (vUlu() && maNektar()) { // odevzdat nektar } else { if (plnyKosicek()) { // let k úlu } else { if (naKvetineSNektarem()) { // sbirej nektar } else { preletni(); } } } }
Dědičnost
strana 109
private // // } private // } private // } private // } private if
}
}
void preletni() { vyber náhodně květinu v nejbližším okolí přesuň se na vybranou květinu boolean maNektar() { obsah metody boolean vUlu() { obsah metody boolean plnyKosicek() { obsah metody boolean naKvetineSNektarem() { (pozice.jeKvetina()) { Kvetina kvetina = pozice.getKvetina(); return kvetina.maNektar();
} else { return false; }
Třída Motyl vypadá následovně: public class Motyl { private Pozice pozice; public void jedenPohyb () { if (naKvetineSNektarem()) { // sbirej nektar } else { preletni(); } } private void preletni() { // vyber náhodně květinu v nejbližším okolí // přesuň se na vybranou květinu } private boolean naKvetineSNektarem() { if (pozice.jeKvetina()) { Kvetina kvetina = pozice.getKvetina(); return kvetina.maNektar(); } else { return false; } } }
Když porovnáte obě třídy, zjistíte, že mají stejné dvě metody – preletni() a naKvetineSNektarem(). Proto by bylo vhodné vytvořit předka, který by obsahoval tyto metody. Nemá však smysl vytvářet instance tohoto předka, proto předek bude abstraktní. V hlavičce abstraktní třídy musí být uvedeno slovo abstract, v abstraktní třídě mohou být abstraktní metody. Abstraktní metoda je taková, u které se při návrhu požaduje, aby ji měl implementovanou každý potomek abstraktní třídy. V abstraktní třídě se uvede pouze hlavička metody s modifikátorem abstract.
Dědičnost
strana 110
Abstraktní třída může obsahovat datové atributy i konstruktory. I když nelze vytvořit instanci abstraktní třídy, musí konstruktor abstraktní třídy existovat – jak jsme si již řekli dříve, tak v konstruktoru potomka se nejdříve volá konstruktor předka a teprve poté se provádí další příkazy v konstruktoru potomka. Pokud nenaimplementujeme alespoň jeden konstruktor, bude stejně jako u „obyčejné“ třídy překladačem doplněn implicitní konstruktor. Překladač Javy kontroluje, zda kdekoli v kódu není vytvářena instance abstraktní třídy. Třída LetajiciHmyz bude obsahovat abstraktní metodu jedenPohyb(), konkrétní metody preletni() a naKvetineSNektarem(). Součásti třídy je i datový atribut pozice, který je potřeba v obou konkrétních metodách. Metody preletni() a naKvetineSNektarem() mají modifikátor přístupu protected, aby se mohly používat v potomcích. public abstract class LetajiciHmyz { private Pozice pozice; public abstract void jedenPohyb (); protected void preletni() { // vyber náhodně květinu v nejbližším okolí // přesuň se na vybranou květinu } protected boolean naKvetineSNektarem() { if (pozice.jeKvetina()) { Kvetina kvetina = pozice.getKvetina(); return kvetina.maNektar(); } else { return false; } } }
Třída Motyl potom bude vypadat takto: public class Motyl extends LetajiciHmyz { public void jedenPohyb () { if (naKvetineSNektarem()) { // sbirej nektar } else { preletni(); } } }
Úpravy ve třídě Vcela budou velmi podobné. Nelze vytvořit přímo instanci abstraktní třídy, tj. není možné přímo zavolat konstruktor new LetajiciHmyz(). Lze však vytvořit instanci některého z potomků a přetypovat ho na typ LetajiciHmyz, např. takto: LetajiciHmyz hmyz = new Motyl();
V diagramu tříd se jméno abstraktní třídy píše kurzívou, obdobně i jméno abstraktní metody se píše kurzívou. Druhou alternativou je vyznačit abstraktní třídy a metody pomocí stereotypu.
Dědičnost
strana 111
Obrázek 11.4 Vztah abstraktní třídy LetajiciHmyz a tříd Motyl a Vcela
11.5. Polymorfismus a překrytí metod Polymorfismus ve spojitosti s dědičností souvisí s tématy přetypování referenčních typů, překrývání metod a pozdní vazba. Většinou jsme se již s nimi seznámili, zde si je ještě zopakujeme.
11.5.1.
Přetypování referenčních typů
Jak jsme si již uvedli v kapitole 3, i u referenčních typů lze používat přetypování. U těchto typů je přetypování závislé na dědičnosti – lze přetypovávat na typ předka (implicitní přetypování), někdy lze přetypovávat na potomka. Ukážeme si to na příkladu tříd Ucet a ZiroUcet. Instanci třídy ZiroUcet je možné používat všude, kde lze použít instanci třídy Ucet. Pro třídu SeznamUctu si ukážeme dvě metody – metodu pridej() pro přidání účtu a metodu vypis(), která bude vypisovat seznam účtů. public class SeznamUctu { private List seznam;
}}}
public void pridej(Ucet ucet) { seznam.add(ucet); } public void vypis() { for (Ucet ucet : seznam) { System.out.println(ucet.getVlastnik() + "\tstav: " + ucet.getStav());
Nyní můžeme přidávat do seznamu jak obyčejné účty, tak i žirové účty. Pokud bychom však pro žirové účty chtěli vypisovat i limit kontokorentu, musíme v metodě vypis() pomoci operátoru instanceof zjišťovat, že se jedná o instanci třídy ZiroUcet, a tu poté explicitně přetypovat a zavolat metodu getLimit().
Dědičnost
strana 112
public void vypis() { for (Ucet ucet : seznam) { if (ucet instanceof ZiroUcet) { ZiroUcet ziro = (ZiroUcet) ucet; System.out.println(ziro.getVlastnik() + "\tstav: " + ziro.getStav()+"\tlimit: "+ ziro.getLimit()); } else { System.out.println(ucet.getVlastnik() + "\t" + ucet.getStav()); } } }
Na další stránce si ukážeme vhodnější řešení metody vypis(), kdy místo rozskoku použijeme pro rozlišení polymorfismus. Dalším příkladem přetypování může být metoda equals(), kterou jsme si uvedli u operátoru instanceof v kapitole 3: public boolean equals (Object o) { if (o instanceof Mistnost) { Mistnost druha = (Mistnost)o; return nazev.equals(druha.nazev); } else { return false; } }
Parametr metody equals() je typu Object, tj. lze uvést instanci libovolné třídy. Při volání metody equals() v následujícím kódu se automaticky přetypuje instance komora třídy Mistnost na typ Object, uvnitř metody equals() se přetypuje zpět na typ Mistnost. Mistnost komora = new Mistnost("komora", "sklad vedle kuchyně"); // ... mnoho řádků kódu ... if (sousedniMistnost.equals(komora)) {
Vlastní instance se při přetypování nemění. Metody má instance přiřazeny v okamžiku vytvoření instance, žádným přetypováním nelze změnit přiřazení metod. Na druhou stranu přetypování ovlivňuje překlad – pokud potomka přetypujeme na předka, tak překladač bude kontrolovat, zda používáme pouze metody, které zná předek. Proto je např. nutné ve výše uvedené metodě equals() přetypovat Object zpět na místnost, aby byl dostupný datový atribut nazev.
11.5.2.
Překrývání metod, pozdní vazba a polymorfismus
Do třídy Ucet doplníme metodu getPopis(): public String getPopis() { return ucet.getVlastnik() + "\tstav: " + ucet.getStav(); }
Ve třídě ZiroUcet metoda getPopis() vrátí též limit: public String getPopis() { return ziro.getVlastnik() + "\tstav: " + ziro.getStav()+"\tlimit: "+ ziro.getLimit(); }
Pokud již máme takto připraveny třídy Ucet a ZiroUcet, bude metoda vypis() ve třídě SeznamUctu velmi jednoduchá.
Dědičnost
strana 113
public void vypis() { for (Ucet ucet : seznam) { System.out.println(ucet.getPopis()); } }
Při tomto použití se již uplatňuje polymorfismus – volají se různé metody, byť to v kódu není přímo vidět. Pokud se ze seznamu vybere instance třídy Ucet, zavolá se metoda getPopis() ze třídy Ucet, pokud se vybere instance třídy ZiroUcet, zavolá se metoda getPopis() z této třídy. Toto chování je umožněno pozdní vazbou (late binding či dynamic binding) – když se vytvoří instance, přiřazují se k ní jednotlivé metody. Pokud se některé metody v hierarchii dědičnosti překrývají, přiřadí se z nich ta metoda, která je nejblíže. Obvykle je to metoda stejné třídy. Pokud se volá nějaká metoda instance, hledá se v seznamu metod přiřazených instanci. V některých programovacích jazycích se používá též časná vazba (early binding či static binding) – volaná metoda se přiřazuje při překladu. V našem příkladu s metodou vypis() by se při použití časné vazby vždy volala metoda getPopis() ze třídy Ucet, neboť při překladu se vybírá metoda dle uvedeného typu. V kódu je řečeno, že proměnná ucet je typu Ucet, tudíž se vezme metoda getPopis() z této třídy. Java časnou vazbu nepodporuje.
11.5.3.
Příklad polymorfismu
Následuje další příklad dědičnosti s abstraktní třídou Pizza a čtyřmi podřízenými konkrétními třídami. Ve třídě Pizza jsou dvě abstraktní metody (pripravit() a upect()) a dvě konkrétní metody (nakrajet() a zabalit()), viz diagram na obrázku 11.5. Aby bylo možno mluvit o polymorfismu, musí existovat alespoň jedna další třída, která bude pracovat s větším počtem instancí různých potomků třídy Pizza a volat některé z metod, které jsou přetíženy v potomcích (tj. metody pripravit() a upect()). Můžeme si představit, že máme zásobník objednávek, do kterého jsou vloženy jednotlivé objednávky na pizzu. Následuje kód, který představuje činnost kuchaře: vezme objednávku, z objednávky postupně připraví jednotlivé požadované pizzy a nakonec všechny pizzy z objednávky odešle. while (!zasobnikObjednavek.empty()) { Objednavka objednavka = zasobnikObjednavek.getObjednavka(); for (Pizza pizza : objednavka.pozadovanePizzy()) { pizza.pripravit(); pizza.upect(); pizza.nakrajet(); pizza.zabalit(); objednavka.vlozPizza(); } objednavka.odeslat(); }
Dědičnost
strana 114
Obrázek 11.5 Abstraktní třída Pizza a její potomci Použití polymorfismu se obvykle též projeví v diagramu tříd. Třída Kuchar se neodkazuje na jednotlivé instance, ale na předka, viz obrázek 11.6.
Obrázek 11.6 Vztah mezi třídami při polymorfismu Použití polymorfismu s dědičností je velmi podobné polymorfismu při použití rozhraní. Z důvodů větší nezávislosti a pružnosti návrhu se obvykle navrhuje rozhraní, které definuje požadované chování (požadované metody). Toto rozhraní implementuje abstraktní třída – její hlavní význam je v úspoře kódu při psaní konkrétních potomků. Výsledná struktura je pro náš příklad s pizzou zobrazena na následujícím obrázku. Při pohledu do knihoven Javy zjistíme, že mnoho tříd je navrženo tímto způsobem, např. kolekce a mapy.
Dědičnost
strana 115
Obrázek 11.7 Rozhraní, abstraktní třída a konkrétní třídy
11.6. Použití dědičnosti Dědičnost by se měla používat v situacích, kdy potomek je podtypem svého předka, tj. existuje mezi nimi vztah „je nějaký“ (v angličtině se používá „is-a“). Pokud má být nějaká třída B potomkem třídy A, měli bychom si kladně odpovědět na tyto otázky: „B je A?“ či „Je každý B také A?“. Nemůžeme-li na takou otázku odpovědět kladně, neměli bychom používat dědičnost. V případě bankovních účtů lze odpovědět kladně na výroky „Žirový účet je účtem?“ a „Je každý žirový účet také účtem?“. Důsledky použití dědičnosti v situaci, kdy je porušen vztah „je nějaký“, si ukážeme dále v této kapitole. Vedle vztahu „je nějaký“ jsou vztahy, které lze vyjádřit pomocí „je částí“ („part-of“) a „má“ („has-a“). Pokud jsou mezi třídami tyto vztahy, nepoužívá se dědičnost, ale většinou kompozice. Důležité je též si uvědomit, že dědičnost vyjadřuje vztah tříd, ne vztah instancí. Pro objektový jazyk C++ s vícenásobnou dědičností se obvykle doporučuje, aby základem nějaké dědičné hierarchie byla abstraktní třída. Toto neplatí plně pro jazyky s rozhraními, jako je např. Java, kde se jako základ dědičné hierarchie obvykle používá rozhraní.
11.6.1.
Důvody pro použití dědičnosti
Dědičnost se využívá v různých situacích, za různými účely. V praxi lze důvody pro použití dědičnosti obtížně odlišit. Zde si uvedeme tři nejčastější důvody k použití dědičnosti.
Dědičnost
strana 116
Specializace Jedním z nejčastějších důvodů pro použití dědičnosti je specializace existujících tříd a objektů. Při specializaci získává třída nové datové atributy a chování proti původní třídě. Ukázkou specializace je příklad s bankovním účtem a žirovým účtem. Jinou formou specializace je většina situací, kdy předkem je abstraktní třída – viz příklad s pizzou. Překrývání metod a polymorfismus Častým důvodem k dědičnosti je možnost využití překrývání metod a následně polymorfismu – různí potomci mají rozdílně implementována některá chování (některé metody). Při volání takové metody programátor nemusí uvažovat o tom, které konkrétní instanci posílá zprávu, neboť každá instance má k sobě přiřazen svůj specifický kód. Znovupoužití kódu Jednou z prvních motivací pro dědičnost bylo umožnit nové třídě znovu použít kód, který již existoval v jiné třídě. Pokud vede k dědičnosti pouze tento motiv, vznikne hierarchie, kdy věcně nelze přetypovat potomka na předka. Příklad si uvedeme v podkapitole věnované chybám v návrhu dědičnosti. V těchto situacích se preferuje před dědičností jiná technika – kompozice. Příklad kompozice najdete ke konci této kapitoly.
11.6.2.
Porušení vztahu „je nějakým“ při návrhu dědičnosti
Nejčastějším úskalím používání dědičnosti je situace, kdy programátor využívá dědičnosti pouze z důvodu úspory kódu a z tohoto důvodu poruší vztah „is-a“. Uvedeme si dva příklady. V prvním příkladu vytváříme dvě třídy představující grafické tvary – třídu Ctverec a třídu Obdelnik. U třídy Ctverec postačuje jeden datový atribut – délka strany a, u obdélníku potřebujeme ještě stranu b. Tj. třída Obdelnik rozšiřuje třídu Ctverec a tudíž je kandidátem na potomka třídy Ctverec. Na následujícím diagramu jsou zobrazeny datové atributy a metody obou tříd.
Obrázek 11.8 Struktura tříd Ctverec a Obdelnik Tyto třídy na první pohled fungují tak, jak mají – lze vytvářet čtverce i obdélníky, správně se počítají obvody i obsahy. Nyní dostaneme za úkol vytvořit metodu, která zdvojnásobí plochu čtverce:
Dědičnost
strana 117
public void zdvojnasobitPlochu(Ctverec ctvr) { ctvr.setStranaA(ctvr.getStranaA() * Math.sqrt(2)); }
Na první pohled metoda funguje dobře, problém nastane v okamžiku, kdy zavoláme tuto metodu s instancí třídy Obdelnik. Plochu obdélníka metoda nezdvojnásobí – metoda nedělá to, co jsme chěli. Problém spočívá v nesprávně použité dědičnosti, obdélník není čtvercem – není zde vztah „isa“. Druhý příklad chybného použití dědičnosti si ukážeme na příkladu ze standardních knihoven Javy. V balíčku java.util je třída Stack, která nabízí datovou strukturu zásobník. Zásobník má tři základní operace: vkládání prvků na vrchol zásobníků (push), odebírání prvků z vrcholu zásobníků (pop) a získání prvku z vrcholu zásobníku (peek). Pro realizaci zásobníku je výhodné použít seznam (List – viz kapitola 10), neboť ten již obsahuje metody pro vkládání prvků do seznamu, vyndávání prvků ze seznamu. Autor třídy Stack použil dědičnosti – třída Stack je potomkem třídy Vector, což je jedna z implementací seznamu v Javě. Tím, že je třída Stack potomkem seznamu jsou však porušena pravidla pro zásobník – ze třídy Vector se dědí další metody, které umožňují vkládat dovnitř zásobníku či vybírat zevnitř zásobníku. Tuto chybu v návrhu dědičnosti však již nelze odstranit, neboť takto definovaná třída Stack se již začala používat v mnoha aplikacích.
11.6.3.
Dědičnost narušuje zapouzdření
Problémem používání dědičnosti je, že narušuje jinou základní objektovou vlastnost – zapouzdření. Programátor třídy potomka musí vědět nejen to, co dělají metody předka, ale i jak to dělají. Projevuje se to i v dokumentaci metod – u metod, které se mohou překrývat, by měl popis zahrnovat i vnitřní fungování metody. Potomek je též velmi závislý na změnách rodičovské třídy. Představme si, že máme třídy Kosodelnik a Obdelnik. Obdélník je speciálním případem kosodélníku (úhel 90º), proto může být třída Obdelnik potomkem třídy Kosodelnik. Následuje jednoduchý návrh třídy Kosodelnik (úhel je potřeba zadávat v radiánech): public class Kosodelnik { protected double stranaA = 0; protected double stranaB = 0; protected double uhel = 0;
}
public Kosodelnik(double stranaA, double stranaB, double uhel) { this.stranaA = stranaA; this.stranaB = stranaB; this.uhel = uhel; } public double obvod() { return 2 * (stranaA + stranaB); } public double obsah() { return stranaA * stranaB * Math.sin(uhel); }
Při využití dědičnosti bude třída Obdelnik poměrně krátká (úhel 90º odpovídá hodnotě polovina π v radiánech): public class Obdelnik extends Kosodelnik { public Obdelnik(double stranaA, double stranaB) { super(stranaA, stranaB, Math.PI/2); } }
Dědičnost
strana 118
Když se však nyní rozšíří třída Kosodelnik o metodu setUhel() pro změnu úhlu, tak dojde k narušení dědičnosti objektů26. Tuto metodu zdědí i potomek, po jejím zavolání u instance třídy Obdelnik potom najednou budeme mít obdélníky bez pravých úhlů. To však odporuje definici obdélníka. Jedním řešením je doplnit do třídy Obdelnik metodu setUhel() a v ní vyvolat výjimku (např. UnsupportedOperationException). Tj. v této variantě je potřeba při změně předka upravit i (některé) potomky – to však předpokládá, že autor úpravy v předkovi má k dispozici všechny potomky. Dalším řešením je nedávat do třídy Kosodelnik metodu setUhel(), tj. navrhnout ji jako readonly třídu obdobně jako u třídy String. Pokud se má změnit některý ze základních datových atributů jako úhel či strana, tak se vytvoří nová instance třídy Kosodelnik. Třetím řešením je při návrhu třídy Obdelnik nepoužívat dědičnost, ale použít kompozici – ve třídě Obdelnik bude datový atribut typu Kosodelnik a většina volání metod třídy Obdelnik bude přesměrována na odpovídající metody třídy Kosodelnik: public class Obdelnik { private Kosodelnik kosodelnik; public Obdelnik(double stranaA, double stranaB) { kosodelnik = new Kosodelnik(stranaA, stranaB, Math.PI/2); } public double obvod() { return kosodelnik.obvod(); } public double obsah() { return kosodelnik.obsah(); } }
Nevýhodou varianty s kompozicí však je, že při používání těchto tříd nelze využít výhod polymorfismu (lze to obejít při vhodném využití rozhraní). Pokud se již dědičnosti mezi třídami Kosodelnik a Obdelnik někde používá, tak toto řešení nepřichází v úvahu.
26
Zde máme na mysli narušení věcné/logické dědičnosti tříd, formální pravidla dědičnosti v Javě se tímto nenaruší.
Výjimky
strana 119
12.Výjimky Při vytváření programu je třeba vždy zohlednit to, že za běhu programu se mohou objevit chyby, které je třeba ošetřit. Například uživatel se snaží otevřít neexistující soubor pro čtení a nebo místo čísla vloží na vstupu text. S chybovými stavy se lze vypořádat dvěmi způsoby: ♦ Testováním splnění podmínek před vyvoláním kódu. Např. lze zjistit, zda soubor existuje před tím, než se bude otevírat pro čtení, lze zjistit, zda uživatel zadal pouze číslice. V některých situacích nelze tento postup použít – např. když v průběhu čtení souboru havaruje disk. ♦ Přes mechanismus výjimek – kód se napíše s vírou ve splnění podmínek, pokud v průběhu nastane chyba, tak se ošetří pomocí mechanismu výjimek. Volba příslušného způsobu ošetření závisí na typu chyby, na pravděpodobnosti vzniku chybového stavu, na nákladech spojených s příslušnými typy ošetření výjimky. Použití mechanismu výjimek vede obvykle k přehlednějšímu kódu, někdy však za cenu snížení rychlosti algoritmu.
12.1. Druhy výjimek Když v programu dojde k chybě, vznikne výjimka – přeruší se zpracování programu, vytvoří se objekt s informacemi o chybě a pokračuje se na místě, na které uživatel umístil kód s ošetřením chyby. Předkem pro všechny výjimky je třída Throwable a jejími potomky třídy Error a Exception. Java také umožňuje tvorbu vlastních výjimek. Hierarchie tříd pro výjimky je zachycena na obrázku 12.1. Se třídou Throwable se přímo nepracuje, poskytuje totiž velmi obecnou informaci, že nastala nějaká chyba. Třída Error reprezentuje chyby systému Javy (např. nedostatek paměti – OutOfMemoryError, přetečení zásobníku – StackOverflowError, chyby v souboru class – ClassFormatError, ClassCircularityError, VerifyError, NoClassDefFoundError), v programu se obvykle neošetřují (některé ani nelze ošetřit). Třída Exception a její potomci, mimo větev RuntimeException, jsou označováni jako kontrolované (synchronizované) výjimky, u kterých překladač kontroluje, zda je ošetřujeme. Mezi typické zástupce patří chyby vzniklé při práci se vstupy a výstupy. Na možnost výskytu výjimky musíme v kódu nějak reagovat (viz dále), jinak překladač vypíše upozornění a kód nepřeloží. Třída RuntimeException a její potomci reprezentují chyby, na které lze také úspěšně reagovat, ale u kterých není překladačem vyžadováno jejich ošetření. Jsou to například ArithmeticException, ArrayIndexOutOfBoundsException, NullPointerException nebo NumberFormatException. V mnoha případech může programátor předejít těmto výjimkám vhodným testováním parametrů či pečlivostí při programování.
Výjimky
strana 120
Obrázek 12.1 Hierarchie výjimek s příklady v jednotlivých skupinách
12.2. Vyvolání výjimky Výjimky mohou vznikat pouze za následujících situací: ♦ chybný stav aplikace detekovaný virtuálním strojem, sem patří: * chybný výraz, např. celočíselné dělení nulou, * chyby při natažení tříd do paměti, * nedostatečné zdroje, např. nedostatek paměti, * vnitřní chyba virtuálního stroje, ♦ vyvoláním výjimky příkazem throw – nejčastější případ. Při psaní svých metod můžeme vyvolat výjimku příkazem throw, což může vypadat následovně: if (promenna == null) { throw new NullPointerException(); }
Vyvoláním výjimky se přeruší provádění metody. Při vyvolání výjimky můžeme předat jako parametr i podrobnější popis výjimky: if (promenna == null) { throw new NullPointerException("promenna = null"); }
Výjimky
strana 121
12.3. Ošetření výjimky Java poskytuje dva způsoby práce se vzniklou výjimkou: ♦ zachycení a ošetření (jazyková konstrukce try … catch), ♦ předání výjimky výše (throws) – týká se kontrolovaných výjimek, pokud jejich ošetření chceme předat do volající metody. Při vyvolání výjimky hledá JVM nejbližší úsek kódu ošetřující příslušnou výjimku – JVM jde v protisměru volání jednotlivých metod, v každé metodě hledá, zda volání bylo v bloku try a zda je u bloku ošetření tohoto typu výjimky nebo je uveden předek této výjimky. Pokud nikde ošetření tohoto typu výjimky nenajde, použije standardní ošetření, ve kterém se vypíší informace o výjimce a aplikace skončí. Následuje ukázka standardního chybového hlášení, na kterém je vidět typ výjimky a sekvence volání jednotlivých metod (ukázka je z projektu Skola).
Vznikla výjimka NullPointerException a to v metodě hashCode() ve třídě Osoba (konkrétně na řádku 91 zdrojového kódu Osoba.java). Tato metoda byla volána z metody hash() ve třídě HashMap, která byla volána z metody put() ve třídě HashMap, atd. Na předposledním řádku se odkazuje výraz na konstruktor. Tato chyba vznikla z toho důvodu, že místo jména pracovníka je napsána konstanta null. U některých výjimek (většinou potomků třídy Error) může být popis chyby kratší. Následující příklad ukazuje chybové hlášení při pokusu o spuštění neexistující třídy.
12.3.1.
Odchytávání výjimek (try catch)
Pokud chceme výjimku ošetřit v metodě sami, uzavřeme část kódu, ve kterém může vzniknout chyba, do bloku uvedeného slovem try. V rámci bloku try by měly být uvedeny příkazy, které odpovídají správnému průběhu programu. Po tomto bloku následuje alespoň jeden blok začínající klíčovým slovem catch v závorce následovaný typem (jménem třídy) výjimky a jménem proměnné, ve které budou uloženy informace o chybě (obvykle se používá jméno proměnné e)27. Těchto bloků catch může být více. Platí však, že jednotlivé bloky jsou procházeny postupně a první, který vyhovuje (tj. výjimka, která nastala, je instancí uvedené třídy nebo jejího potomka), tuto výjimku ošetří. Výjimky je tedy třeba při odchytávání uvádět v pořadí od konkrétních k obecným (od potomků k předkům). Jako poslední může být v try catch uveden blok finally, příkazy uvedené v tomto bloku jsou provedeny vždy, tj.: ♦ když k žádné výjimce nedojde, ♦ když je vyhozena výjimka a je zpracována v některém bloku catch, ♦ když vznikne výjimka, která není zpracována v blocích catch, ♦ když vznikne výjimka v některém bloku catch. 27
Deklarace bloku catch částečně připomíná deklaraci metody. Formální parametr (obvykle se používá identifikátor e) má platnost v rámci následujícího bloku.
Výjimky
strana 122
Následuje formální specifikace bloku try catch: try { blok chráněných příkazů } catch (xxxException e) { reakce na výjimku daného typu } catch (...... ....... finally { ukončovací příkazy }
Proměnná uvedená jako formální parametr (obvykle e) v bloku catch obsahuje odkaz na konkrétní výjimku. U všech výjimek lze zjistit popis chyby či vypsat popis včetně odkazu na příslušné řádky zdrojového kódu, kde k výjimce došlo. Požadované informace zjistíme pomocí metod deklarovaných ve společném předkovi všech chyb a výjimek třídě Throwable (některé výjimky mají další metody a informace). Jejich přehled je uveden v následující tabulce. metoda
popis
String getMessage()
vrátí popis chyby (může být prázdný)
String toString()
vrátí jméno třídy chyby následované popisem (výsledek metody getMessage())
StackTrace[] getStackTrace()
vrací zásobník s popisem průběhu vzniku a posílání výjimky, každá položka odpovídá jednomu řádku ve volací sekvenci volání metod
void printStackTrace()
vypíše výsledek metody getStackTrace() na System.err (standardně na konzolu)
Tabulka 12.1 Přehled informativních metod třídy Throwable Použití odchycení a zpracování výjimky si ukážeme na následujícím příkladě. Jedná se o metodu getInt() ze třídy CteniZKonzole z projektu Trojúhelníky v kapitole 17. Tato metoda má přečíst z konzole celé číslo zadané uživatelem. Je zde použit nekonečný cyklus (řádek 4), který ukončí příkaz return v případě, že uživatelem zadaná hodnota je celé číslo. Uvnitř cyklu je v bloku try (řádky 5 až 11) uveden kód představující optimální průběh (uživatel zadá celé číslo). Řádek 6 kódu zapouzdří vstup z konzole – bude vysvětleno v kapitole 13 o vstupních a výstupních proudech. Řádek 7 obsahuje příkaz pro vypsání promptu na konzoli a na řádku 8 je do proměnné radek načtena uživatelem zapsaná hodnotu. Kód na řádcích 6 a 8 může vyvolat výjimku typu IOException, která patří mezi kontrolované výjimky (podrobnosti o IOException viz kapitola 13). Musí tedy být uveden blok catch, který tento typ výjimek odchytí a zpracuje (viz řádky 12 až 14). Pravděpodobnost výskytu chyby vstupu při čtení z konzole je velmi malá, použijeme tedy jen jednoduchý výpis pomocí System.out.println(e) – vypíše se popis výjimky vráceny metodou toString(). Na řádku 9 je načtený řetězec převáděn na celé číslo typu int. Pokud uživatel zadá řetězec, který nelze na celé číslo převést, bude metodou parseInt() vyhozena výjimka typu NumberFormatException. Výjimka tohoto typu je potomkem RuntimeException, takže blok catch s tímto typem výjimky není překladačem vyžadován. Na řádcích 15 až 17 je uvedeno ošetření tohoto typu výjimky.
Výjimky
strana 123
1 public int getInt(String prompt) { 2 int cislo = 0; 3 String radek; 4 while (true) { 5 try { 6 BufferedReader vstup = new BufferedReader (new InputStreamReader(System.in)); 7 System.out.print(prompt+": "); 8 radek = vstup.readLine(); 9 cislo = Integer.parseInt(radek); 10 return cislo; 11 } 12 catch (IOException e) { 13 System.out.println(e); 14 } 15 catch (NumberFormatException e) { 16 System.out.println("chyba: nebylo vloženo celé číslo"); 17 } 18 } 19 } Doporučujeme nepoužívat prázdný blok catch, který výjimku zachytí a „zahodí“. Tím se ztratí potřebné informaci pro odhalení chyby. Také není vhodné odchytávat všechny výjimky jedním blokem catch, ve kterém budeme mít uvedenu třídu Exception. Lepší je uvést jednotlivé výjimky, neboť nám to umožňuje lépe ošetřit jednotlivé stavy.
12.3.2.
Throws
Pokud tvoříme metodu, ve které může dojít ke kontrolované výjimce, a my ji v rámci této metody nechceme nebo neumíme ošetřit, musíme překladači explicitně sdělit, že ji předáváme k ošetření do nadřazené úrovně, tj. metodě, která naši metodu vyvolala. Toho dosáhneme tím, že v hlavičce metody použijeme klíčové slovo throws28 a třídu výjimky (popřípadě více tříd). V následujícím příkladu metoda předává výše výjimku (a její potomky), která může vzniknout při čtení ze souboru: public String ctiVetu ( ) throws IOException { ... } Je vhodné uvádět throws i pro nekontrolované výjimky, neboť poté při čtení kódu je hned vidět, že může vzniknout výjimka. V každém případě by měly být výjimky popsány v komentáři k metodě.
12.4. Často používané typy výjimek Nejčastěji používané typy výjimek jsou uvedeny v tabulce 12.2. S výjimkou posledních dvou jsou všechny potomkem RuntimeException, tj. nemusí se povinně odchytávat.
28
Slovo throws se do češtiny často překládá jako odmítání a z tohoto důvodu se často používá termín odmítnutí výjimky – nám připadá vhodnější termín předání výjimky výš.
Výjimky
strana 124
výjimka
popis
IllegalArgumentException
Používá se v situaci, kdy byla metoda zavolána se "špatným" parametrem. Použijeme ji například při vytváření čtverce – při zadání záporné délky strany vyvoláme tuto výjimku.
NullPointerException
Vzniká v situaci, kdy identifikátor neobsahuje odkaz na instanci, ale konstantu null. Příklady situací: ♦ je volána metoda instance, ale identifikátor obsahuje hodnotu null, ♦ přistupuje se k datovému atributu instance, ale identifikátor má hodnotu null, ♦ přistupuje se k proměnné length pole, které ještě nebylo vytvořeno.
IllegalStateException
Používá se v situaci, kdy je zavolána přípustná metoda, ale instance je ve stavu, kdy metodu nelze provést. Např. pokud se po zavření souboru objeví požadavek na čtení věty, vznikne tato výjimka.
NumberFormatException
Vzniká při převodu řetězce na číslo a vstupní řetězec nelze na číslo převést. Např. při převodu následujících řetězců na int: "45gt" nebo "4.8".
ArrayIndexOutOfBoundException Zadaný index je mimo rozsah pole, blíže je popsáno v kapitole 10. ClassCastException
Chyba při přetypování instancí, tato výjimka je popsána v kapitole 11.
CloneNotSupported
Třída nepodporuje vytváření kopií instance, viz popis metody clone() v kapitole 6.
FileNotFoundException
Soubor neexistuje či nelze vytvořit, bližší popis je v kapitole 13.
IOException
Chyba vstupu/výstupu, předek více podrobných výjimek, např. FileNotFoundException, blíže viz kapitola 13.
Tabulka 12.2 Přehled nejčastěji používaných výjimek
Vstupy a výstupy
strana 125
13.Vstupy a výstupy 13.1. Základní principy práce se soubory Pro používání souborů v programu je potřeba zvládnout minimálně následující tři skupiny operací: ♦ čtení z textového souboru, ♦ zápis do textového souboru, ♦ operace na adresářové struktuře – nalezení souboru v adresáři, zjištění údajů o souboru, přejmenování souboru, výmaz souboru, založení adresáře, zrušení adresáře. Průběh čtení z textového souboru má ve většině programovacích jazyků strukturu zobrazenou na obrázku13.1.
otevření souboru
čtení první věty
konec souboru? true false zpracování věty
uzavření souboru
čtení další věty
Obrázek 13.1 Průběh čtení z textového souboru Tato struktura zajišťuje přečtení a zpracování všech vět v souboru i ošetření situace, kdy v souboru není ani jedna věta. Ostatní případné chyby (neexistence souboru, chyby na disku, atd.) je nutné ošetřovat jinak – většinou se používají výjimky, některé chybové stavy lze testovat předem (např. existenci souboru). Pro zápis do textového souboru je struktura programu méně formalizovaná. Před psaním do souboru je potřeba soubor otevřít. Obvykle je možné upřesnit, zda se vytváří nový soubor, přepisuje stávající soubor či zda se bude zapisovat na konec existujícího souboru. V průběhu aplikace je poté možné zapisovat do souboru jednotlivé řádky. Nesmí se zapomenout na uzavření souboru – pokud se soubor explicitně neuzavře, obvykle chybí část textu ve vytvořeném souboru. Ošetřování chyb je podobné, jako při čtení souboru. Operace nad adresářovou strukturou jsou v jednotlivých jazycích implementovány různě, v Javě je většina těchto operací v samostatné třídě File.
Vstupy a výstupy
strana 126
13.2. Vstupy a výstupy v Javě Pro práci se vstupy a výstupy nám Java poskytuje celou řadu tříd a jejich metod. Základní třídy jsou uloženy v balíčku java.io, další lze nalézt jinde29. Koncepce vstupu a výstupu je založena na mechanizmu tzv. vstupních a výstupních proudů (stream) a jejich obalování dalšími třídami (filtry) pro přidání další funkčnosti30. Třídy pro práci se soubory lze rozdělit do následujících skupin: abstraktní třída (předek)
třídy pracující s konkrétními typy úložišť dat
filtry
poznámky
čtení po bytech
InputStream
FileInputStream, PipedInputStream, ByteArrayInputStream, …
BufferedInputStream, DataInputStream, ObjectInputStream, GZIPInputStream, DigestInputStream, CipherInputStream, AudioInputStream, …
čtení po znacích
Reader
FileReader, PipedReader, ByteArrayReader, …
BufferedReader, LineNumberReader, …
filtr InputStrea mReader převádí instanci potomka třídy InputStream na Reader
zápis po bytech
OutputStream
FileOutputStream, PipedOutputStream, ByteArrayOutputStream, …
PrintStream, BufferedOutputStream, ObjectOutputStream, DataOutputStream, GZIPOutputStream, DigestOutputStream, CipherOutputStream, …
zápis po znacích
Writer
FileWriter, PipedWriter, ByteArrayWriter, …
PrintWriter, BufferedWriter, ...
filtr OutputStreamWriter převádí instanci potomka třídy OutputStream na Writer
Tabulka 13.1 Rozdělení tříd pro práci s proudy do základních skupin Rozlišení tříd v závislosti na tom, zda pracují s byty či se znaky vychází z významu textových souborů a z používání 16–bitového kódování znaků v Javě. Pokud se mají číst či zapisovat řetězce (instance třídy String), měli by se používat potomci tříd Reader či Writer. V každé skupině je abstraktní třída, která je předkem ostatních a která definuje základní operace dostupné ve všech potomcích. Vstup/výstup je vždy vázán na konkrétní úložiště, ze kterého se mají údaje číst či kam se mají zapisovat. Nejčastěji se používají soubory, lze používat i rouru (Pipe), vyhrazenou část paměti (ByteArray) i další. Pro čtení ze sítě (ukládání na síť) nejsou k dispozici veřejné třídy, ale např. třída URL poskytuje metody pro získání konkrétní instance pro čtení ze sítě (zápis na síť) – ukázka použití sítě jako úložiště je na straně 132. Ve většině případů nám nepostačuje funkčnost základní třídy a chceme ji doplnit o další – buffrování proudů z důvodu výkonnosti, práci s celými řádky, podpora binárních dat, komprimace dat, šifrování dat, atd. Filtry jsou potomky příslušné abstraktní třídy, a tudíž se dají vzájemně zapouzdřovat do sebe v rámci příslušné skupiny. 29
Od verze 1.4 nabízí Java další třídy pro práci se souboru v balíčku java.nio – cílem těchto tříd je vyšší výkonnost v oblastech síťové komunikace, použití regulárních výrazů při čtení ze souborů, využití bufferů, podpora znakových sad. 30 Třídy pro vstup/výstup jsou typickou ukázkou použití návrhového vzoru decorator.
Vstupy a výstupy
strana 127
Zvláštní postavení mezi filtry mají třídy InputStreamReader a OutputStreamReader, které slouží pro převod ze čtení/zápisu po bytech do čtení/zápisu po znacích. Následující obrázek ukazuje zapouzdření tříd v případě, kdy se mají přečíst řádky z textového souboru na disku, který je zkomprimovaný pomocí metody GZIP:
BufferedReader InputStreamReader GZIPInputStream FileInputStream Obrázek 13.2 Zapouzdření tříd při čtení komprimovaného souboru Filtr pro dekomprimaci souboru je pouze pro čtení po bytech, z toho důvodu se použije pro otevření souboru třída FileInputStream, která se zabalí filtrem pro dekomprimaci (GZIPInputStream) a dále se převede na Reader pomocí třídy InputStreamReader. Výsledek se zapouzdří do třídy BufferedReader, která poskytuje metodu pro čtení po řádcích. Se třídami pro vstup/výstup jsou spojené kontrolované výjimky. Nejčastěji se odchytávají výjimky FileNotFoundException a obecná výjimka IOException.
13.3. Vstupní proudy Pro vstupy slouží proudy založené na třídách InputStream a Reader. V následujících tabulkách je uveden přehled tříd z balíčku java.io, které se týkají čtení ze vstupních proudů: třída
použití
FileInputStream
čtení ze souboru, parametrem konstruktoru je String se jménem souboru nebo objekt typu File
PipedInputStream
čtení z roury (do které zapisuje PipedOutputStream)
SequenceInputStream
vytvoří jeden vstupní proud ze dvou vstupních proudů, které jsou parametrem konstruktoru
ByteArrayInputStream
čtení z pole bytů v paměti, které je parametrem konstruktoru
Tabulka 13.2 Třídy z balíčku java.io pro vstup po bytech z jednotlivých uložišť třída
použití
BufferedInputStream
vytváří buffer pro čtení, čímž je toto čtení efektivnější
DataInputStream
čte data z binárního souboru (který je ve formátu přenositelném mezi různými platformami), soubor lze vytvářet pomocí třídy DataOutputStream
PushbackInputStream
umožňuje vrátit část přečtených bytů zpět do vstupního proudu
Tabulka 13.3 Třídy (filtry) z balíčku java.io přidávající funkčnost pro čtení po bytech Pro čtení po znacích je deklarována abstraktní třída Reader a její potomci. Měly by se používat vždy, kdy se čte text, neboť v této třídě je garantována správná obsluha znakových sad a převod textu do vnitřního kódování Javy (do znakové sady Unicode). V tabulce13.4. je uveden přehled tříd pro vytvoření konkrétních potomků třídy Reader a jejich srovnání s potomky třídy InputStream:
Vstupy a výstupy
strana 128
třída
použití
odpovídající InputStream
InputStreamReader
převádí InputStream na Reader
-
FileReader
čtení ze souboru, parametrem konstruktoru je String se jménem souboru nebo objekt typu File
FileInputStream
PipedReader
čtení z roury (z objektu, do kterého zapisuje PipedWriter)
PipedInputStream
CharArrayReader
čtení z pole znaků v paměti, které je parametrem konstruktoru
ByteArrayInputStream
StringReader
převede String na Reader
StringBufferInputStream
Tabulka 13.4 Třídy pro čtení po znacích z jednotlivých úložišť a jejich obdoba pro čtení po bytech třída
použití
odpovídající InputStream
BufferedReader
vytváří buffer pro čtení, současně poskytuje metodu readLine() pro čtení po řádcích
BufferedInputStream
LineNumberReader
přidává metodu pro číslování čtených textových řádků
LineNumberInputStream
PushbackReader
umožňuje vrátit část přečtených znaků zpět do vstupního proudu
PushBackInputStream
Tabulka 13.5 Třídy (filtry) pro rozšíření funkčnosti potomků třídy Reader
13.3.1.
Čtení z textového souboru
Pokud chceme číst po řádcích textový soubor uložený na disku, je nutné si vytvořit a zapouzdřit vhodný vstupní proud. Prvním krokem je vytvoření instance třídy FileReader – pro čtení souboru A.TXT by vytvoření instance vypadalo následovně: FileReader vstupZn = new FileReader ("A.TXT");
Instance třídy FileReader podporuje čtení po znacích pomocí metody read() int znak = vstupZn.read();
Ukázka čtení ze souboru po znacích31 je uvedena dále v této kapitole na straně 133. Protože chceme číst po řádcích, je potřeba zabalit instanci třídy FileReader do filtru BufferedReader, který poskytuje metodu readLine() pro čtení jednotlivých řádků: BufferedReader vstupRad = new BufferedReader (vstupZn); String radek = vstupRad.readLine(); // čtení první řádky
Pokud metoda readLine() při čtení řádku zjistí, že je konec souboru, vrátí hodnotu null. Pokud není konec souboru, vrátí metoda readLine() instanci třídy String obsahující přečtený řádek. V našem případě se výsledek metody readLine() ukládá do proměnné radek. Tím máme
31
Metoda read() vrací přečtený znak, při zjištění konce souboru vrátí hodnotu −1. To je důvod, proč metoda read() vrací hodnoty typu int a ne char – při přečtení znaku vrací kladné číslo či nulu, které lze převést na typ char, záporné hodnotě −1 žádný znak neodpovídá.
Vstupy a výstupy
strana 129
k dispozici základní kameny pro vytvoření cyklu while pro čtení řádek s testem na konec souboru. Pro uzavření souboru je nutné zavolat metodu close()32. while (radek != null) { … zpracování řádky … radek = vstupRad.readLine(); } vstupRad.close();
Základní cyklus pro čtení souboru se v Javě občas zapisuje zkráceně (otvírání a zavírání souboru a obsluha výjimek zůstávají stejné). Je to ukázáno v následujícím příkladu, ve kterém se přečtený řádek vypíše na standardní výstup. String radek; while ((radek = vstup.readLine()) != null) { System.out.println (radek); } vstup.close();
Chyby při čtení souboru se odchytávají pomocí výjimek FileNotFoundException a IOException. Tyto výjimky je nutné odchytit, neboť patří mezi kontrolované výjimky (potomci třídy Exception, ale ne RunTimeException – viz kapitola věnovaná výjimkám). Výjimka FileNotFoundException upozorňuje na nejčastější chybu při čtení souboru – neexistenci vstupního souboru. Výjimka FileNotFoundException je potomkem třídy IOException a proto musí být uvedena před výjimkou IOException (při odchytávání výjimek se jde od konkrétních k obecným). Výjimka IOException vznikne při obecné chybě vstupu/výstupu. Lze ji použít i pro odchycení dalších chyb vstupu/výstupu, neboť je předkem ostatních výjimek vstupu/výstupu (ztrácejí se přitom ale informace týkající se konkrétního typu výjimky). Bez ošetření výjimky IOException není tento program přeložitelný. Následující kód ukazuje, jak přečíst po řádcích textový soubor A.TXT a vypsat ho na konzolu. try { BufferedReader vstup = new BufferedReader (new FileReader ("A.TXT")); String radek; radek = vstup.readLine(); while (radek != null) { System.out.println (radek); radek = vstup.readLine(); } vstup.close(); } catch (FileNotFoundException e) { System.out.println ("Soubor A.TXT neexistuje"); } catch (IOException e){ System.out.println ("Chyba na vstupu souboru A.TXT"); } Zkuste si na papíře simulovat průběh obou variant algoritmu pro čtení z textového souboru na souboru se třemi řádky a na prázdném souboru.
13.3.2.
Čtení z konzole
Pro čtení z konzole lze použít systémovou proměnnou System.in, což je instance třídy InputStream a tudíž je možno číst pouze po bytech. Tento standardní vstup otevírá JVM vždy při 32
Metoda close() je k dispozici i ve třídě FileReader při čtení po znacích.
Vstupy a výstupy
strana 130
své inicializaci (stejně jako proměnnou System.out pro standardní výstup). Pro přečtení řádky z konzole je tedy nutné tento proud obalit potřebnými filtry. Nejprve se převede vstup ze čtení po bytech na čtení po znacích zabalením do instance třídy InputStreamReader: InputStreamReader ctiZnak = new InputStreamReader(System.in);
Pro čtení po řádcích tento vstup zabalíme ještě do instance třídy BufferedReader jako při čtení ze souboru. Standardní vstup se zavírá automaticky při skončení programu, není tedy nutné použít metodu close(). Stejně jako při čtení ze souboru musíme ošetřit výjimky IOException. Následující příklad přečte jednu řádku z konzole: System.out.print("Zadej text: "); try { BufferedReader cti =new BufferedReader (new InputStreamReader(System.in)); String radek = cti.readLine(); System.out.println (radek); } catch (IOException e) { System.out.println("chyba vstupu"); }
13.4. Výstupní proudy Obdobně jako u vstupu lze třídy pro výstup rozdělit do čtyř skupin: třídy pro vytvoření výstupního proudu pro zápis po bytech (OutputStream), třídy pro rozšíření funkčnosti výstupního proudu, třídy pro vytvoření výstupu po znacích (Writer) a třídy pro rozšíření funkčnosti při výstupu po znacích. třída
použití
FileOutputStream
zápis do souboru, parametrem konstruktoru je String se jménem souboru nebo objekt typu File, při použití druhého parametru typu boolean lze přidávat na konec existujícího souboru,
PipedOutputStream
zápis do roury (ze kterého čte PipedInputStream)
ByteArrayOutputStream
zápis do pole bytů v paměti, které je parametrem konstruktoru
Tabulka 13.6 Třídy z balíčku java.io pro zápis do jednotlivých uložišť po bytech třída
použití
BufferedOutputStream
vytváří buffer pro efektivnější zápis
DataOutputStream
do výstupního proudu zapisuje proměnné a objekty v binárním formátu přenositelném mezi platformami, vytvořené soubory lze číst přes DataInputStream či v jiných programovacích jazycích
PrintStream
vypisuje textovou reprezentaci proměnných a objektů pomocí metod print(), println() a printf()
Tabulka 13.7 Třídy (filtry) z balíčku java.io pro přidání funkčnosti při zápisu po bytech
Vstupy a výstupy
strana 131
třída
použití
odpovídající OutputStream
OutputStreamWriter
převádí OutputStream na Writer
-
FileWriter
zápis do souboru, parametrem konstruktoru je String se jménem souboru nebo objekt typu File
FileOutputStream
PipedWriter
zápis do roury (do objektu, ze kterého čte PipedReader)
PipedOutputStream
StringWriter
zápis do bufferu, který může být převeden do objektu String či StringBuffer
-
CharArrayWriter
zápis do pole znaků v paměti
ByteArrayOutputStream
Tabulka 13.8 Třídy z balíčku java.io pro zápis do jednotlivých uložišť po znacích třída
použití
odpovídající OutputStream
BufferedWriter
vytváří buffer pro efektivnější zápis
BufferedOutputStream
PrintWriter
vypisuje textovou reprezentaci proměnných a objektů pomocí metod print(), println() a printf()
PrintStream
Tabulka 13.9 Třídy (filtry) z balíčků java.io pro rozšíření funkčnosti při zápisu po znacích
13.4.1.
Zápis do textového souboru
Pro zápis do textového souboru je potřeba vytvořit instanci potomka třídy Writer, který umí zapisovat do souboru na disku – vytvořit instanci třídy FileWriter, kde parametrem konstruktoru bude String se jménem souboru33. Pokud soubor na disku neexistuje, bude vytvořen nový, pokud existuje, bude přepsán. Jestliže chceme do již existujícího souboru připisovat na konec, musíme použít konstruktor třídy FileWriter se dvěma parametry. Druhým parametrem je logická hodnota, která určuje, zda budeme zapisovat za konec souboru (true) nebo původní soubor přepisovat (false). Pro zápis po znacích do souboru A.TXT bude řádek kódu s vytvořením instance třídy FileWriter vypadat takto: FileWriter vystupZn = new FileWriter ("A.TXT");
Takto připravený výstup nám umožní zápis po znacích. Výhodnější je však zapisovat po řádcích, proto použijeme ještě filtr PrintWriter, který podporuje převody proměnných a objektů do textového tvaru (např. číslo typu int převede do textové reprezentace) a který má metodu println() pro zápis celého řádku. PrintWriter vystup = new PrintWriter (vystupZn);
Po skončení zápisu nesmíme zapomenout na metodu close() pro zavření souboru, jinak může dojít ke ztrátě části dat. Jako u každé práce s vstupy a výstupy je nutné ošetřit výjimky. Následující příklad ukazuje zapsání deseti řádek do textového souboru na disk po řádcích (metodou println()).
33
Parametrem konstruktoru třídy FileWriter může být též instance třídy File popisující soubor.
Vstupy a výstupy
strana 132
try { PrintWriter vystup = new PrintWriter (new FileWriter("A.TXT")); for (int i=0; i<10 ; i++) vystup.println("řádek "+i); vystup.close(); } catch (IOException e) { System.out.println("Chyba při zápisu"); }
Výstup na konzolu (System.out) nemusíme nijak "zabalit", protože autoři Javy použili pro statickou proměnnou System.out typ PrintStream, tj. třídu která umí zapisovat celé řádky (tj. má k dispozici metodu println()).
13.5. Další třídy a metody pro práci s proudy Základy mechanismu práce se vstupními a výstupními proudy jsou definovány v balíčku java.io. Existují další třídy a metody pro vytváření vstupních proudů (např. pro čtení ze sítě, pro čtení BLOB z databází) i další třídy pro přidání funkčnosti k proudům (např. komprimace či šifrování).
13.5.1.
Čtení ze sítě
Nejdříve si ukážeme, jak číst soubor ze sítě. Základem je třída URL, v rámci které se zadává adresa souboru, ke kterému chceme přistupovat. Nejjednodušší je zadat textovou adresu jako parametr konstruktoru: URL mojeURL = new URL("http://www.vse.cz/index.html");
Pokud zadáme špatný parametr, vyvolá konstruktor kontrolovanou výjimku MalformedURLException. Instance třídy URL může vytvořit vstupní proud následujícím způsobem: InputStream is = mojeURL.openStream();
S takto vytvořeným proudem můžeme pracovat jako s kterýmkoliv jiným proudem. Následující příklad vypíše soubor na URL http://www.vse.cz/index.html na standardní výstup (porovnejte s prvním příkladem v této kapitole na straně 129): try { URL mojeURL = new URL("http://www.vse.cz/index.html”); InputStream is = mojeURL.openStream(); BufferedReader vstup = new BufferedReader (new InputStreamReader (is)); String radek = vstup.readLine(); while (radek != null) { System.out.println (radek); radek = vstup.readLine(); } vstup.close(); } catch (MalformedURLException e) { System.out.println ("Chybne URL: "+mojeURL); } catch (IOException e){ System.out.println ("Chyba na vstupu"); e.printStackTrace(); }
Vstupy a výstupy
13.5.2.
strana 133
Komprimace a dekomprimace souborů
Třídy a metody pro komprimaci a dekomprimaci souborů typu ZIP a GZIP jsou v balíčku java.util.zip. Následující příklad zkomprimuje vstupní soubor, jehož jméno je zadáno jako parametr na příkazové řádce do výstupního zkomprimovaného souboru test.gz. Všimněte si, jak je uděláno zapouzdření potřebných filtrů při vytváření výstupního souboru. import java.io.*; import java.util.zip.*; public class GZIPCompress { public static void main (String [] args){ if (args.length == 0) { System.out.println("Nutno zadat jmeno souboru"); System.exit(1); } try { BufferedReader vstup = new BufferedReader ( new FileReader(args[0])); BufferedOutputStream vystup = new BufferedOutputStream ( new GZIPOutputStream ( new FileOutputStream("test.gz"))); int znak; znak = vstup.read(); while (znak != -1) { vystup.write(znak); znak = vstup.read(); } vstup.close(); vystup.close(); } catch (FileNotFoundException e) { System.out.println("Soubor "+args[0]+" nelze otevrit"); } catch (IOException e){ System.out.println ("Chyba na vstupu/vystupu"); e.printStackTrace(); } } }
13.6. Třída File Třída File slouží pro manipulaci se soubory a adresáři. Instance třídy File může odkazovat na adresář nebo soubor. Při vytváření instance není nutné, aby soubor (případně adresář) fyzicky existoval na disku – třída poskytuje metody pro vytváření souborů a adresářů, pro testování existence. Pro oddělování adresářů v popisu cesty používáme lomítko /, ve Windows lze použít i zpětné lomítko (ve zdrojovém textu programu se musí uvést dvě zpětná lomítka, neboť zpětné lomítko má i speciální význam v řetězcích). Lze použít i statickou proměnnou File.separator, která obsahuje oddělovač v závislosti na operačním systému. Instanci třídy File je možné vytvořit pomocí tří různých konstruktorů: ♦ File(String jmeno); ♦ File(String cesta, String jmeno); ♦ File(File adresar, String jmeno);
Vstupy a výstupy
strana 134
příklad použití
význam
File mujSoubor = new File ("a.txt")
instance mujSoubor nastavena na soubor a.txt v aktuálním adresáři
File mojeDopisy = new File("C:"+File.separator+ "dopisy")
instance mojeDopisy nastavena na adresář dopisy na disku C (popř. na soubor, neboť z kódu není poznat, zda se jedná o adresář či o soubor)
File mujDopis = new File("C:/dopisy/dopis1.txt")
instance mujDopis nastavena na soubor dopis1.txt v adresáři dopisy na disku C
File mujDopis = new File("C:\\dopisy","dopis1.txt")
instance mujDopis nastavena na soubor dopis1.txt v adresáři dopisy na disku C
File dopis = new File(mojeDopisy,"dopis1.txt")
instance mujDopis nastavena na soubor dopis1.txt v adresáři dopisy na disku C, použili jsme instanci mojeDopisy vytvořenou dříve
Tabulka 13.10 Použití konstruktorů třídy File Pokud chceme ověřit fyzickou existenci souboru nebo adresáře, použijeme u instance třídy File metodu exists(). Třída dále obsahuje metody isFile() a isDirectory(), které zjistí, zda daná instance třídy File je soubor či adresář (musí fyzicky existovat na disku, není nutné předtím volat test exists()). Všechny tyto tři metody vracejí hodnotu typu boolean. Pro vytvoření adresáře slouží metoda mkdir(), pro vytvoření souboru metoda createNewFile(). Zjistit velikost existujícího souboru nebo adresáře můžeme pomocí metody length(). Soubor nebo adresář je možno smazat metodou delete() nebo přejmenovat pomocí metody renameTo(). Pro výpis adresáře slouží metoda list(), která vrací pole řetězců se jmény souborů a adresářů. Následující program vypíše obsah adresáře dokumenty na disku C. File adresar = new File ("C:/dokumenty"); String [] obsah = adresar.list(); for (int i = 0;i < obsah.length;i++) System.out.println(obsah[i]); }
Nyní program upravíme tak, aby vypsal pouze adresáře obsažené v adresáři dokumenty. File adresar = new File ("C:/dokumenty"); String [] obsah = adresar.list(); for (int i = 0;i < obsah.length;i++){ File prvek = new File (adresar,obsah[i]); if (prvek.isDirectory()) { System.out.println(obsah[i]); }}
Pokud chceme použít pro výpis masku (tj. vypsat pouze některé soubory či adresáře na základě nějaké podmínky), lze v metodě list() uvést jako parametr instanci třídy, která implementuje rozhraní FilenameFilter. Pro využití této možnosti musíme napsat třídu, která bude implementovat toto rozhraní, a bude obsahovat metodu boolean accept (File dir, String name). Tato metoda se poté bude volat pro každý nalezený soubor v adresáři a měla by vracet hodnotu true pro každý soubor, který se má vypisovat.
Projekt Obrázek
strana 135
14.Projekt Obrázek 14.1. Základní popis, zadání úkolu Pracujeme na projektu Obrázek, který je ke stažení na http://java.vse.cz/. Po otevření v BlueJ vytvoříme instanci třídy Obrazek a zavoláme metodu kresli(). Výsledkem je obrázek domečku:
Obrázek 14.1 Obrázek domečku z projektu Obrázek Naším úkolem je: ♦ přidat do obrázku sluníčko, ♦ doplnit obsah metody, po jejímž zavolání projede na obrázku před domečkem auto, ♦ doplnit metodu pro východ slunce a metodu pro západ slunce. Tento projekt má následující cíle: ♦ ukázat doplnění datového atributu (slunce) a příslušného kódu do projektu, ♦ na třídě Auto ukázat, že pro vytvoření instance (objektu) může být více konstruktorů, ♦ na doplnění auta do obrázku ukázat rozdíl mezi lokální proměnnou a datovým atributem, ♦ ukázat, jak doplnit obsah předpřipravené metody (prujezdAuta()), ♦ ukázat, jak doplnit celé metody (vychodSlunce() a zapadSlunce()), ♦ ukázat volání jiné metody stejné instance (volání setCernoBily() v metodě zapadSlunce()).
14.2. Struktura tříd Projekt Obrázek se skládá ze šesti tříd: Platno – tato třída představuje prostor (plátno), na kterém se vykreslují jednotlivé tvary, Kruh, Ctverec, Trojuhelnik – toto jsou jednotlivé tvary, které se kreslí na plátno, lze s nimi po plátně pohybovat, měnit jejich velikost a barvu, Auto – obdoba předchozích tříd – na plátně se vykreslí auto, Obrazek – představuje obrázek domečku – vykreslí dva čtverce (zeď a okno) a jeden trojúhelník na plátně.
Projekt Obrázek
strana 136
Následuje diagram tříd (obrázek z BlueJ):
Obrázek 14.2 Diagram tříd projektu Obrázek, převzato z BlueJ
14.3. Popis metod jednotlivých tříd Nejprve popíšeme metody tříd Auto, Ctverec, Kruh a Trojuhelnik. Třídu Platno podrobně popisovat nebudeme, třída zajišťuje plochu o velikosti 300x300 bodů pro vykreslování jednotlivých instancí tříd Auto, Ctverec, Kruh, Trojuhelnik a Obrazek. Jedna instance této třídy je společná pro všechny vykreslované tvary. třída
konstruktor
popis konstruktoru
Ctverec
Ctverec()
vytvoří červený čtverec o straně 30 bodů na souřadnicích 60,50 a vykreslí ho na plátno
Kruh
Kruh()
vytvoří modrý kruh o průměru 30 bodů na souřadnicích 20,60 a vykreslí ho na plátno
Trojuhelnik
Trojuhelnik()
vytvoří zelený trojúhelník o šířce 40 a výšce 30 bodů na souřadnicích 50,15 a vykreslí ho na plátno
Auto
Auto()
vytvoří modré auto na souřadnicích 60,40 a vykreslí ho na plátno
Auto(int xPozice,int yPozice )
vytvoří modré auto na zadaných souřadnicích
Tabulka 14.1 Přehled konstruktorů tříd projektu
Projekt Obrázek
strana 137
metoda
popis metody
posunVpravo()
posune daný tvar o 20 bodů vpravo
posunVlevo()
posune daný tvar o 20 bodů vlevo
posunNahoru()
posune daný tvar o 20 bodů nahoru
posunDolu()
posune daný tvar o 20 bodů dolu
posunHorizontalne(int vzdalenost)
posune daný tvar o zadaný počet bodů (+ vpravo, − vlevo)
posunVertikalne(int vzdalenost)
posune vertikálně daný tvar o zadaný počet bodů (+ dolů, − nahoru)
pomaluPosunHorizontalne(int vzdalenost)
pomalé posunutí tvaru o zadaný počet bodů horizontálně
pomaluPosunVertikalne(int vzdalenost)
pomalé posunutí tvaru o zadaný počet bodů vertikálně
zmenVelikost(int novaVelikost)
změní velikost daného tvaru, tato metoda neexistuje u třídy Auto (plátno umí nakreslit jen jednu velikost auta)
zmenBarvu(String novaBarva)
změní barvu zadaného tvaru
Tabulka 14.2 Přehled metod tříd Kruh, Ctverec, Trojuhelnik a Auto Vysvětlíme si následující část kódu s instancí třídy Ctverec: Ctverec zed; zed = new Ctverec(); zed.posunVertikalne(80); zed.zmenVelikost(100);
Na prvním řádku je deklarace proměnné zed typu Ctverec, na druhém řádku se vytvoří instance třídy Ctverec (zavolá se konstruktor) a odkaz se uloží do proměnné zed – čtverec se nakreslí na plátno. Na třetím řádku se instanci čtverce pošle zpráva, aby se posunula vertikálně o 80 bodů doprava – u instance, na kterou odkazuje proměnná zed se zavolá metoda posunVertikalne(80). Na čtvrtém řádku se zvětší velikost strany čtverce na velikost 100 bodů.
14.4. Kód třídy Obrazek 1 /** 2 * Tato třída reprezentuje jednoduchý obrázek. 3 * Obrázek se nakreslí po zavolání metody kresli. 4 * Obrázek může být změněn - můžete ho upravit na černobílý 5 * a zpět na barevný. 6 * Tato třida je napsána jako jeden z prvních příkladů 7 * pro výuku Javy v BlueJ. 8 * 9 * @author Michael Kolling 10 * @author Luboš Pavlíček 11 * @version 1.0 (15 July 2000) 12 * @version 1.1cz (30 July 2004) 13 */
Projekt Obrázek 14 public class Obrazek { 15 private Ctverec zed; 16 private Ctverec okno; 17 private Trojuhelnik strecha; 18 19 /** 20 * Konstruktor pro vytvoření instance třídy Obrazek 21 */ 22 public Obrazek() { 23 // zde není žádný obsah 24 // datové atributy mají automaticky počáteční hodnotu null 25 // a vlastní kreslení obrázku 26 //(a tim i inicializace datových atributů) 27 //je v metodě kresli() 28 } 29 /** 30 * Nakreslí obrázek. 31 */ 32 public void kresli() { 33 zed = new Ctverec(); 34 zed.posunVertikalne(80); 35 zed.zmenVelikost(100); 36 37 okno = new Ctverec(); 38 okno.zmenBarvu("cerna"); 39 okno.posunHorizontalne(20); 40 okno.posunVertikalne(100); 41 42 strecha = new Trojuhelnik(); 43 strecha.zmenVelikost(50, 140); 44 strecha.posunHorizontalne(60); 45 strecha.posunVertikalne(70); 46 } 47 48 /** 49 * změní obrázek na černobílý 50 */ 51 public void setCernoBily() { 52 if (zed != null) { // pouze pokud je již nakreslen 53 zed.zmenBarvu("cerna"); 54 okno.zmenBarvu("bila"); 55 strecha.zmenBarvu("cerna"); 56 } 57 } 58 59 /** 60 * změní obrázek zpět na barevný 61 */ 62 public void setBarevny() { 63 if (zed != null) { // pouze pokud je již nakreslen domeček 64 zed.zmenBarvu("cervena"); 65 okno.zmenBarvu("cerna"); 66 strecha.zmenBarvu("zelena"); 67 } 68 } 69
strana 138
Projekt Obrázek
strana 139
70 /** 71 * při zavolání této metody by mělo před domečkem projet auto 72 */ 73 public void prujezdAuta() { 74 if (zed != null) { // pouze pokud je již nakreslen domeček 75 // ZDE DOPLNIT PŘÍSLUSNÝ KÓD 76 } 77 } 78 }
Třída Obrazek má tři datové atributy (viz řádky 15 až 17 kódu): ♦ zed a okno typu Ctverec, ♦ atribut strecha typu Trojuhelnik. Tyto datové atributy vyjadřují základní prvky, ze kterých je sestaven obrázek. Na rozdíl od tříd Ctverec, Kruh a Trojuhelnik se instance třídy Obrazek nezobrazí na plátně po zavolání konstruktoru, ale až po zavolání metody kresli(). Konstruktor třídy Obrazek je proto prázdný. Metoda kresli() vytvoří instance jednotlivých částí obrázku, nastaví jejich umístění, velikost a barvu pomocí příslušných metod. Metody pro změnu na černobílý (metoda setCernoBily() na řádcích 51 až 57) a barevný (metoda setBarevny() na řádcích 62 až 67) překreslují již vytvořený obrázek. Podmínka na začátku každé z těchto metod zjišťuje, zda je již nakreslena zeď (tj. zda byla vytvořena instance v metodě kresli() a uložena do proměnné zed). Pokud zavoláte metodu setBarevny() nebo metodu setCernoBily() před zavoláním metody kresli(), nic se nestane.
14.5. Postup řešení 14.5.1.
Dokreslení obrázku (přidání slunce)
Naším prvním úkolem je přidat do obrázku slunce, které bude v této fázi řešení na obrázku neustále na jednom místě. Při změně na černobílý obrázek nebude slunce zobrazeno (je noc) – barva slunce se změní na bílou. Slunce bude reprezentováno datovým atributem typu Kruh. Ve třídě Obrazek v části deklarace datových atributů (řádky 15 až 17 kódu) přibude následující řádek: private Kruh slunce;
Metoda kresli() se doplní o vytvoření instance třídy Kruh a změnu barvy, velikosti a umístění této instance. Kód by mohl vypadat takto: slunce = new Kruh(); slunce.zmenBarvu("zluta"); slunce.posunHorizontalne(180); slunce.posunVertikalne(-50); slunce.zmenVelikost(60);
Po přeložení, vytvoření instance třídy Obrazek a spuštění metody kresli() se zobrazí následující obrázek:
Projekt Obrázek
strana 140
Obrázek 14.3 Vzhled obrázku po přidání slunce Po úspěšném přeložení třídy Obrazek si všimněte, že se změnil diagram tříd zobrazený v BlueJ – automaticky přibyla vazba užití od třídy Obrazek ke třídě Kruh, neboť ve třídě Obrazek se nyní používá třída Kruh. Ještě je potřeba upravit metody setCernoBily() a setBarevny(). Do metody setCernoBily() přidáme řádek se změnou barvy slunce na bílou (nebude na bílém pozadí vidět) a v metodě setBarevny() řádek se změnou barvy slunce na žlutou.
14.5.2.
Průjezd auta
Pro řešení druhého úkolu byla již ve zdrojovém kódu třídy Obrazek předpřipravena metoda prujezdAuta(), při jejím zavolání by před domečkem mělo projet auto. Je potřeba se rozhodnout, zda vytvořená instance třídy Auto bude datovým atributem či lokální proměnnou. Auto bude na obrázku pouze tehdy, když bude spuštěna tato metoda; nebude potřeba v žádné jiné metodě, při novém zavolání metody může vzniknout a projet nové auto. Z toho vyplývá, že instance třídy Auto může být lokální proměnná metody. V metodě prujezdAuta() vytvoříme instance třídy Auto a zavoláme metodu pro jeho pomalý posun. Kód metody prujezdAuta() vidíte v následujícím výpisu: 1 public void prujezdAuta() { 2 if (zed != null) { // pouze pokud je již nakreslen domeček 3 Auto ford = new Auto(0,240); 4 ford.pomaluPosunHorizontalne(300); 5 } 6 }
Animace auta je velmi nedokonalá – při průjezdu auto bliká, při nesprávném umístění bude auto umazávat domeček atd. V rámci tohoto jednoduchého příkladu však tyto problémy nebudeme řešit, jejich odstranění by vyžadovalo použití vícevrstvého plátna. Po úspěšném přeložení třídy Obrazek se do diagramu tříd zobrazeného v BlueJ promítne další změna, přibude vazba užití od třídy Obrazek ke třídě Auto.
14.5.3.
Východ a západ slunce
Pro práci se sluncem si lze vytvořit dvě představy: ♦ každé ráno vznikne nové slunce, při západu slunce zaniká (pohled „země-placka“) – v této variantě v metodě vychodSlunce() vzniká instance třídy Kruh, která představuje slunce,
Projekt Obrázek
strana 141
v metodě zapadSlunce() tato instance zaniká. Instance kruhu se vytváří také na začátku (v metodě kresli()), neboť se začíná dnem a ne nocí, ♦ slunce obíhá kolem dokola, večer zapadne, ráno vyjde (pohled „geocentrický“34) – v této variantě vzniká pouze jedna instance, při západu slunce se příslušný kruh přesune mimo viditelnou část plátna, při východu se přesune na viditelnou část plátna. Řešení metod ve variantě „země-placka“ by mohlo vypadat následovně: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
/** * Při zavolání metody vyjde slunce, pokud již není na obloze. */ public void vychodSlunce() { if (slunce == null) {//zapadlé slunce se pozná dle hodnoty null slunce = new Kruh(); slunce.zmenBarvu("zluta"); slunce.posunHorizontalne(-60); slunce.posunVertikalne(-10); slunce.zmenVelikost(60); slunce.pomaluPosunHorizontalne(60); setBarevny(); // slunce je již na obrázku, nastává den slunce.pomaluPosunHorizontalne(180); } } /** * Při zavolání metody zapadne slunce, pokud je na obloze. */ public void zapadSlunce () { if (slunce != null) { // slunce již musí existovat slunce.pomaluPosunHorizontalne(90); setCernoBily(); slunce.pomaluPosunHorizontalne(30); slunce=null; // zde se zruší odkaz na instanci slunce } }
V metodě vychodSlunce() nejprve zjistíme, jestli je slunce na obrázku nebo ne (řádek 5 výpisu). Pokud na obrázku žádné slunce není (proměnná slunce odkazuje na hodnotu null), vytvoříme instanci třídy Kruh, nastavíme barvu, posuneme kruh na kraj plátna a změníme jeho velikost (řádky 6 až 10 výpisu). Poté spustíme animaci východu slunce pomocí metody pomaluPosunHorizontalne(). Slunce popojde kousek po obrázku a nastane den – pro obarvení obrázku zavoláme metodu setBarevny(). Na řádku 13 slunce pokračuje ve svém pohybu po obloze. Metoda pro západ slunce je velmi podobná. Pokud je slunce na obrázku (proměnná slunce odkazuje na instanci, tj. nemá hodnotu null), začne se pohybovat, obrázek se změní s barevného na černobílý a slunce zapadne úplně. Poté je instance zrušena, protože odkaz na ni je nastaven na hodnotu null.
34
Pohled „heliocentrický“ je pro naši úlohu obtížně použitelný, neboť obrázek je kreslen z perspektivy uživatele na zemi.
Projekt Obrázek
strana 142
Povšimněte si, že pokud spouštíme jinou metodu z téže třídy, uvádíme pouze název metody a případné parametry. Metoda bude spuštěna na té instanci, která spouští metodu obsahující toto volání (instance pošle zprávu sama sobě). Na řádku 12 výpisu se obrázek změní z černobílého na barevný – provedou se na tomto místě všechny příkazy obsažené v metodě setBarevny(). Volání metody stejné instance by správně mělo obsahovat odkaz na sebe sama pomocí slova this:
this.setBarevny();
Java programátorovi usnadňuje práci tím, že nevyžaduje zápis pomocí this – překladač toto přiřazení do výsledného kódu doplní sám.
V geocentrické variantě je potřeba pomocí dalšího datového atributu sledovat, zda je den či noc – použijeme datový atribut jeDen typu boolean, který bude mít hodnotu true, pokud je den a false, pokud je noc. Instance třídy Kruh představující slunce vzniká pouze jednou, a to v metodě kresli(). V noci (na konci metody zapadSlunce()) se slunce přesune kolem zeměkoule na svoji výchozí pozici na východě. Deklarace a inicializace by mohla vypadat následovně: private boolean jeDen; // datový atribut ..... public void kresli() { ..... jeDen = true; // nastavení počáteční hodnoty // v metodě kresli ..... }
Vlastní kód metod zapadSlunce() a vychodSlunce() bude vypadat následovně: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public void zapadSlunce () { if (jeDen) { // je den slunce.pomaluPosunHorizontalne(80); setCernoBily(); slunce.pomaluPosunHorizontalne(20); slunce.posunHorizontalne(-360); // posunu do výchozí pozice jeDen = false; // poznačím si, že je noc } } public void vychodSlunce() { if (! jeDen) { // je noc slunce.pomaluPosunHorizontalne(20); setBarevny(); // slunce je již na obrázku, nastává den slunce.pomaluPosunHorizontalne(240); jeDen = true; // poznačím si, že je den } }
14.6. Domácí úkoly 1. Přidejte do projektu metody pro příjezd a odjezd auta. 2. Nakreslete si vlastní obrázek, např. sněhuláka či stromek.
Projekt Kalkulačka
strana 143
15.Projekt Kalkulačka 15.1. Základní popis, zadání úkolu Pracujeme na projektu Kalkulačka, který je ke stažení na java.vse.cz. Po otevření v BlueJ vytvoříme instanci třídy Kalkulacka a zavoláme metodu show(). Výsledkem je spuštění grafické aplikace představující jednoduchou kalkulačku viz obrázek15.1.
Obrázek 15.1 Vzhled kalkulačky po spuštění Naším úkolem bude „naučit kalkulačku počítat“, zatím neumí nic. Konkrétně to znamená doplnit datové atributy a dokončit předpřipravené metody. Tento projekt má následující cíle: ♦ ukázat jednu z možností oddělení grafického rozhraní od věcné třídy, ♦ navrhnout datové atributy pro třídu Kalkulátor, ♦ procvičit základní operace s primitivními datovými typy (čísla, znaky, logická hodnota), ♦ naučit se příkaz if a vytváření podmínek.
15.2. Struktura tříd Projekt Kalkulačka se skládá z následujících tříd (obrázek z BlueJ):
Obrázek 15.2 Diagram tříd projektu Kalkulačka, převzato z BlueJ
Projekt Kalkulačka
strana 144
15.3. Popis komunikace mezi objekty V konstruktoru třídy Kalkulacka se vytváří instance třídy Kalkulator a instance třídy GrafikaKalkulacky, která jako parametr při vzniku získá odkaz na vytvořenou instanci třídy Kalkulator. V metodě show() se zobrazí grafické rozhraní kalkulačky a předá se mu řízení komunikace. Komunikace mezi uživatelem a instancemi tříd GrafikaKalkulacky a Kalkulator probíhá v této posloupnosti: 1. Uživatel stiskne některou klávesu (na obrázku je to stisknutí tlačítka s hodnotou 3). 2. Instance třídy GrafikaKalkulacky vyhodnotí, které tlačítko to bylo a zavolá odpovídající metodu ze třídy Kalkulator. Na našem obrázku zavolá metodu cislice() a jako parametr jí předá číslo 3, které odpovídá stisknuté klávese. Tato metoda zpracuje poslaný údaj. 3. Instance třídy GrafikaKalkulacky zavolá metodu getHodnotaKZobrazeni() instance třídy Kalkulator. Tato metoda vrátí hodnotu, která má být zobrazena na displeji. 4. Instance třídy GrafikaKalkulacky zobrazí na displeji obdrženou hodnotu.
Obrázek 15.3 Znázornění komunikace mezi jednotlivými instancemi při stisknutí číslice 3 Třída GrafikaKalkulacky zajišťuje obsluhu grafického rozhraní a řízení aplikace, instance třídy Kalkulator provádí vlastní výpočty (vždy vypočte hodnotu, která se má zobrazit na displeji). Třída Kalkulator obsahuje pouze hlavičky metod, naším úkolem je doplnit potřebné datové atributy a obsah metod.
15.4. Popis kódu třídy Kalkulator 1 /** 2 * Tato třída provádí vlastní výpočty kalkulátoru. 3 * Třída má tři skupiny metod: 4 * a) pomocí metody getHodnotaKZobrazeni() grafická 5 * třída zjištuje, co má zobrazit na displeji, 6 * b) metody cislice(), plus(), minus(), rovnaSe() a vymaz() se 7 * volají při stisknutí příslušné klávesy na kalkulačce, 8 * c) metody getAutor() a getVerze() jsou informační. 9 * Třída není dokončena 10 * - úkolem studentů je tuto třídu dokončit, tj. navrhnout 11 * datové atributy instancí třídy a dokončit obsah metod. 12 * @author Luboš Pavlíček 13 * @version 1.0 (31 July 2004)
Projekt Kalkulačka 14 */ 15 public class Kalkulator { 16 17 /** 18 * konstruktor třídy 19 */ 20 public Kalkulator () { 21 // DOPLNIT TUTO METODU 22 } 23 24 /** 25 * Metoda vrací hodnotu, která se má zobrazit na displeji 26 * kalkulačky. Tato metoda se obvykle volá po zavolání metody 27 * odpovídající stisku tlačítka. 28 * 29 * @return hodnota k zobrazení 30 */ 31 public int getHodnotaKZobrazeni() { 32 // DOPLNIT TUTO METODU 33 return 1; 34 } 35 36 /** 37 * metoda se volá při stisknutí tlačítka s číslicí na 38 * kalkulačce. Parametrem je hodnota na stisknuté klávese. 39 * 40 * @param hodnota hodnota na stisknutém tlačítku, 41 * hodnota je v rozsahu od 0 do 9 42 */ 43 public void cislice(int hodnota) { 44 // DOPLNIT TUTO METODU 45 } 46 47 /** 48 * metoda se volá při stisknutí tlačítka "+" (plus) 49 * na kalkulačce 50 */ 51 public void plus() { 52 // DOPLNIT TUTO METODU 53 } 54 55 /** 56 * metoda se volá při stisknutí tlačítka "-" (minus) 57 * na kalkulačce 58 */ 59 public void minus() { 60 // DOPLNIT TUTO METODU 61 } 62 63 /** 64 * metoda se volá při stisknutí tlačítka "=" (rovná se) 65 * na kalkulačce 66 */ 67 public void rovnaSe() { 68 // DOPLNIT TUTO METODU 69 } 70
strana 145
Projekt Kalkulačka
strana 146
71 /** 72 * metoda se volá při stisknutí tlačítka "C" (clear) 73 * na kalkulačce 74 */ 75 public void vymaz() { 76 // DOPLNIT TUTO METODU 77 } 78 79 /** 80 * metoda vrací jméno autora, např. "autor: Jan Novák" 81 * 82 * @return řetězec se jménem autora 83 */ 84 public String getAutor() { 85 // UPRAVIT TUTO METODU 86 return "......."; 87 } 88 89 /** 90 * metoda vrací označení verze, např. "verze 1.0.3" 91 * 92 * @return řetězec s verzí programu 93 */ 94 public String getVerze() { 95 // UPRAVIT TUTO METODU 96 return "0.0"; 97 } 98 }
Jak je vidět ze zdrojového kódu, třída Kalkulator nic neumí. Ať je na kalkulačce stisknuto cokoli, vrací na displeji vždy hodnotu 1. Metoda getAutor() vrací "......." a metoda getVerze() vrací řetězec "0.0".
15.5. Postup řešení: Řešení si rozdělíme do několika kroků: ♦ doplnění autora a verze (toto sice nenapomůže počítání kalkulačky, ale zvýší sebevědomí programátora), ♦ vkládání čísla (číselné klávesy), ♦ výmaz čísla (klávesa C), ♦ sčítání dvou čísel (klávesy + a =), ♦ sčítání více čísel, ♦ odčítání.
15.5.1.
Doplnění autora a verze
Nejjednodušší je doplnění metod getAutor() a getVerze(). Do kódu na řádcích 86 a 96 stačí pouze doplnit správné údaje. Ověřte si zobrazování autora a verze – údaje by se měly zobrazovat po stisknutí tlačítka
Projekt Kalkulačka
15.5.2.
strana 147
Vkládání prvního čísla
Druhým krokem bude „naučit“ kalkulačku správně zobrazovat první zadávané číslo. Po každém stisku tlačítka na kalkulačce je zavolána metoda cislice(). V parametru hodnota dostaneme číslici, kterou uživatel zadal. Na následujícím obrázku vidíte, jak probíhá komunikace mezi instancemi tříd GrafikaKalkulacky a Kalkulator v situaci, kdy uživatel zadává číslo 35.
Obrázek 15.4 Znázornění komunikace mezi instancemi při zadávání čísla 35 Mezi jednotlivými stisky tlačítek si musíme pamatovat, co již bylo zadáno. Budeme tedy potřebovat datový atribut pro uchovávání vkládaného čísla (a současně čísla, které se bude vracet metodou getHodnotaKZobrazeni() k zobrazení na displeji). Toto číslo bude typu int (kalkulačka umí pouze celá čísla) a jeho deklarace může vypadat následovně: private int hodnotaKZobrazeni;
V konstruktoru přiřadíme do datového atributu počáteční hodnotu35 0. Ve výše uvedeném příkladu by po stisknutí klávesy 3 (tj. na konci provádění metody cislice()) měl tento datový atribut obsahovat hodnotu 3, po následném stisknutí klávesy 5 hodnotu 35. 35
Toto přiřazení není nutné – Java automaticky číselným datovým atributům přiděluje počáteční nulu. V případě lokálních proměnných to ale neplatí. I z tohoto důvodu je vhodné si zvyknout vždy přiřazovat počáteční hodnotu datovým atributům a lokálním proměnným.
Projekt Kalkulačka
strana 148
Postupným vkládáním jednotlivých číslic se skládá hodnota čísla. Číslo je zadáváno zleva, při zadání další číslice se předchozí hodnota zvětší o jeden řád (z jednotek budou desítky, z desítek stovky atd.) a přičte se k ní hodnota naposledy vložené číslice. Kód metody cislice() by mohl vypadat následovně: public void cislice(int hodnota) { this.hodnotaKZobrazeni = this.hodnotaKZobrazeni*10+hodnota; }
Proměnná hodnotaKZobrazeni je uvozena klíčovým slovem this, které zdůrazňuje, že se jedná o datový atribut této instance. Pokud nedochází ke kolizi jména datového atributu s lokální proměnnou či jménem parametru, není potřeba v Javě toto klíčové slovo uvádět. Kód proto může vypadat následovně: public void cislice(int hodnota) { hodnotaKZobrazeni = hodnotaKZobrazeni*10+hodnota; }
Všimněte si též parametru metody cislice() – deklarace se skládá z typu (int) a identifikátoru (hodnota). Důležitý je datový typ – při volání metody se kontroluje pouze typ. Identifikátor slouží pro programátora metody – pomocí tohoto identifikátoru se programátor na parametr metody odkazuje. Není problém tento identifikátor změnit, aniž by bylo potřeba něco měnit ve třídách/metodách, které tuto metodu volají. Metoda by mohla vypadat následovně: public void cislice(int cislo) { hodnotaKZobrazeni = hodnotaKZobrazeni*10+cislo; }
Metoda getHodnotaKZobrazeni() vrací obsah datového atributu hodnotaKZobrazeni, její kód bude vypadat následovně: public int getHodnotaKZobrazeni() { return hodnotaKZobrazeni; }
Přeložte aplikaci a vyzkoušejte kalkulačku. Nyní již můžete vkládat čísla do kalkulačky, narazíte však na dva problémy: ♦ pro zadání nového čísla je potřeba znovu spustit kalkulačku, ♦ při vložení většího počtu číslic dojde k přetečení rozsahu číselného typu int (přibližně 2 miliardy) – tento problém nebudeme řešit.
15.5.3.
Operace výmaz (C)
Po stisknutí klávesy výmaz (C) se volá metoda vymaz() a měla by se vynulovat hodnota na displeji. Průběh komunikace vidíte na obrázku 15.5. Řešení této situace je poměrně jednoduché – v metodě vymaz() by se měla vynulovat hodnota datového atributu hodnotaKZobrazeni. Kód metody vymaz() bude vypadat následovně: public void vymaz() { hodnotaKZobrazeni = 0; }
Projekt Kalkulačka
strana 149
Obrázek 15.5 Znázornění komunikace mezi instancemi při stisknutí tlačítka C
15.5.4.
Operace plus (+)
Nyní budeme řešit situaci, kdy uživatel vloží první číslo (např. 35) a stiskne tlačítko se znaménkem plus.
Po stisknutí tlačítka plus by měla na displeji zůstat zobrazena hodnota 35 nebo by se měla zobrazit nula. Varianta s nulou je jednodušší, proto s ní začneme. Musíme si zapamatovat první číslo momentálně uložené v datovém atributu hodnotaKZobrazeni. Při vkládání druhého čísla se obsah tohoto atributu přepíše, je tedy nutné uložit jeho obsah do dalšího datového atributu. Bude opět typu int a můžeme ho pojmenovat např. prvniOperand, deklarace bude vypadat takto: private int prvniOperand;
V konstruktoru mu přiřadíme jako počáteční hodnotu nulu. Metoda plus() vypadá nyní následovně: public void plus() { prvniOperand = hodnotaKZobrazeni; hodnotaKZobrazeni = 0; }
Po vložení dalšího čísla a stisknutí tlačítka rovná se (=) by se na displeji měl zobrazit výsledek. V metodě rovnaSe() se sečte první operand s aktuálně vloženým číslem a výsledek se uloží do datového atributu hodnotaKZobrazeni: public void rovnaSe() { hodnotaKZobrazeni = prvniOperand + hodnotaKZobrazeni; }
15.5.5.
Operace mínus (−)
Místo plus může uživatel stisknout klávesu minus:
Projekt Kalkulačka
strana 150
Obdobně jako u sčítání se výsledek počítá v metodě rovnaSe(). Abychom odlišili sčítání od odčítání, musíme v metodách plus() a minus() uložit nejen první operand, ale i typ operace. Pro uložení požadované operace se nabízí několik možností: ♦ použijeme datový atribut typu char, ♦ použijeme datový atribut typu String, ♦ použijeme datový atribut typu int a nadefinujeme konstanty pro jednotlivé operace, ♦ použijeme výčtový datový typ pro označení operací. Ukážeme si nejprve použití prvního řešení, potom i řešení dle třetí varianty. Druhá varianta je velmi podobná první, jen si musíme uvědomit, že String je referenční datový typ a pro porovnávání je třeba použít metodu equals() a ne operátory == či !=. Čtvrtou variantu si ukazovat nebudeme, použití výčtového typu bude ukázáno v projektu Trojúhelníky (viz kapitola 17 str. 168). Datový atribut typu char pro uložení typu operace pojmenujeme operator a jeho deklarace bude vypadat takto: private char operator;
V konstruktoru budeme atribut operator inicializovat hodnotou mezera (nezapomeňte, že znaky se uvozují apostrofy, ne uvozovkami). Tato hodnota bude znamenat, že není požadována žádná operace. V metodě plus() musíme přidat uložení typu operace do proměnné operator, metoda minus() je velmi podobná metodě plus(): public void plus() { prvniOperand = hodnotaKZobrazeni; hodnotaKZobrazeni = 0; operator='+'; } public void minus() { prvniOperand = hodnotaKZobrazeni; hodnotaKZobrazeni = 0; operator='-'; }
V kódu metody rovnaSe() je potřeba pomocí selekce if rozlišovat, zda uživatel stiskl tlačítko plus či minus. Po provedení výpočtu se do proměnné operator vloží mezera: public void rovnaSe() { if (operator == '+') { hodnotaKZobrazeni = prvniOperand + hodnotaKZobrazeni; } else { if (operator == '-') { hodnotaKZobrazeni = prvniOperand - hodnotaKZobrazeni; } } operator=' '; }
Projekt Kalkulačka
15.5.6.
strana 151
Komunikace mezi instancemi
Na obrázku 15.6 je sekvenční diagram zobrazující průběh komunikace mezi instancemi kalkulačky při zadávání výrazu 35 + 2 = 37. Na diagramu není znázorněn vstup od uživatele.
Obrázek 15.6 Diagram zobrazuje průběh komunikace mezi instancemi při zadávání 35+2=
15.5.7.
Vkládání dalších čísel
Pokud po stisknutí tlačítka rovná se (=) a zobrazení výsledku uživatel stiskne další číslici, tak se nezačne zobrazovat nové číslo, ale číslice se připojí na konec spočítaného výsledku. Tj. po následující posloupnosti kláves
Projekt Kalkulačka
strana 152
se na displeji zobrazí číslo 372. Pro ošetření této situace potřebujeme v metodě cislice() odlišit situaci, kdy se začne vkládat nová hodnota. Tento datový atribut můžeme pojmenovat např. noveCislo a bude typu boolean, tj. bude nabývat hodnot true (uživatel začíná vkládat nové číslo) nebo false (uživatel pokračuje vložením stávajícího čísla). V konstruktoru zinicializujeme tento datový atribut s hodnotou true (po spuštění kalkulačky začíná uživatel s vkládáním nového čísla). Na konci metody rovnaSe() se přiřadí hodnota true do proměnné noveCislo. public void rovnaSe() { if (operator == '+') { hodnotaKZobrazeni = prvniOperand + hodnotaKZobrazeni; } else { if (operator == '-') { hodnotaKZobrazeni = prvniOperand - hodnotaKZobrazeni; } } operator=' '; noveCislo = true; }
Musíme změnit i kód metody cislice(). Pokud je v atributu noveCislo hodnota true, musíme do atributu hodnotaKZobrazeni vložit obsah parametru metody a změnit obsah atributu noveCislo na false. Metoda cislice() bude vypadat takto: public void cislice(int hodnota) { if (noveCislo) { hodnotaKZobrazeni = hodnota; noveCislo = false; } else { hodnotaKZobrazeni = hodnotaKZobrazeni*10 + hodnota; } }
Stejnou proměnnou můžeme též využít v metodách plus() a minus() – po stisknutí klávesy plus zůstane zobrazena předchozí hodnota a po stisknutí další číslice se začne číslo vkládat od nuly. Metoda plus() by nyní vypadala takto: public void plus() { prvniOperand = hodnotaKZobrazeni; noveCislo = true; operator='+'; }
Metoda minus() bude doplněna o přiřazení hodnoty true do proměnné noveCislo. Existence nových datových atributů se musí projevit i v kódu metody vymaz(). Když uživatel stiskne tlačítko představující tuto operaci, musí se „vynulovat“ všechna nastavení. Kód metody bude vypadat takto: public void vymaz() { prvniOperand = 0; operator = ' '; hodnotaKZobrazeni = 0; noveCislo = true; }
Projekt Kalkulačka
15.5.8.
strana 153
Operace plus – pokračování
Zatím jsme uvažovali o situaci, že uživatel sčítal/odčítal jen dvě čísla. Uživatel může však sečíst více čísel:
Na začátku metody plus() musíme zjistit, zda uživatel v předchozím kroku již požadoval nějakou operaci nebo ne. Pokud ano, je třeba dříve zadanou operaci (může to být i minus) nejdříve provést, její výsledek zobrazit a též uložit jako prvniOperand. Kód metody plus() by mohl vypadat takto: public void plus() { if (operator == '+') { prvniOperand = prvniOperand + hodnotaKZobrazeni; } else { if (operator == '-') { prvniOperand = prvniOperand - hodnotaKZobrazeni; } else { prvniOperand = hodnotaKZobrazeni; } } operator='+'; hodnotaKZobrazeni = prvniOperand; noveCislo=true; }
Metoda minus() bude z větší části duplicitní s metodou plus(). Duplicit v kódu je však ještě více, velká část kódu metody rovnaSe() je také stejná. Z důvodu lepší udržovatelnosti a rozšiřovatelnosti kódu je vhodné shodný kód umístit do jedné privátní metody. Pokud do kalkulačky přidáme ještě násobení a dělení, budeme výpočty řešit pouze v jedné metodě. Metodu, do které přesuneme společný kód, nazveme vypocet(). Je to pomocná metoda pro jiné metody ze třídy Kalkulator a není třeba (není to ani vhodné) ji volat z jiných tříd – proto bude označena modifikátorem private. Kód metody vypocet() a upravený kód metod plus() a rovnaSe() vidíme na následujícím výpise. Kód metody minus() bude analogický s kódem metody plus(). /** * metoda se volá při stisknutí tlačítka "+" (plus) na kalkulačce */ public void plus() { vypocet(); hodnotaKZobrazeni=prvniOperand; noveCislo=true; operator='+'; } /** * metoda se volá při stisknutí tlačítka "=" (rovná se) na kalkulačce */ public void rovnaSe() { vypocet(); hodnotaKZobrazeni = prvniOperand; noveCislo=true; operator=' '; }
Projekt Kalkulačka
strana 154
/** * metoda spočítá mezivýsledek a uloží ho do proměnné prvniOperand */ private void vypocet () { if (operator == '+') { prvniOperand = prvniOperand + hodnotaKZobrazeni; } else { if (operator == '-') { prvniOperand = prvniOperand - hodnotaKZobrazeni; } else { prvniOperand = hodnotaKZobrazeni; } } }
15.5.9.
Řešení s konstantami
Řešení, ve kterém jsou pro označení operací místo znaků (typu char) použity pojmenované konstanty, se bude od předchozího lišit v několika drobnostech. Konstanty pro jednotlivé operace deklarujeme jako datové atributy s modifikátorem final: private final int ZADNA_OPERACE = 0; private final int OPERACE_PLUS = 1; private final int OPERACE_MINUS = 2;
Datový atribut operator bude typu int a v konstruktoru nastavíme jeho počáteční hodnotu na ZADNA_OPERACE. V metodách plus(), minus() a rovnaSe() budeme místo znaků přiřazovat do tohoto datového atributu odpovídající konstantu. Metoda vypocet() se změní takto: /** * metoda spočítá mezivýsledek a uloží ho do proměnné prvniOperand */ private void vypocet () { if (operator == OPERACE_PLUS) { prvniOperand = prvniOperand + hodnotaKZobrazeni; } else { if (operator == OPERACE_MINUS) { prvniOperand = prvniOperand - hodnotaKZobrazeni; } else { prvniOperand = hodnotaKZobrazeni; } } }
15.6. Domácí úkoly 1. Zjistěte, zda se po následující kombinaci kláves zobrazí výsledek 2
tj. že se neprovedla opět operace plus. Pokud ne, tak opravte kód.
Projekt Kalkulačka
strana 155
2. Kalkulačka ve Windows v následující situaci vypočte 70 (první operand použije i jako druhý operand). Upravte tak kód třídy Kalkulator.
3. Upravte kód třídy Kalkulator tak, aby se při opakovaném stisku klávesy rovná se zopakovala poslední operace. Např. v následující kombinaci by se měla zobrazit hodnota 39:
4. Zkuste ošetřit přetečení rozsahu čísla int, tj. aby po dosažení velikosti čísla int nebylo možno vkládat další číslice. 5. Předělejte vnořené příkazy if na příkaz switch v metodě vypocet().
Projekt Kalkulačka
strana 156
Projekt Hádání slov
strana 157
16.Projekt Hádání slov 16.1. Základní popis, zadání úkolu Pracujeme na projektu Hádání slov, který je ke stažení na java.vse.cz. Po otevření v BlueJ vytvoříme instanci třídy HadaniSlov a zavoláme metodu show(). Spustí se grafická aplikace umožňující hádání slov viz obrázek 16.1.
Obrázek 16.1 Vzhled aplikace Hádání slov po spuštění Naším úkolem je: ♦ rozšířit tuto aplikaci tak, aby poskytovala více slov k hádání, ♦ poskytovat slova k hádání v náhodném pořadí. Cílem tohoto projektu je: ♦ naučit se používat seznamy v Javě (konkrétně třídu java.util.ArrayList a některé její metody), ♦ naučit se generovat náhodná čísla v Javě.
16.2. Struktura tříd Projekt Hádání slov je tvořen těmito třídami:
Obrázek 16.2 Struktura tříd projektu Hádání slov, převzato z BlueJ
Projekt Hádání slov
strana 158
16.3. Popis komunikace mezi objekty V konstruktoru třídy HadaniSlov se vytvoří instance třídy PoskytovatelSlov a předá se jako parametr konstruktoru při vytváření instance třídy GrafikaHadani. V konstruktoru třídy PoskytovatelSlov je vytvořena instance třídy HledaneSlovo. Při zobrazení grafiky na obrazovce instance třídy GrafikaHadani zavolá metodu getSlovo() instance PoskytovatelSlov a získá odkaz na instanci třídy HledaneSlovo. Dokud hráč neuhádne slovo, posílá GrafikaHadani zprávy instanci třídy HledaneSlovo. Po uhodnutí slova GrafikaHadani požádá o další slovo instanci PoskytovalSlov. Pokud již nemá další slovo k hádání, vrátí hodnotu null a na obrazovce se zobrazí zpráva, že již není další slovo k hádání. Instance třídy GrafikaHadani se při komunikaci s instancí třídy HledaneSlovo nejdříve „zeptá“ na nápovědu (voláním metody getNapoveda()) a na políčka k zobrazení (voláním metody kZobrazeni()). Po stisknutí tlačítka uživatelem pošle grafika stisknuté písmeno instanci třídy HledaneSlovo (volání metody stisknutoPismeno) a následně požádá o políčka k zobrazení (opět volání metody kZobrazení()). Dále se zeptá, zda je již celé slovo uhodnuto (metoda nalezenoVse()) – pokud ano, tak zobrazí příslušnou zprávu na obrazovce a přejde k dalšímu slovu. Postup komunikace mezi jednotlivými třídami je zachycen v diagramu na obrázku 16.3.
Projekt Hádání slov
strana 159
Obrázek 16.3 Diagram komunikace mezi instancemi při hádání slov
16.4. Popis kódu třídy PoskytovatelSlov Třída PoskytovatelSlov obsahuje datový atribut slovo typu HledaneSlovo (řádek 13 v následujícím výpise). V konstruktoru je slovo inicializováno (řádek 19). Metoda getSlovo() pro každé zavolání vrací odkaz na tuto instanci. Mimo tuto metodu jsou zde i dvě jednoduché metody poskytující jméno autora a číslo verze (řádky 34 – 53). Díky této implementaci je možné hádat pouze jedno slovo. Jakmile hráč uhodne, je mu znovu nabídnuto stejné slovo, navíc již s doplněnými písmeny z minulého hádání.
Projekt Hádání slov 1 /** 2 * Tato třída zpřístupňuje grafickému rozhraní slovo, 3 * které se má hádat. Slovo je 4 * instancí třídy HledaneSlovo a obsahuje vedle vlastního 5 * slova i nápovědu, která se zobrazí. 6 * Třída dále poskytuje informace o autorovi a o verzi. 7 * 8 * @author Luboš Pavlíček 9 * @version 1.0 10 */ 11 public class PoskytovatelSlov { 12 13 private HledaneSlovo slovo; 14 /** 15 * konstruktor pro vytvoření instance třídy pro 16 * poskytování slov 17 */ 18 public PoskytovatelSlov() { 19 slovo = new HledaneSlovo("velbloud","pouštní zvíře"); 20 } 21 22 /** 23 * Metoda vrací slovo k hádání jako instanci třídy 24 * HledaneSlovo (tj. ke slovu je připojena nápověda). 25 * Pokud není již další slovo k dispozici, vrátí 26 * hodnotu null 27 * 28 * @return slovo k hádání 29 */ 30 public HledaneSlovo getSlovo() { 31 return slovo; 32 } 33 34 /** 35 * Metoda vrací jméno autora programu – 36 * jméno se zobrazuje v grafickém rozhraní 37 * 38 * @return jméno autora, např. "L. Pavlicek" 39 */ 40 public String getAutor() { 41 return "???"; 42 } 43 44 /** 45 * Metoda vrací verzi programu - ta se zobrazuje 46 * v grafickém rozhraní 47 * 48 * @return verze programu, např. "1.0" 49 */ 50 public String getVerze() { 51 return "0.1"; 52 } 53 }
strana 160
Projekt Hádání slov
strana 161
16.5. Postup řešení 16.5.1.
Přidání možnosti hádat více slov
Naším prvním úkolem je úprava třídy PoskytovatelSlov, aby nabízela více slov k hádání. Jakmile hráč vyčerpá zásobu slov, vrátí metoda getSlovo() hodnotu null. Nebudeme potřebovat datový atribut slovo, ale jiný, který umožní uchovat větší počet slov k hádání. Pro jejich uložení použijeme instanci třídy ArrayList, kterou nazveme seznamSlov. Třída ArrayList je z balíčku java.util a je tedy nutné psát plné jméno java.util.ArrayList nebo na úvodní řádky kódu doplnit import následujícím způsobem: import java.util.ArrayList;
Do tohoto seznamu budeme ukládat instance třídy HledaneSlovo. Při deklaraci seznamu uvedeme toto: private ArrayList seznamSlov;
Nyní musíme v konstruktoru tento datový atribut inicializovat (řádek 22) a naplnit zásobou slov k hádání (řádky 23 – 28). Třída HledaneSlovo má několik nedostatků, na které je nyní nutno poukázat: neumí samohlásky s interpunkcí a nezná velká písmena. Slova k hádání tedy mohou obsahovat pouze ty znaky, které jsou uvedeny na „klávesnici“ grafického uživatelského rozhraní aplikace, např. Praha s malým p na začátku (řádek 27). 1 import java.util.ArrayList; 2 3 /** 4 * Tato třída zpřístupňuje grafickému rozhraní slova, 5 * která se mají hádat. Slovo je instancí třídy HledaneSlovo, 6 * slova jsou uložena v seznamu (ArrayList). 7 * Třída dále poskytuje informace o autorovy a o verzi. 8 * 9 * @author Luboš Pavlíček 10 * @version 1.1 11 */ 12 public class PoskytovatelSlov { 13 14 private ArrayList seznamSlov; 15 private int index; 16 17 /** 18 * konstruktor pro vytvoření instance třídy pro 19 * poskytování slov 20 */ 21 public PoskytovatelSlov() { 22 seznamSlov = new ArrayList(); 23 seznamSlov.add(new HledaneSlovo("velbloud", 24 "pouštní zvíře")); 25 seznamSlov.add(new HledaneSlovo ("veverka", 26 "zrzavé zvíře")); 27 seznamSlov.add(new HledaneSlovo ("praha", 28 "naše hlavní město")); 29 index = 0; 30 } 31
Projekt Hádání slov 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
strana 162
/** * Metoda vrací slovo k hádání jako instanci třídy HledaneSlovo * (tj. ke slovu je připojena nápověda). * Pokud není již další slovo k dispozici, vrátí hodnotu null * * @return slovo k hádání */ public HledaneSlovo getSlovo() { HledaneSlovo slovo = null; if (index < seznamSlov.size() ) { slovo = seznamSlov.get(index); index++; } return slovo; }
Metoda getSlovo() při každém zavolání postupně vrací jednotlivá slova, pokud už není žádné slovo k dispozici, vrátí hodnotu null. Je nutné přidat další datový atribut, abychom si zapamatovali, které slovo je na řadě (musí to být datový atribut, aby se v něm uchovávala pozice mezi jednotlivými voláními metody getSlovo()). Využijeme toho, že ArrayList používá indexy – budeme si uchovávat, které slovo je další na řadě k hádání. Datový atribut pojmenujeme index, bude typu int (řádek 15) a v konstruktoru mu přiřadíme hodnotu 0 (řádek 29). Hodnota nula odkazuje na první prvek v seznamSlov, protože Java používá indexy v rozsahu 0 až n-1, kde n je počet prvků. V metodě getSlovo() si na začátku deklarujeme pomocnou proměnnou slovo typu HledaneSlovo a inicializujeme ji hodnotou null (řádek 40). Potom zkontrolujeme, zda hodnota atributu index je menší než počet prvků v seznamu slov, který zjistíme metodou size() – řádek 41. Pokud je index menší, do pomocné proměnné uložíme metodou get(index) odkaz na instanci třídy HledaneSlovo, která je na pozici udané indexem (řádek 42). Následně zvýšíme hodnotu indexu o jedna, abychom při příštím volání metody getSlovo() poskytli následující slovo (řádek 43). Pokud je index větší nebo roven počtu prvků seznamu, znamená to, že již nemáme další slovo k hádání – v proměnné slovo tedy ponecháme hodnotu null, kterou jsme přiřadili při inicializaci. Větev else příkazu if proto nepotřebujeme. Na konci metody příkazem return předáme obsah pomocné proměnné slovo (řádek 45 výpisu).
16.5.2.
Poskytování slov k hádání v náhodném pořadí
Nyní budeme řešit druhou část zadání, tj. poskytování slov v náhodném pořadí. Budeme postupovat následovně: ♦ Potřebujeme náhodné číslo z intervalu 0 až n-1 (n je počet slov k hádání v kopii seznamu), neboť indexy jsou od 0 do n-1. Pro generování náhodných čísel poskytuje Java třídu Random z balíčku java.util. Stejně jako u třídy ArrayList můžeme psát buď celý název (java.util.Random) nebo na začátku zdrojového textu importovat tuto třídu (import java.util.Random;) a poté psát jenom název Random. V konstruktoru vytvoříme instanci třídy Random (řádek 16 deklarace a řádek 30 inicializace). Třída Random má více metod pro generování náhodných čísel, my použijeme metodu nextInt(int n), která vrací celé kladné číslo z intervalu <0, n), tj. od nuly (včetně) po hodnotu parametru (není v intervalu zahrnuta)36. Jako parametr použijeme aktuální velikost seznamu slov (řádek 43). 36
Čtenáře může napadnout, co vrací metoda nextInt(int n), pokud bude parametrem záporná hodnota. Máte tří možnosti, jak to zjistit – podívat se do dokumentace třídy Random, napsat si vlastní zkušební program a podívat se do zdrojového textu třídy Random.
Projekt Hádání slov
strana 163
♦ Slovo k hádání, které je uloženo v seznamu na místě s indexem odpovídajícím náhodnému číslu, uložíme do pomocné proměnné slovo (deklarace a inicializace na řádku 41). ♦ V seznamu toto slovo zrušíme, metoda vrátí vybrané slovo z pomocné proměnné. ♦ Pokud již v seznamu slov k hádání není žádné slovo, vrátí metoda hodnotu null, která se ukládá do pomocné proměnné slovo na začátku metody (zda je či není slovo k hádání k dispozici, se zjišťuje na řádku 42). V uvedeném řešení se slovo určené k hádání maže ze seznamu slov, tj. po předání již není k dispozici v instanci třídy PoskytovatelSlov. Pokud bychom chtěli slova dále uchovávat, vytvořili bychom na začátku kopii seznamu a slova bychom vydávali z kopie. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
import java.util.Random; import java.util.List; import java.util.ArrayList; /** * Tato třída zpřístupňuje v náhodném pořadí grafickému rozhraní * slova, která se mají hádat. Slovo je instancí třídy * HledaneSlovo, slova jsou uložena v seznamu (ArrayList). * Třída dále poskytuje informace o autorovi a o verzi. * * @author Luboš Pavlíček * @version 1.1 */ public class PoskytovatelSlov { private List seznamSlov; private Random nahoda; /** * konstruktor pro vytvoření instance třídy pro * poskytování slov */ public PoskytovatelSlov() { seznamSlov = new ArrayList(); seznamSlov.add(new HledaneSlovo ("velbloud", "pouštní zvíře")); seznamSlov.add(new HledaneSlovo ("panenka", "hračka pro holčičky")); seznamSlov.add(new HledaneSlovo ("praha", "naše hlavní město")); nahoda = new Random(); } /** * Metoda vrací slovo k hádání jako instanci třídy HledaneSlovo * (tj. ke slovu je připojena nápověda). * Pokud není již další slovo k dispozici, vrátí hodnotu null * * @return slovo k hádání */
Projekt Hádání slov 41 42 43 44 45 46 47 48 49
strana 164
public HledaneSlovo getSlovo() { HledaneSlovo slovo = null; if (seznamSlov.size() > 0 ){ int index = nahoda.nextInt(seznamSlov.size()); slovo = seznamSlov.get(index); seznamuSlov.remove(index); } return slovo; }
16.6. Domácí úkol 1. Upravte aplikaci tak, aby umožňovala používat malá a velká písmena (tj. aby slovo Praha bylo s velkým písmenem P) a aby se nerozlišovaly samohlásky s čárkami, háčky (např. pokud by se mělo hádat slovo „nápad“, tak by se po volbě tlačítka „a“ odkryly znaky „a“ a „á“). Neměli by jste upravovat grafické rozhraní aplikace.
Projekt Trojúhelníky
strana 165
17.Projekt Trojúhelníky 17.1. Základní popis, zadání úkolu Pracujeme na projektu Trojúhelníky, který je ke stažení na java.vse.cz. Aplikace je napsána s textovým uživatelským rozhraním – v BlueJ se vypisuje a načítá vstup z klávesnice v samostatném okně pojmenovaném Terminál. Pro spuštění vytvořte instanci třídy Trojuhelniky a zavolejte metodu zakladniCyklus(). Výsledek spuštění v BlueJ vidíte na obrázku 17.1.
Obrázek 17.1 Začátek komunikace aplikace Trojúhelníky po spuštění V tomto projektu budeme řešit tyto úkoly: ♦ napsat metodu main() pro spuštění aplikace, ♦ přidat další variantu trojúhelníka, pro kterou bude aplikace umět spočítat parametry trojúhelníka (rovnostranný trojúhelník), ♦ doplnit detekci chybových stavů a generování výjimek do třídy Trojuhelnik, ♦ ošetřit vzniklé výjimky ve třídě Trojuhelniky. Tento projekt má následující cíle: ♦ ukázat základní použití výčtového typu v Javě, ♦ ukázat používání a vytváření statických metod, ♦ ukázat používání výjimek v Javě.
17.2. Struktura tříd Projekt Trojúhelníky se skládá z následujících tříd (obrázek z BlueJ):
Obrázek 17.2 Diagram tříd aplikace Trojúhelníky z BlueJ
Projekt Trojúhelníky
strana 166
17.3. Popis komunikace mezi objekty Při vytváření instance třídy Trojuhelniky se jednotlivé varianty (instance třídy Varianta) vloží do seznamu. Identifikace varianty se provádí pomocí konstanty z výčtového typu TypTrojuhelnika. Dále je v konstruktoru vytvořena instance třídy CteniZKonzole. Po spuštění metody zakladniCyklus() je vypsáno menu a pomocí instance třídy CteniZKonzole načtena volba uživatele. Cyklus probíhá dokud není zadána volba −1 pro konec. Po načtení volby od uživatele se v metodě zobrazVysledky() zkontroluje platnost volby. Na základě údajů o variantě jsou pomocí třídy CteniZKonzole načteny další údaje o trojúhelníku (délky stran, úhly atd.). Pokud uživatel nezadá číslo, ale např. písmeno A, zopakuje se čtení z konzole. Dostaneme tedy pouze číselné parametry. Pokračuje se voláním statické metody getTrojuhelnik() třídy Trojuhelnik, která na základě varianty a parametrů vrátí instanci třídy Trojuhelnik. Pokud zadané údaje nepředstavují trojúhelník (např. pokud ve variantě pravoúhlý AC je strana A delší než strana C), je vrácena hodnota null. Následně se vypíší informace o trojúhelníku nebo chybové hlášení, pokud trojúhelník nelze vytvořit. Výpis výsledků, když uživatel zadá volbu 0 a délky stran 5 a 8, vidíte na obrázku 17.3.
Obrázek 17.3 Ukázka komunikace s aplikací Trojúhelníky Následující diagram zobrazuje průběh komunikace při dotazu na jeden trojúhelník. Začíná se zobrazením nabídky voleb, končí zobrazením hodnot o trojúhelníku na obrazovce. V diagramu je též zachycen vstup údajů od uživatele.
Projekt Trojúhelníky
Obrázek 17.4 Diagram zobrazuje průběh komunikace při dotazu na jeden trojúhelník
strana 167
Projekt Trojúhelníky
strana 168
17.4. Popis výčtového typu (enum) TypTrojuhelnika 1 /** 2 * Výčtový typ TypTrojuhelnika představuje jednotlivé typy 3 * trojúhelníků, se kterými umí pracovat třídy aplikace 4 * Trojuhelniky. 5 * @author Jarmila Pavlíčková 6 * @version 1.0, duben 2005 7 */ 8 public enum TypTrojuhelnika { 9 PRAVOUHLY_A_B, PRAVOUHLY_A_C, ROVNOSTRANNY; 10 }
Výčtový typ TypTrojuhelnika obsahuje konstanty představující jednotlivé typy trojúhelníků, pro které aplikace umí vypočítat jejich strany, úhly, obvod a obsah. V deklaraci výčtového typu je již další předpřipravená konstanta ROVNOSTRANNY pro typ trojúhelníka, který máme do aplikace přidat. Výčtový typ se používá na dvou místech: ♦ jako datový atribut třídy Varianta, která popisuje podrobnosti o příslušné variantě trojúhelníka (kolik je potřeba zadat hodnot, text dotazů na jednotlivé hodnoty), ♦ jako parametr při vytváření vlastního trojúhelníka – na základě typu se vybere ve statické metodě getTrojuhelnik() třídy Trojuhelnik příslušná metoda pro vytvoření instance třídy Trojuhelnik.
17.5. Popis kódu třídy Trojuhelnik 1 /** 2 * Třída popisuje trojúhelník. 3 * Pro získání instance existuje větší množství statických 4 * metod, které vytvářejí trojúhelník na základě jednotlivých 5 * známých parametrů. 6 * 7 * @author Luboš Pavlíček 8 * @version 1.0 srpen 2004 9 */ 10 public class Trojuhelnik{ 11 // datové atributy trojúhelníka 12 private double stranaA; 13 private double stranaB; 14 private double stranaC; 15 16 /** 17 * Vytvoření trojúhelníka při znalosti všech tří stran. 18 */ 19 20 private Trojuhelnik(double stranaA, double stranaB, 21 double stranaC) { 22 this.stranaA = stranaA; 23 this.stranaB = stranaB; 24 this.stranaC = stranaC; 25 } 26
Projekt Trojúhelníky 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
strana 169
/** * Statická metoda pro získání instance pravoúhlého * trojúhelníka (tj. úhel gama je 90°) * při zadání velikosti strany A a strany B (tj. obou odvěsen). * * @param stranaA délka strany A * @param stranaB délka strany B * @return instance třídy Trojuhelnik, která má úhel gama 90° * a zadanou délku strany A a strany B */ public static Trojuhelnik getPravouhlyAB (double stranaA, double stranaB) { return new Trojuhelnik(stranaA, stranaB, Math.sqrt(stranaA*stranaA + stranaB*stranaB)); } /** * Statická metoda pro získání instance pravoúhlého * trojúhelníka (tj. úhel gama je 90°) při zadání * velikosti strany A a strany C (tj. jedné odvěsny a přepony). * * @param stranaA délka strany A (odvěsna) * @param stranaC délka strany C (přepona) * @return instance třídy Trojuhelnik, která má úhel gama 90° * a zadanou délku strany A a strany C */ public static Trojuhelnik getPravouhlyAC (double stranaA, double stranaC) { if (stranaC <= stranaA) { return null; } else { return new Trojuhelnik(stranaA, Math.sqrt(stranaC *stranaC - stranaA*stranaA), stranaC); } } /** * Obecná statická metoda pro získání instance trojúhelníka, * kdy je zadán typ trojúhelníka (viz enum TypTrojuhelnika) * a pole s potřebnými parametry. * * @param typ typ trojúhelníka * @param parametry pole s parametry pro příslušný typ * trojúhelníka * @return instance třídy Trojuhelnik příslušného typu * a odpovídající zadaným parametrům */
Projekt Trojúhelníky 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
public static Trojuhelnik getTrojuhelnik (TypTrojuhelnika typ, double [] parametry) { if (typ == TypTrojuhelnika.PRAVOUHLY_A_B) { if (parametry.length < 2) { return null; } return getPravouhlyAB(parametry[0], parametry[1]); } if (typ == TypTrojuhelnika.PRAVOUHLY_A_C) { if (parametry.length < 2) { return null; } return getPravouhlyAC(parametry[0], parametry[1]); } return null; } /** * Vrací délku strany A trojúhelníka * * @return délka strany A trojúhelníka */ public double getStranaA() { return stranaA; } /** * Vrací délku strany B trojúhelníka * * @return délka strany B trojúhelníka */ public double getStranaB() { return stranaB; } /** * Vrací délku strany C trojúhelníka * * @return délka strany C trojúhelníka */ public double getStranaC() { return stranaC; } /** * Vrací velikost úhlu alfa (proti straně A) trojúhelníka * ve stupních * * @return velikost úhlu alfa trojúhelníka. */ public double getAlfa() { return Math.toDegrees(Math.acos( (stranaB*stranaB + stranaC*stranaC - stranaA*stranaA)/(2*stranaB*stranaC))); } /** * Vrací velikost úhlu beta (proti straně B) trojúhelníka * ve stupních
strana 170
Projekt Trojúhelníky
strana 171
133 * 134 * @return velikost úhlu beta trojúhelníka. 135 */ 136 public double getBeta() { 137 return Math.toDegrees(Math.acos( (stranaA*stranaA + 138 stranaC*stranaC - stranaB*stranaB)/(2*stranaA*stranaC))); 139 } 140 141 /** 142 * Vrací velikost úhlu gama (proti straně C) trojúhelníka 143 * ve stupních 144 * 145 * @return velikost úhlu gama trojúhelníka. 146 */ 147 public double getGama() { 148 return Math.toDegrees(Math.acos( (stranaB*stranaB + 149 stranaA*stranaA - stranaC*stranaC)/(2*stranaB*stranaA))); 150 } 151 152 /** 153 * Vypočte obvod trojúhelníka. 154 * 155 * @return obvod trojúhelníka 156 */ 157 public double obvod () { 158 return (stranaA + stranaB + stranaC); 159 } 160 161 /** 162 * Vypočte obsah trojúhelníka. 163 * 164 * @return obsah trojúhelníka 165 */ 166 public double obsah () { 167 double polObvod=obvod()/2; 168 return Math.sqrt(polObvod*(polObvod-stranaA) *(polObvod169 stranaB) * (polObvod - stranaC)); 170 } 171 }
Třída Trojuhelnik představuje trojúhelník a poskytuje metody pro výpočet jeho parametrů (tj. úhly, obvod a obsah). V konstruktoru třídy předpokládáme, že trojúhelník je vždy zadán třemi stranami. Tento konstruktor je deklarován jako private a tudíž ho nelze spustit mimo třídu. Jednotlivé instance trojúhelníků, zadávané podle volby uživatele, nejsou tedy vytvářeny přímo voláním konstruktoru, ale pomocí statické metody getTrojuhelnik(), ve které se udává typ trojúhelníka (konstanta z TypTrojuhelnika). Toto řešení (privátní konstruktor) je zvoleno z toho důvodu, že přes konstruktory není možné rozlišit jednotlivé typy trojúhelníků, neboť některé varianty se neliší pořadím či počtem parametrů (např. trojúhelník se třemi libovolnými stranami má tři vstupní parametry, trojúhelník se dvěmi stranami a jedním úhlem má také tři vstupní parametry). Metoda getTrojuhlenik podle konstanty výčtového typu TypTrojuhelnika zavolá odpovídající statickou metodu. Metoda zkontroluje validitu dat (např. v pravoúhlém trojúhelníku nesmí být strana A delší než strana C) a případně dopočítá chybějící strany trojúhelníka, aby mohla spustit konstruktor. Jsou vytvořeny dvě takové metody getPravouhlyAB() a getPravouhlyAC(), které pomocí Pythagorovy věty dopočítávají třetí stranu trojúhelníka. Ostatní metody ve třídě Trojuhelnik jsou již metodami instance a pro vytvořený trojúhelník spočítají úhly, obvod a obsah.
Projekt Trojúhelníky
17.6. Popis kódu třídy Trojuhelniky 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
import java.util.ArrayList; import java.util.List; /** * Základní třída aplikace Trouhelniky zobrazuje nabídku. * Na základě vybrané varianty se zeptá na příslušné parametry * a poté zobrazí informace o trojúhelníku. * * @author Luboš Pavlíček * @version 1.0 srpen 2004 */ public class Trojuhelniky { private List varianty; private CteniZKonsole vstup; /** * Vytváří instanci třídy Trojuhelniky */ public Trojuhelniky() { varianty = new ArrayList(); varianty.add(new Varianta(TypTrojuhelnika.PRAVOUHLY_A_B, "Pravoúhlý trojúhelník, známy strany A a B", "délka strany A", "délka strany B", null)); varianty.add(new Varianta(TypTrojuhelnika.PRAVOUHLY_A_C, "Pravoúhlý trojúhelník, známy strany A a C", "délka strany A", "délka strany C", null)); vstup = new CteniZKonsole(); } /** * Metoda zobrazí nabídku z pole variant a přidá volbu Konec. */ private void zobrazNabidku() { for (int i = 0; i< varianty.size(); i++) { Varianta var = varianty.get(i); System.out.printf("%2d. %s%n",i,var.getPopis()); } System.out.println("-1. Konec"); } /** * Metoda pro přístušnou volbu načte potřebné parametry, * vytvoří Trojuhelnik a zobrazí informace o trojúhelniku. * * @param volba - zvolená varianta z pole varianty */ private void zobrazVysledky(int volba) { if ((volba < 0) | (volba >= varianty.size())) { System.out.println("tuto volbu neznam"); return; } Varianta var = varianty.get(volba); String dotaz1=var.getDotaz1(); String dotaz2=var.getDotaz2(); String dotaz3=var.getDotaz3(); double [] parametry = new double[var.getPocetParametru()];
strana 172
Projekt Trojúhelníky 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
if (dotaz1 != null) { parametry[0] = vstup.getDouble(dotaz1); } if (dotaz2 != null) { parametry[1] = vstup.getDouble(dotaz2); } if (dotaz3 != null) { parametry[2] = vstup.getDouble(dotaz3); } Trojuhelnik troj = Trojuhelnik.getTrojuhelnik(var.getTyp(), parametry); if (troj == null) { System.out.println("!!! metoda getTrojuhelnik nevratila pro zadane parametry trojuhelnik !!!!"); System.out.println("\t typ: " + var.getTyp()); for (int i=0; i < parametry.length; i++) { System.out.printf("\tparametry[%d] : %f%n" i,parametry[i]); } } else { System.out.println("=== parametry trojuhelniku ==="); System.out.printf(" strany: a=%14f b=%14f c=%14f%n", troj.getStranaA(),troj.getStranaB(), troj.getStranaC()); System.out.printf(" uhly: alfa=%11f beta=%11f gama=%11f%n",troj.getAlfa(), troj.getBeta(),troj.getGama()); System.out.printf(" obvod: %f%n",troj.obvod()); System.out.printf(" obsah: %f%n%n",troj.obsah()); } } /** * Metoda přečte volbu uživatele. * Neprovádí se kontrola přípustnosti vstupu. * * @return volba uživatele */ private int nactiVolbu() { return vstup.getInt("zadej volbu"); } /** * Metoda zajišťuje základní cyklus aplikace. Tj. * * - zobrazení menu *
- načtení volby od uživatele *
- načtení potřebných parametrů *
- zobrazení informací o trojúhelníku *
* Toto pořadí se opakuje do té doby, než uživatel zvolí * volbu Konec. */
strana 173
Projekt Trojúhelníky
strana 174
110 public void zakladniCyklus() { 111 int volba = 0; 112 zobrazNabidku(); 113 volba = nactiVolbu(); 114 while (volba != -1) { 115 zobrazVysledky(volba); 116 zobrazNabidku(); 117 volba = nactiVolbu(); 118 } 119 System.out.println("Konec programu"); 120 } 121 }
Všimněte si v metodě zobrazNabidku() způsobu procházení seznamu pomocí cyklu for s řídící proměnnou cyklu – tato varianta je zvolena, aby bylo možné vypsat i pořadové číslo nabídky. U některých výstupů je použito formátování řetězců v metodě printf – viz popis třídy String v kapitole 5.4.
17.7. Postup řešení 17.7.1.
Vytvoření metody main, pro spouštění aplikace z příkazové řádky
Jak bylo uvedeno v úvodu, při spuštění projektu vytvoříme instanci třídy Trojuhelniky a zavoláme metodu zakladniCyklus(). Do metody main ve třídě Trojuhelniky tento postup zapíšeme v kódu: 1 2 3 4 5 6 7 8 9
/** * Metoda main pro spuštění aplikace * * @param args pole argumentů vstupní řádky – nezpracovávají se */ public static void main (String [] args) { Trojuhelniky troj = new Trojuhelniky(); troj.zakladniCyklus(); }
17.7.2.
Přidání nového typu trojúhelníka (rovnostranného)
Typ ROVNOSTRANNY je již ve výčtovém typu TypTrojuhelnika uveden. Musíme provést změny pouze ve třídách Trojuhelnik a Trojuhelniky. Ve třídě Trojuhelniky doplníme do seznamu variant (datový atribut varianty typu List37 na řádku 13 výpisu třídy Trojuhelniky) novou variantu s těmito parametry: ♦ prvním parametrem je typ trojúhelníka – použijeme konstantu ROVNOSTRANNY z výčtového typu TypTrojuhelnika, ♦ druhým parametrem je text zobrazovaný v nabídce – vypíše se „Rovnostranný trojúhelník“, ♦ třetím až pátým parametrem jsou texty dotazů na hodnoty vkládané uživatelem – první dotaz bude na délku strany („délka strany“), více dotazů není potřeba, proto za čtvrtý a pátý parametr použijeme konstantu null. Následující kód přidáme do třídy Trojuhelniky za řádek číslo 25. varianty.add(new Varianta(TypTrojuhelnika.ROVNOSTRANNY, "Rovnostranný trojúhelník", "délka strany",null,null)); 37
Datový atribut varianty je typu List, konkrétní instance je typu ArrayList, viz volání konstruktoru na řádku 19 zdrojového kódu třídy Trojuhelnik.
Projekt Trojúhelníky
strana 175
Další úpravy se budou týkat kódu třídy Trojuhelnik. Musíme napsat statickou metodu getRovnostranny(), která na základě délky strany (parametru) vytvoří instanci trojúhelníka. Kód této metody je uveden v následujícím výpisu: 1 /** 2 * Statická metoda pro získání instance rovnostranného 3 *trojúhelníka při zadání velikosti strany. 4 * 5 * @param strana délka strany 6 * @return instance třídy Trojuhelnik, která má všechny úhly 60° 7 * a zadanou délku strany 8 */ 9 public static Trojuhelnik getRovnostranny (double strana) { 10 return new Trojuhelnik(strana, strana, strana); 11 }
Dále upravíme kód metody getTrojuhelnik(), která dle typu trojúhelníka rozhodne, kterou konkrétní metodu pro vytvoření trojúhelníka zavolá. Do kódu této metody připíšeme rozhodování pro typ ROVNOSTRANNY – spustí se metoda getRovnostranny(). Následující kód se umístí do metody getTrojuhelnik() za řádek 88 ve výpisu kódu třídy. if (typ == TypTrojuhelnika.ROVNOSTRANNY){ if (parametry.length < 1){ return null; } return getRovnostranny(parametry[0]); }
17.7.3.
Zjišťování chyb ve třídě Trojuhelnik, generování výjimek
Aplikace již funguje téměř dobře, chybí ale detekce některých chybových stavů. Na následujícím obrázku je vidět situace, že lze zadat trojúhelník se zápornou délkou strany.
Obrázek 17.5 Výpis aplikace při zadání záporné délky strany Některé chybové stavy jsou již detekovány, obecnost chybového hlášení příliš nepomůže s určením důvodu problému. Chyba se projeví např. při zadání stejně dlouhé strany a a c u pravoúhlého trojúhelníka.
Obrázek 17.6 Chybové hlášení při zadání stejných délek stran A a C
Projekt Trojúhelníky
strana 176
Pro detekci chybových stavů se většinou používá příkaz if s příslušnými podmínkami. Je potřeba však ještě vyřešit způsob, jak předat informaci o chybě včetně vhodného popisu z místa detekce na místo, kde se může chyba ošetřit (např. vypsat hlášení uživateli). Někdy máme štěstí, detekce a ošetření chyby je na jednom místě. Častější je, že se jedná o různá místa v kódu aplikace, často i o různé třídy. Ve většině současných jazyků lze použít dva mechanismy: ♦ Přes návratovou hodnotu metody – tento způsob je již nyní použit např. při kontrole, zda strana c je delší než strana a, podívejte se na řádky 55 až 57 ve třídě Trojuhelnik. V případě chybového stavu se vrací hodnota null. Nevýhodou tohoto mechanismu jsou komplikace při návrhu a používání metod a omezené možnosti informování o chybových stavech. Tento mechanismus též nelze použít v konstruktorech (konstruktory nemají návratovou hodnotu). ♦ Pomocí výjimek, kdy někam umístíme kód ošetřující chybu (blok try catch) a jinam umístíme kód oznamující, že vznikla chyba (vyvolání výjimky pomocí throw). Nevýhodou výjimek je jejich pomalost, výhodou jednodušší návrh metod, přehlednější kód. My budeme používat mechanismus výjimek – při zjištění chybného vstupního parametru (např. záporná délka strany) vygenerujeme výjimku IllegalArgumentException. Tato standardní výjimka svým názvem dobře vystihuje charakter chyby, je zbytečné vytvářet vlastní výjimky. Ošetření chybových stavů – výpis vhodného chybového hlášení umístíme do třídy Trojuhelniky, neboť je vhodné operace pro komunikaci s uživatelem soustředit do jedné třídy (je to přehlednější, jednoznačně jsou rozděleny funkce mezi třídy, usnadní to v budoucnu výměnu textového rozhraní za grafické). V konstruktoru třídy Trojuhelnik budeme detekovat dva chybové stavy – délka některé strany je menší nebo rovna nule a součet libovolných stran je menší nebo roven straně třetí: /** * Vytvoření trojúhelníka při znalosti všech tří stran. * @exception IllegalArgumentException - pokud je některá * strana <= 0 nebo pokud je součet libovolných dvou stran menší .* roven straně třetí */ private Trojuhelnik(double stranaA, double stranaB, double stranaC) throws IllegalArgumentException { if ((stranaA <=0) | ( stranaB <= 0) | (stranaC <= 0)) { throw new IllegalArgumentException( "strana trojúhelníka musí být větší než 0"); } if ((stranaA + stranaB <= stranaC) | (stranaA + stranaC <= stranaB) | (stranaB + stranaC <= stranaC)) { throw new IllegalArgumentException("součet dvou stran "+ "trojúhelníka musí být větší než strana třetí"); } this.stranaA = stranaA; this.stranaB = stranaB; this.stranaC = stranaC; }
či
Všimněte si, že popis výjimky je uveden i v dokumentačních komentářích. V hlavičce metody nemusí být v našem případě uvedeno throws IllegalArgumentException, neboť tato výjimka nepatří mezi povinně odchytávané, je však vhodnější ji uvádět, neboť čtenář kódu si možnosti vzniku výjimky všimne hned na začátku metody. Metodu getPravouhlyAB() nebudeme doplňovat o detekci chybových stavů – všechny v úvahu přicházející chybové stavy budou detekovány v konstruktoru třídy Trojuhelnik. Je však opět vhodné doplnit komentáře k metodě o možnost vzniku výjimky, podobně i hlavičku metody o throws. V metodě getPravouhlyAC() nahradíme vracení hodnoty null generováním výjimky:
Projekt Trojúhelníky
strana 177
public static Trojuhelnik getPravouhlyAC (double stranaA, double stranaC) throws IllegalArgumentException { if (stranaC <= stranaA) { throw new IllegalArgumentException("u pravoúhlého " + "trojúhelníka musí být strana C větší než strana A"); } else { return new Trojuhelnik(stranaA, Math.sqrt(stranaC*stranaC - stranaA*stranaA), stranaC); } }
Obdobně v metodě getTrojuhelnik() nahradíme vracení hodnoty null generováním výjimky: throw new IllegalArgumentException( "nedostatečný počet parametrů v poli");
17.7.4.
Ošetřování výjimek ve třídě Trojuhelniky
Používání výjimek má jednu výhodu – pokud je programátor neošetří, použije se standardní mechanismus ošetření – vypíší se informace o chybě a aplikace se ukončí. Pokud nyní zadáme zápornou délku strany, tak se zobrazí na konzole následující výpis:
Obrázek 17.7 Výpis aplikace při zadání záporné délky strany Z tohoto popisu chyby bude programátor většinou nadšen – ve výpisu vidí typ a popis chyby i kde v kódu ji má hledat – výjimka IllegalArgumentException vznikla na řádku 25 v konstruktoru třídy Trojuhelnik a konstruktor byl volán z metody getPravouhlyAB() na řádku 46. Tato metoda byla volána z metody getTrojuhelnik(), ta byla volána z metody zobrazVysledky() ve třídě Trojuhelniky, atd. Spokojenost uživatele však bude minimální. Výjimky budeme odchytávat a ošetřovat ve druhé části metody zobrazVysledky(). Umístíme sem konstrukci try catch, v bloku try bude volání metody getTrojuhelnik() a výpis parametrů trojúhelníka při správně zadaných parametrech. Blok catch bude pouze jeden, v něm budeme odchytávat výjimku IllegalArgumentException. Zprávu předávanou přes výjimku získáme pomocí metody getMessage(). private void zobrazVysledky(int volba) { if ((volba < 0) | (volba >= varianty.size())) { System.out.println("tuto volbu neznam"); return; } Varianta var = varianty.get(volba); String dotaz1=var.getDotaz1(); String dotaz2=var.getDotaz2(); String dotaz3=var.getDotaz3(); double [] parametry = new double[var.getPocetParametru()];
Projekt Trojúhelníky
}
strana 178
if (dotaz1 != null) { parametry[0] = vstup.getDouble(dotaz1); } if (dotaz2 != null) { parametry[1] = vstup.getDouble(dotaz2); } if (dotaz3 != null) { parametry[2] = vstup.getDouble(dotaz3); } try { Trojuhelnik troj = Trojuhelnik.getTrojuhelnik(var.getTyp(), parametry); System.out.println("=== parametry trojuhelniku ==="); System.out.printf(" strany: a=%14f b=%14f c=%14f%n", troj.getStranaA(),troj.getStranaB(),troj.getStranaC()); System.out.printf(" uhly: alfa=%11f beta=%11f gama=%11f%n", troj.getAlfa(),troj.getBeta(),troj.getGama()); System.out.printf(" obvod: %f%n",troj.obvod()); System.out.printf(" obsah: %f%n%n",troj.obvod()); } catch (IllegalArgumentException exc) { System.out.println("!!! chybné vstupní parametry: "+ exc.getMessage()); System.out.println("\t typ: " + var.getTyp()); for (int i=0; i < parametry.length; i++) { System.out.printf("\t parametry[%d] : %f%n", i, parametry[i]); } }
17.8. Domácí úkoly 1. Doplňte aplikaci o další typy trojúhelníků: - obecný trojúhelník, zadány tři strany, - obecný trojúhelník, zadány dvě strany a úhel, - obecný trojúhelník, zadány dva úhly a strana. U všech typů doplňte i kontrolu, zda zadané údaje opravdu představují trojúhelník (libovolný úhel i součet dvou úhlů musí být menší než 180 stupňů, součet dvou stran trojúhelníka musí být vždy větší než třetí strana atd.). 2. Upravte aplikaci, aby se varianty při výpisu číslovaly od jedničky a ne od nuly. 3. Upravte předchozí projekt Kalkulačka tak, aby používal pro uložení operace výčtový typ (enum). 4. Nastudujte v dokumentaci Javy možnosti rozšiřování výčtového typu enum a zkuste sloučit třídu Varianta a výčtový typ TypTrojuhelnika do jednoho výčtového typu.
Projekt Škola
strana 179
18.Projekt Škola 18.1. Základní popis, zadání úkolu Pracujeme na projektu Škola, který je ke stažení na java.vse.cz. Je to pouze fragment aplikace – cílem je ukázat, jak v instancích zachytit stromovou strukturu. Po dokončení inicializace aplikace se vytvoří instance třídy Skola. Po zavolání metody vypis() by se na obrazovku měla vypsat organizační struktura školy (variantně s pracovníky či bez pracovníků). V tomto projektu budeme řešit tyto úkoly: ♦ navrhnout datové atributy ve třídě Utvar pro uložení podřízených útvarů a osob pracujících v útvaru, ♦ doplnit obsah metod pridej() ve třídě Utvar, ♦ vypsat seznamu podřízených útvarů, tj. doplnit obsah metod vypis() ve třídě Utvar. Tento projekt má následující cíle: ♦ ukázat vytvoření složitější datové struktury – v tomto projektu se vytvoří stromová struktura instancí (organizační struktura školy). Projekt též ukazuje rozdíly mezi strukturou tříd a strukturou instancí. ♦ ukázat přetěžování metod na metodě pridej().
18.2. Struktura tříd Projekt Škola se skládá s následujících tříd (obrázek z BlueJ):
Obrázek 18.1 Diagram tříd projektu Škola převzatý z BlueJ
Projekt Škola
strana 180
Obrázek 18.2 Diagram tříd projektu Škola včetně datových atributů a metod
18.3. Popis kódu třídy Skola Třída Skola je hlavní třídou, ve které se vytvářejí zkušební data. Třída dále obsahuje metodu vypis(), přes kterou se vypisuje organizační struktura. V konstruktoru třídy Skola se vytvářejí jednotlivé testovací instance třídy Utvar a Osoba a vzájemně se spojují mezi sebou. 1 /** 2 * Tato třída je součástí projektu Skola a obsahuje základní 3 * třídu pro školu. V konstruktoru je i inicializace 4 * organizační struktury školy. 5 * 6 * @author Luboš Pavlíček 7 * @version 1.0, srpen 2004 8 */ 9 public class Skola { 10 11 private Utvar skola; 12 /** 13 * Konstruktor pro instance třídy Skola. Vytvářejí se 14 * instance tříd Utvar a Osoba, které se propojují do sebe. 15 */ 16 public Skola() { 17 skola = new Utvar("VŠE","Vysoká škola ekonomická"); 18 Utvar fak1 = new Utvar("F1","Fakulta financí a účetnictví"); 19 Utvar fak2 = new Utvar("F2","Fakulta mezinárodních vztahů"); 20 Utvar fak3 = new Utvar("F3","Fakulta podnikohospodářská");
Projekt Škola 21 22 23 24 25 26 27 28 29 30 31 32
strana 181
Utvar fak4 = new Utvar("F4", "Fakulta informatiky a statistiky"); Utvar fak5 = new Utvar("F5","Fakulta národohospodářská"); Utvar fak6 = new Utvar("F6","Fakulta managementu"); skola.pridej(fak1); skola.pridej(fak2); skola.pridej(fak3); skola.pridej(fak4); skola.pridej(fak5); skola.pridej(fak6); fak4.pridej(new Utvar("KMAT","katedra matematiky")); Utvar kstp = new Utvar("KSTP","katedra statistiky a pravděpodobnosti"); fak4.pridej(new Utvar("KEST", "katedra ekonomické statistiky")); fak4.pridej(new Utvar("KDEM","katedra demografie")); fak4.pridej(new Utvar("KEKO","katedra ekonometrie")); fak4.pridej(new Utvar("KSA","katedra systémové analýzy")); fak4.pridej(new Utvar("KIZI", "katedra informačního a znalostního inženýrství")); fak4.pridej(new Utvar("KFIL","katedra filosofie")); Utvar kit = new Utvar("KIT", "katedra informačních technologii"); fak4.pridej(kit); fak4.pridej(kstp); skola.pridej(new Osoba("Durčáková Jaroslava", "Doc. Ing.", "CSc.")); Osoba dekan = new Osoba ("Hindls Richard", "Prof. Ing.", "CSc."); fak4.pridej(dekan); kstp.pridej(dekan); kit.pridej(new Osoba("Voříšek Jiří","Prof. Ing.", "CSc.")); kit.pridej(new Osoba("Buchalcevová Alena", "Ing.","PhD.")); kit.pridej(new Osoba("Pavlíčková Jarmila", "Ing.","")); kit.pridej(new Osoba("Pavlíček Luboš","Ing.","")); kit.pridej(new Osoba("Tichý Vladimír", "RNDr.",""));
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 } 57 /** 58 * Metoda vypíše organizační strukturu 59 * 60 * @param vcetneOsob zda vypsat i osoby přiřazené do 61 * jednotlivých útvarů 62 */ 63 public void vypis(boolean vcetneOsob) { 64 skola.vypis(vcetneOsob); 65 } 66 }
18.4. Popis kódu tříd Utvar a Osoba Nebudeme zde uvádět kód tříd Utvar a Osoba, neboť jsou jednoduché. Třída Osoba popisující jednu osobu má tři datové atributy – jmeno, titulPred a titulZa, které se inicializují přes konstruktor. Třída Osoba dále obsahuje metody pro získání hodnot těchto atributů a metody toString(), equals() a hashCode(). Třída Utvar, která představuje jeden útvar z organizační struktury má dva datové atributy – nazev a zkratka, které se inicializují přes konstruktor. Vedle metod pro získání těchto datových atributů má
Projekt Škola
strana 182
metody equals() a hashCode(). Dále třída obsahuje hlavičky metod pridej() pro vložení podřízených útvarů a pracovníků útvaru a hlavičku metody vypis() pro vypsání označení útvaru, pracovníků útvaru a podřízených útvarů. Úkolem je doplnit do této třídy datové atributy pro uložení podřízených útvarů a pracovníků útvaru a dokončit metody pridej() a vypis().
18.5. Postup řešení 18.5.1.
Deklarace datových atributů a vkládání instancí
Pro uložení podřízených útvarů můžeme použít: ♦ seznam (List), ♦ množinu (Set), ♦ mapu (Map), ♦ pole (Array). Vzhledem k tomu, že podřízené útvary by měly být jedinečné, odpovídá nejlépe množina. Je splněna i podmínka pro použití množin – třída Utvar má implementovány metody equals() a hashCode(). Z implementací množiny použijeme nejrychlejší – HashSet. V deklaraci nadefinujeme datový atribut jako typ Collection, neboť nepotřebujeme používat speciální metody množiny a použití tohoto typu nám umožňuje v budoucnosti snadnou změnu implementace. Pro ukládání pracovníků útvaru též použijeme množinu (třída Osoba má též implementovány metody equals() a hashCode()). Následuje deklarace datových atributů a konstruktor třídy Utvar: private private private private
String nazev; String zkratka; Collection podrizene; Collection pracovnici;
public Utvar(String zkratka, String nazev) { this.zkratka = zkratka; this.nazev = nazev; podrizene = new HashSet (); pracovnici = new HashSet (); }
Pro vkládání podřízených útvarů a pracovníků útvaru se doplní předpřipravené metody pridej(): public void pridej (Osoba osoba) { pracovnici.add(osoba); } public void pridej (Utvar utvar) { podrizene.add(utvar); }
Všimněte si, že jsou zde dvě metody stejného jména s různým typem parametrů. Jedná se o tzv. přetížení metod. Volba správné metody se provádí na základě typu parametru. Podívejte se na kód konstruktoru třídy Skola – zde to vypadá, že se používá stejná metoda pro přidání jak podřízených útvarů, tak pracovníků útvaru. Přetěžování metod zjednodušuje psaní kódu na straně používání metod i částečně na straně třídy, která metodu poskytuje (přijímá zprávy). Následující diagram zobrazuje vztahy mezi instancí třídy Skola a instancemi třídy Utvar. Z diagramu je zřejmé, že jsme vytvořili stromovou strukturu instancí. Z důvodů přehlednosti nejsou do diagramu začleněny instance třídy Osoba – jejich doplnění by pro čtenáře neměl být problém (nezapomeňte, že na instanci Prof. Hindlse budou dva odkazy – jednou z fakulty F4, podruhé z útvaru KSTP).
Projekt Škola
strana 183
Obrázek 18.3 Diagram zobrazuje vztahy mezi instancí třídy Skola a instancemi třídy Utvar
18.5.2.
Výpis organizační struktury
V první verzi metody vypis() ve třídě Utvar se nejdříve vypíše zkratka a jméno útvaru a poté se postupně volá metoda vypis() pro všechny podřízené útvary (aby se vypsala jména všech podřízených útvarů). Pokud podřízený útvar má opět podřízené útvary (fakulta má katedry), tak se opět vypisují jejich jména. public void vypis(boolean vcetneOsob) { System.out.println(zkratka+" - " + nazev); for (Utvar utvar : podrizene) { utvar.vypis(vcetneOsob); } }
Projekt Škola
strana 184
Obrázek 18.4 Výpis organizační struktury pomocí první verze metody vypis() Když si nyní vytvoříme instanci třídy Skola a zavoláme metodu vypis(), bude vypsaná organizační struktura vypadat podobně jako na obrázku 18.4. Výsledek má dva nedostatky: ♦ útvary se nevypisují setříděně (nejlépe dle abecedy), ♦ nejsou vidět úrovně organizační struktury – je potřeba vhodně doplnit odsazování.
Obrázek 18.5 Schématické znázornění volání metod vypis() u jednotlivých instancí v situaci, kdy by v aplikaci byly dvě fakulty a dvě katedry
18.5.3.
Setřídění výpisu
Problém se tříděním lze vyřešit několika způsoby. My si zde popíšeme nejjednodušší – pro uložení instancí podřízených útvarů se použije třída TreeSet(), která automaticky třídí uložené instance.
Projekt Škola
strana 185
Předpokladem pro použití této třídy je však implementace rozhraní Comparable u objektů, které se mají vkládat do této datové struktury. Hlavičku třídy Utvar je potřeba proto rozšířit o implementaci rozhraní: public class Utvar implements Comparable {
Rozhraní Comparable vyžaduje, aby třída obsahovala metodu public int compareTo(Utvar druhy)
která vrací ♦ kladnou hodnotu, pokud aktuální objekt má být zařazen před objekt zadaný jako parametr, ♦ nulu, pokud na pořadí nezáleží (tj. je jedno, v jakém pořadí budou), ♦ zápornou hodnotu, pokud aktuální objekt má být zařazen za objekt zadaný jako parametr. Metoda compareTo() by mohla vypadat následovně38: public int compareTo(Utvar druhy) { return zkratka.compareTo(druhy.zkratka); }
Třídit se bude dle zkratky – využije se metody compareTo() ve třídě String. Vlastní změna z dynamické struktury HashSet na TreeSet je triviální úprava v konstruktoru třídy Utvar (nezapomeňte doplnit import java.util.TreeSet): podrizene = new TreeSet ();
Pro setřídění osob v útvaru je potřeba též implementovat rozhraní Comparable ve třídě Osoba a u datového atributu pracovnici použít implementaci TreeSet. Pokud by jste ukládali útvary či osoby do seznamu (List), tak je možné tento seznam setřídit pomocí Collections.sort(pracovniciList);
Opětně je předpokladem, že objekty vložené do seznamu implementují rozhraní Comparable. Statická metoda Collections.sort() nemůže třídit množiny (implementace Set).
18.5.4.
Zvýraznění organizační struktury (odsazování)
Odsazování je složitější – abychom věděli, o kolik by se měl název konkrétního útvaru odsunout, potřebujeme vědět, na jaké úrovni řízení příslušný útvar je (na nulté úrovni je celá škola, na první úrovni fakulty, na druhé úrovni jsou katedry). Ve třídě Utvar si však neuchováváme informaci o této úrovni, ani o nadřízeném útvaru (pokud by byl známý nadřízený útvar, bylo by možné úroveň dopočítat). Úroveň řízení lze též zjistit z průběhu vlastního výpisu – začíná se na nulté úrovni, při výpisu podřízených útvarů se úroveň řízení zvyšuje. Doplníme tedy metodu vypis() o druhý parametr, který bude určovat úroveň řízení. Metoda vypis() by nyní mohla vypadat následovně: public void vypis(boolean vcetneOsob, int uroven) { for (int i=0; i< uroven; i++) { System.out.print(" "); } System.out.println(zkratka+" - " + nazev); for (Utvar utvar : podrizene) { utvar.vypis(vcetneOsob, uroven+1); } } 38
Někoho by mohlo na tomto kódu překvapit, že se porovná privátní datový atribut zkratka z této instance s privátním datovým atributem zkratka z druhé instance – vypadá to na porušení zapouzdření. Modifikátory přístupu ale určují přístup k datovým atributům a metodám na úrovni třídy, ne na úrovni instancí. Zde jsou obě proměnné ze stejné třídy, tudíž k narušení zapouzdření u privátních proměnných nedochází.
Projekt Škola
strana 186
Před spuštěním ještě nezapomeňte upravit volání metody vypis() ve třídě Skola – je potřeba doplnit druhý parametr. Výpis poté bude vypadat následovně:
Obrázek 18.6 Výpis útvarů se setříděním a odsazením jednotlivých úrovní
18.5.5.
Doplnění výpisu osob
Vypsání pracovníků je jednoduché – do metody vypis() se doplní cyklus procházející seznam pracovníků. Jména pracovníků je též vhodné odsazovat – protože odsazování je nyní potřeba na více místech, přesuneme kód pro odsazování do samostatné privátní metody. Metody vypis() a odsadit() by mohly vypadat následovně: public void vypis(boolean vcetneOsob, int uroven) { odsadit(uroven*3); System.out.println(zkratka+" - " + nazev); if (vcetneOsob) { for (Osoba osoba : pracovnici) { odsadit((uroven+1)*3); System.out.println("- " + osoba.toString()); } } for (Utvar utvar : podrizene) { utvar.vypis(vcetneOsob, uroven+1); } } private void odsadit (int pocetMezer) { for (int i=0; i<pocetMezer; i++) { System.out.print(" "); } }
18.6. Domácí úkoly 1. Doplňte do třídy Skola metodu, která by měla jako parametr zkratku útvaru a tento útvar vyhledala a vypsala.
Projekt Škola
strana 187
2. Rozšiřte předchozí zadání o vypsání i nadřízených útvarů, tj. pokud bude parametrem „KIT“, vypíše se VSE – Vysoká škola ekonomická F4 – Fakulta informatiky a statistiky KIT – katedra informačních technologií 3. Vytvořte obdobnou metodu, jako je v předchozím zadání, pro vyhledání osob. Nezapomeňte, že někteří pracovníci jsou ve více útvarech. 4. Doplňte evidenci funkcí u osob – např. rektor, děkan, vedoucí katedry, člen katedry. Zamyslete se nad tím, zda stačí některou třídu doplnit o datové atributy či je potřeba použít další třídu. Nezapomeňte, že některé osoby mohou mít různé funkce v různých útvarech (např. děkan je současně i vedoucím/členem katedry). 5. Rozšiřte předchozí úlohu o setřídění výstupu dle funkcí – např. u katedry by se osoby měly vypisovat dle následujícího pořadí funkcí: vedoucí katedry, zástupce vedoucího katedry, sekretářka, profesoři, docenti, odborní asistenti, asistenti, doktorandi.
Projekt Škola
strana 188
Projekt Adventura
strana 189
19.Projekt Adventura 19.1. Základní popis, zadání úkolu Pracujeme na projektu Adventura, který je ke stažení na java.vse.cz. Po otevření v BlueJ vytvoříme instanci třídy Hra. Po zavolání metody hraj() se spustí jednoduchá adventura s textovým rozhraním, která umožní hráči procházet jednotlivými místnostmi hry. Hráč může zadávat příkazy „jdi“, „napoveda“ a „konec“. Na obrázku 19.1 je zachycen průběh „hraní“ této hry.
Obrázek 19.1 Jednoduchá ukázka průběhu komunikace s adventurou V tomto projektu budeme řešit tyto úkoly: ♦ prvním úkolem je doplnit hru o další místnost/prostor, ♦ dalším je změnit příkaz „napoveda“ na příkaz „pomoc“ (tj. aby uživatel místo příkazu „napoveda“ psal příkaz „pomoc“), ♦ dalším úkolem je doplnit „vítězství“, tj. aby po dosažení konkrétní místnosti hra sama skončila, ♦ posledním úkolem, který si zde ukážeme je doplnění hry o věci – tj. aby v místnostech byly jednotlivé věci, hráč je mohl sbírat do batohu či z batohu vyndávat a pokládat do místnosti. Tento projekt má následující cíle: ♦ ukázat možnost, jak vytvořit síťovou strukturu instancí, ♦ ukázat vytváření nových tříd, doplňování metod do stávajících tříd.
19.2. Struktura tříd Projekt Adventura se skládá z následujících tříd (obrázek z BlueJ):
Projekt Adventura
strana 190
Obrázek 19.2 Struktura tříd projektu Adventura z BlueJ
19.3. Popis komunikace mezi objekty Hra je rozdělena do 5 tříd, které mají takto rozděleny odpovědnosti: ♦ Třída RizeniHry obsahuje základ řízení hry (načtení řádku od uživatele z klávesnice, vytvoření příkazu, jeho předání ke zpracování do instance třídy Hra a vypsání výsledku na obrazovku). Do této třídy je soustředěno čtení z klávesnice a výpis na obrazovku. ♦ Na základě třídy Prikaz se vytváří instance obsahující povel zadaný uživatelem. Třída Prikaz rozdělí vstupní řádek na dvě části – vlastní slovo příkazu a případné druhé slovo příkazu. ♦ Instance třídy SeznamPrikazu obsahuje seznam všech přípustných příkazů. Instance vrací seznam dostupných příkazů a umí zjistit, zda příslušný řetězec je platný příkaz, ♦ Třída Hra představuje hlavní logiku hry – vytvářejí se v ní místnosti a seznam příkazů, obsahuje metody řešící jednotlivé příkazy uživatele. Z RizeniHry se volá hlavně metoda zpracujPrikaz, kterou se předává příkaz zadaný uživatelem. Metoda vrací řetězec, který se má zobrazit na obrazovce. ♦ Instance třídy Mistnost obsahuje jméno místnosti a seznam místností, do kterých vedou východy z aktuální místnosti. Instance pomocí metody seznamVychodu() vrací řetězec se seznamem východů, metoda sousedniMistnost() vrací instanci sousední místnosti odpovídající zadanému řetězci. Diagram tříd používaný v BlueJ zachycuje základní vazby mezi třídami. V některých situacích je pro pochopení aplikace vhodné vytvořit si diagram tříd, který obsahuje i datové atributy a metody. Pro naši adventuru je takový diagram na obrázku 19.3.
Projekt Adventura
Obrázek 19.3 Diagram tříd včetně datových atributů a metod
strana 191
Projekt Adventura
strana 192
19.4. Výpis kódu třídy Místnost 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
import java.util.Set; import java.util.HashSet; /** * Třída Mistnost popisuje jednotlivou místnost ve hře. * Tato třída je součástí projektu jednoduché textové hry. * "Mistnost" reprezentuje jedno místo (místnost, prostor, ..) * ve scénáři hry. Místnost může mít sousední místnosti * připojené přes východy. Pro každý východ si místnost ukládá * odkaz na sousedící místnost (instanci třídy Mistnost). * *@author Michael Kolling, Lubos Pavlicek, Jarmila Pavlickova *@version 3.0 *@created květen 2005 */ class Mistnost { private String nazev; private String popis; private Set<Mistnost> vychody;
// obsahuje sousední místnosti
/** * Vytvoření místnosti (pojmenovaný prostor) se zadaným * popisem, např. "kuchyň", "hala", "trávník před domem" * *@param nazev Jméno místnosti, jednoznačný * identifikátor, pokud možno jedno slovo *@param popis Popis místnosti. */ public Mistnost(String nazev, String popis) { this.nazev = nazev; this.popis = popis; vychody = new HashSet<Mistnost>(); } /** * Definuje východ z místnosti (sousední/vedlejší místnost). * Vzhledem k tomu, že je použit Set pro uložení východů, * může být sousední místnost uvedena pouze jednou * (tj. nelze mít dvoje dveře do stejné sousední místnosti). * Druhé zadání stejné místnosti tiše přepíše předchozí zadání * (neobjeví se žádné chybové hlášení). * Lze zadat též cestu ze/do sebe sama. * * @param vedlejsi místnost, která sousedí s aktuální * místností. */ public void setVychod(Mistnost vedlejsi) { vychody.add(vedlejsi); } /** * Metoda equals pro porovnání dvou místností. Překrývá se metoda * equals ze třídy Object. Dvě místnosti jsou shodné, pokud mají * stejné jméno.
Projekt Adventura 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
* Tato metoda je důležitá z hlediska správného fungování * seznamu místností (Set). * * Bližší popis metody equals je u třídy Object. * *@param o object, který se má porovnávat s aktuálním *@return vrací hodnotu true, pokud zadaná místnost má * stejné jméno, jinak false */ public boolean equals (Object o) { if (o instanceof Mistnost) { Mistnost druha = (Mistnost)o; return nazev.equals(druha.nazev); } else { return false; } } /** * Metoda hashCode vrací číselný identifikátor instance, * který se používá pro optimalizaci ukládání * v dynamických datových strukturách. * Při překrytí metody equals je potřeba překrýt i * metodu hashCode. * Podrobný popis pravidel pro vytváření metody hashCode * je ve třídě Object */ public int hashCode() { return nazev.hashCode(); } /** * Vrací jméno místnosti (bylo zadáno při vytváření místnosti * jako parametr konstruktoru) * *@return Jméno místnosti */ public String getNazev() { return nazev; } /** * Vrací "dlouhý" popis místnosti, který může vypadat * následovně: * Jsi v mistnosti/prostoru vstupni hala budovy VSE * na Jiznim meste. * vychody: chodba bufet ucebna * *@return Dlouhý popis místnosti */ public String dlouhyPopis() { return "Jsi v mistnosti/prostoru " + popis + ".\n" + seznamVychodu(); }
strana 193
Projekt Adventura
strana 194
112 /** 113 * Vrací textový řetězec, který popisuje sousední místnosti 114 * (východy) 115 * vychody: chodba bufet ucebna 116 * 117 *@return Seznam sousedních místností (východů) 118 */ 119 private String seznamVychodu() { 120 String vracenyText = "vychody:"; 121 for (Mistnost sousedni : vychody) { 122 vracenyText += " " + sousedni.getNazev(); 123 } 124 return vracenyText; 125 } 126 127 /** 128 * Vrací místnost, která sousedí s aktuální místností 129 * a jejíž jméno je zadáno jako parametr. Pokud místnost 130 * s udaným jménem nesousedí s aktuální místností, 131 * vrací se hodnota null. 132 * 133 *@param jmenoSousedni Jméno sousední místnosti (východu) 134 *@return Místnost, která se nachází za příslušným východem, 135 * nebo hodnota null, pokud místnost zadaného jména není 136 * sousedem. 137 */ 138 public Mistnost sousedniMistnost(String jmenoSousedni) { 139 if (jmenoSousedni == null) { 140 return null; 141 } 142 for ( Mistnost sousedni :vychody ){ 143 if (sousedni.getNazev().equals(jmenoSousedni)) { 144 return sousedni; 145 } 146 } 147 return null; // místnost nenalezena 148 } 149 }
19.5. Výpis kódu třídy Hra 1 /** 2 * Třída Hra představující logiku adventury. 3 * 4 * Tato třída inicializuje další třídy: 5 * vytváří všechny místnosti (třída Mistnost), 6 * vytváří seznam platných příkazů. 7 * Ve hře se též vyhodnocují jednotlivé příkazy zadané 8 * uživatelem. 9 * 10 *@author Michael Kolling, Lubos Pavlicek, Jarmila Pavlickova 11 *@version 3.0 12 *@created květen 2005 13 */ 14
Projekt Adventura
strana 195
15 class Hra { 16 private SeznamPrikazu platnePrikazy; 17 // obsahuje seznam přípustných slov 18 private Mistnost aktualniMistnost; 19 private boolean konecHry = false; 20 21 /** 22 * Vytváří hru, inicializuje místnosti 23 * a seznam platných příkazů. 24 */ 25 public Hra() { 26 zalozMistnosti(); 27 platnePrikazy = new SeznamPrikazu(); 28 } 29 30 /** 31 * Vytváří jednotlivé místnosti a propojuje je pomocí východů. 32 */ 33 private void zalozMistnosti() { 34 Mistnost hala; 35 Mistnost ucebna; 36 Mistnost bufet; 37 Mistnost chodba; 38 Mistnost kancelar; 39 40 // vytvářejí se jednotlivé místnosti 41 hala = new Mistnost("hala", 42 "vstupni hala budovy VSE na Jiznim meste"); 43 ucebna = new Mistnost("ucebna", "prednaskova ucebna 103JM"); 44 bufet = new Mistnost("bufet", 45 "bufet, kam si muzete zajit na svacinku"); 46 chodba = new Mistnost("chodba","spojovaci chodba"); 47 kancelar = new Mistnost("kancelar", 48 "kancelar vaseho vyucujiciho Javy"); 49 // přiřazují se východy z místností (sousedící místnosti) 50 hala.setVychod(ucebna); 51 hala.setVychod(chodba); 52 hala.setVychod(bufet); 53 ucebna.setVychod(hala); 54 bufet.setVychod(hala); 55 chodba.setVychod(hala); 56 chodba.setVychod(kancelar); 57 kancelar.setVychod(chodba); 58 aktualniMistnost = hala; // hra začíná v místnosti hala 59 } 60 /** 61 * Vrátí úvodní zprávu pro hráče. 62 */ 63 public String vratUvitani() { 64 return "Vitejte!\n" + 65 "Toto je nova, neuveritelne nudna adventura.\n" + 66 "Napiste 'napoveda', pokud si nevite rady, 67 jak hrat dal.\n" + 68 "\n" + 69 aktualniMistnost.dlouhyPopis(); 70 } 71
Projekt Adventura 72 /** 73 * Vrátí závěrečnou zprávu pro hráče. 74 */ 75 public String vratEpilog() { 76 return "Dik, ze jste si zahrali. Ahoj."; 77 } 78 79 /** 80 * Vrací true, pokud hra skončila. 81 */ 82 public boolean konecHry() { 83 return konecHry; 84 } 85 86 /** 87 * Metoda zpracuje příkaz uvedený jako parametr, 88 * tj. spustí odpovídající metodu. 89 * Metoda vrací řetězec, který se má vypsat na obrazovku. 90 * 91 *@param prikaz příkaz, který se má zpracovat (provést) 92 *@return vrací se řetězec, který se má vypsat na obrazovku 93 */ 94 public String zpracujPrikaz(Prikaz prikaz) { 95 String textKVypsani=" .... "; 96 if (platnePrikazy.jePlatnyPrikaz(prikaz.getSlovoPrikazu())){ 97 String povel = prikaz.getSlovoPrikazu(); 98 if (povel.equals("napoveda")) { 99 textKVypsani = napoveda(); 100 } 101 else if (povel.equals("jdi")) { 102 textKVypsani = jdi(prikaz); 103 } 104 else if (povel.equals("konec")) { 105 textKVypsani = konec(prikaz); 106 } 107 } 108 else { 109 textKVypsani="Nevim co tim myslis, tento prikaz neznam?"; 110 } 111 return textKVypsani; 112 } 113 114 // následuje implementace jednotlivých příkazů 115 116 /** 117 * V případě, že příkaz má jen jedno slovo "konec" hra končí, 118 * jinak pokračuje např. při zadání "konec a". 119 * 120 * @return Vrací true v případě, že příkaz má jen jedno slovo 121 * "konec", jinak vrací false 122 */ 123
strana 196
Projekt Adventura
strana 197
124 private String konec(Prikaz prikaz) { 125 if (prikaz.maDruheSlovo()) { 126 return "Ukoncit co? Nechapu, proc jste zadal druhe 127 slovo."; 128 } 129 else { 130 konecHry = true; 131 return "hra ukončena příkazem konec"; 132 } 133 } 134 135 /** 136 * Vypíše základní nápovědu po zadání příkazu "napoveda". 137 * Nyní se vypisuje vcelku primitivní zpráva 138 * a seznam dostupných příkazů. 139 */ 140 private String napoveda() { 141 return "Ztratil ses. Jsi sam(a). Toulas se\n" 142 + "po arealu skoly na Jiznim meste.\n" 143 + "\n" 144 + "Muzes zadat tyto prikazy:\n" 145 + platnePrikazy.vratSeznamPrikazu(); 146 } 147 148 /** 149 * Příkaz "jdi". 150 * Zkouší se vyjít zadaným směrem/do zadané místnosti. 151 * Pokud místnost existuje, vstoupí se do nové místnosti. 152 * Pokud zadaná sousední místnosti (východ) není, 153 * vypíše se chybové hlášení. 154 * 155 *@param prikaz jako druhý parametr obsahuje jméno místnosti, 156 * do které se má jít. 157 */ 158 private String jdi(Prikaz prikaz) { 159 if (!prikaz.maDruheSlovo()) { 160 // pokud chybí druhé slovo (sousední místnost), tak 161 return "Kam mám jít? Musíš zadat jméno místnosti"; 162 } 163 String smer = prikaz.getDruheSlovo(); 164 // zkoušíme přejít do sousední místnosti 165 Mistnost sousedniMistnost = 166 aktualniMistnost.sousedniMistnost(smer); 167 if (sousedniMistnost == null) { 168 return "Tam se odsud jit neda!"; 169 } 170 else { 171 aktualniMistnost = sousedniMistnost; 172 return aktualniMistnost.dlouhyPopis(); 173 } 174 } 175 }
Projekt Adventura
strana 198
19.6. Výpis kódu třídy RizeniHry 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; /** * Class RizeniHry * * Toto je hlavní třída aplikace. * Je to velmi jednoduchá textová hra * - uživatel se pohybuje v daném prostoru. * Je určena jako základ pro další zajímavá rozšíření. * Tato třída vytváří instanci třídy Hra, * která představuje logiku aplikace. * Čte jednotlivé příkazy zadané uživatelem * a předává je logice a vypisuje odpověď logiky na konzoli. * Pokud chcete hrát tuto hru, vytvořte instanci této třídy * a poté na ní vyvolejte metodu "hraj". * * *@author Michael Kolling, Lubos Pavlicek, Jarmila Pavlickova *@version 3.0 *@created květen 2005 */ class RizeniHry { private Hra hra; /** * Vytváří hru. */ public RizeniHry() { hra = new Hra(); } /** * Hlavní metoda hry. * Cyklí se do konce hry (dokud metoda konecHry() z logiky * nevrátí hodnotu true) */ public void hraj() { System.out.println(hra.vratUvitani()); // základní cyklus programu - opakovaně se čtou příkazy // a poté se provádějí do konce hry. while (!hra.konecHry()) { String radek = prectiString(); Prikaz prikaz = new Prikaz(radek); System.out.println(hra.zpracujPrikaz(prikaz)); } System.out.println(hra.vratEpilog()); } /** * Metoda přečte příkaz z příkazového řádku * *@return Vrací přečtený příkaz jako instanci třídy String
Projekt Adventura
strana 199
57 */ 58 private String prectiString() { 59 String vstupniRadek=""; 60 System.out.print("> "); // vypíše se prompt 61 BufferedReader vstup = new BufferedReader 62 (new InputStreamReader(System.in)); 63 try { 64 vstupniRadek = vstup.readLine(); 65 } 66 catch (java.io.IOException exc) { 67 System.out.println("Vyskytla se chyba během čtení příkazu:" 68 + exc.getMessage()); 69 } 70 return vstupniRadek; 71 } 72 }
19.7. Výpis kódu třídy SeznamPrikazu 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
import java.util.Set; import java.util.TreeSet; /** * Třída SeznamPrikazu - obsahuje seznam přípustných příkazů * hry. Používá se pro rozpoznávání příkazů. * * Tato třída je součástí jednoduché textové hry. * *@author Michael Kolling, Lubos Pavlicek, Jarmila Pavlickova *@version 3.0 *@created květen 2005 */ class SeznamPrikazu { // set pro uložení přípustných příkazů private Set<String> platnePrikazy; /** * Konstruktor */ public SeznamPrikazu() { platnePrikazy = new TreeSet<String>(); platnePrikazy.add("jdi"); platnePrikazy.add("konec"); platnePrikazy.add("napoveda"); } /** * Kontroluje, zda zadaný řetězec je přípustný příkaz. * * @param retezec Řetězec, který se testuje, zda je to přípustný příkaz * @return Vrací true, pokud zadaný řetězec je přípustný příkaz */ public boolean jePlatnyPrikaz(String retezec) { return platnePrikazy.contains(retezec); }
Projekt Adventura
strana 200
39 /** 40 * Vrací seznam přípustných příkazů, jednotlivé příkazy jsou 41 * odděleny mezerou. 42 * @return Řetězec, který obsahuje seznam přípustných příkazů 43 */ 44 public String vratSeznamPrikazu() { 45 String seznam = ""; 46 for (String slovoPrikazu : platnePrikazy){ 47 seznam += slovoPrikazu + " "; 48 } 49 return seznam; 50 } 51 }
19.8. Výpis kódu třídy Prikaz 1 /** 2 * Class Prikaz - část jednoduché textové adventury. 3 * 4 * Třída udržuje informace o zadaném příkazu. V současné době 5 * může příkaz mít až dvě slova (např. "jdi kuchyn"). 6 * Pokud má příkaz pouze jedno slovo, je jako druhé slovo 7 * uložena hodnota null. 8 * 9 * Třídu je možné rozšířit na příkazy se třemi či více slovy. 10 * 11 *@author Michael Kolling, Lubos Pavlicek, Jarmila Pavlickova 12 *@version 3.0 13 *@created květen 2005 14 */ 15 16 class Prikaz { 17 private String slovoPrikazu = null; 18 private String druheSlovo = null; 19 20 /** 21 * Vytváří instanci třídy Prikaz. Musí být zadáné první a druhé 22 * slovo příkazu. Pokud příkaz nemá druhé slovo, vloží se 23 * místo něho hodnota null. 24 * 25 *@param prvniSlovo První slovo příkazu 26 *@param druheSlovo Druhé slovo příkazu 27 */ 28 public Prikaz(String prvniSlovo, String druheSlovo) { 29 slovoPrikazu = prvniSlovo; 30 this.druheSlovo = druheSlovo; 31 } 32 /** 33 * Druhý konstruktor pro vytvoření instance třídy Prikaz. 34 * Konstruktor převezme řetězec přečtený z příkazového řádku a 35 * vytvoří instanci třídy Prikaz. Pokud příkaz nemá druhé 36 * slovo, vloží se jako druhý parametr hodnota null. 37 * 38 *@param vstupniRadek Retezec se vstupnim radkem 39 */
Projekt Adventura
strana 201
40 public Prikaz(String vstupniRadek) { 41 String [] slova = vstupniRadek.split("[ \t]+"); 42 if (slova.length > 0) { 43 slovoPrikazu = slova[0]; // první slovo 44 } 45 if (slova.length > 1) { 46 druheSlovo = slova[1]; // druhé slovo 47 } 48 // poznámka: ignoruje se zbytek řádky 49 } 50 51 /** 52 * Vrací se příkaz (první slovo příkazu). 53 * 54 *@return Příkaz (první slovo příkazu), jako řetězec. 55 */ 56 public String getSlovoPrikazu() { 57 return slovoPrikazu; 58 } 59 60 /** 61 * Vrací druhé slovo příkazu. Pokud neexistuje, vrací se 62 * hodnotu null. 63 *@return Druhé slovo příkazu. 64 */ 65 public String getDruheSlovo() { 66 return druheSlovo; 67 } 68 69 /** 70 * Vrací hodnotu true, pokud příkaz má druhé slovo 71 * 72 *@return true, pokud má příkaz druhé slovo 73 */ 74 public boolean maDruheSlovo() { 75 return (druheSlovo != null); 76 } 77 }
19.9. Postup řešení Před řešením jednotlivých příkladů doporučujeme pro pochopení aplikace: ♦ spustit si základ hry a zkusit se pohybovat po místnostech, ♦ udělat první dva domácí úkoly (vytvoření sekvenčních diagramů).
19.9.1.
Úkol 1: doplnění další místnosti
Před řešením této úlohy odpovíme na otázku, kolik je ve hře místností, tj. kolik je instancí třídy Mistnost: ♦ počet instancí odpovídá počtu new s konstruktorem třídy Mistnost, místnosti se vytvářejí pouze ve třídě Hra – celkem 5 místností, ♦ je potřeba odlišovat počet instancí a počet odkazů – na jednu instanci lze odkazovat z různých míst, konkrétně na instanci místnosti s názvem „hala“ jsou na začátku 4 odkazy: * z místnosti „chodba“ je odkaz na „halu“ v seznamu východů (datový atribut vychody), * z místnosti „bufet“ je východ do haly, * z místnosti „ucebna“ je východ do haly,
Projekt Adventura
strana 202
*
poslední odkaz je z instance třídy Hra – odkaz je uložen v datovém atributu aktualniMistnost (viz řádek 60 zdrojového kódu třídy Hra), ♦ místnost, na kterou odkazuje datový atribut aktualniMistnost se mění při přechodu do jiné místnosti (po zadání příkazu „jdi ucebna“ bude datový atribut aktualniMistnost odkazovat na instanci třídy Mistnost s názvem „ucebna“). Při změně hodnoty datového atributu aktualniMistnost: * nemění se žádná instance třídy Mistnost, pouze se mění stav instance třídy Hra, * neztrácí se žádná instance třídy Mistnost – v Javě se ruší instance v případě, že na ně není žádný odkaz, což zde neplatí, neboť místnosti odkazují na sebe navzájem přes východy. Následující obrázek zobrazuje okamžitý stav (konkrétně stav na začátku hry) mezi instancemi třídy Mistnost a instancí třídy Hra. V průběhu aplikace se vztahy mění – z instance třídy Hra odkazuje datový atribut aktualniMistnost na různé instance třídy Mistnost. : Hra -aktualniMistnost
: Mistnost
: Mistnost
nazev = "hala" popis = "vstupní ..." vychody
nazev = "ucebna" popis = "přednášková ..." vychody
: Mistnost nazev = "chodba" popis = "spojovací ..." vychody
: Mistnost nazev = "bufet" popis = "bufet, ..." vychody
: Mistnost nazev = "kancelar" popis = "kancelář ..." vychody
Obrázek 19.4 Diagram objektů zobrazuje vztahy mezi instancemi třídy Mistnost a instancí třídy Hra v projektu Adventura na začátku hry Vlastní hra je poměrně triviální, proto ji zde nebudeme podrobněji popisovat. Při vytváření místnosti nezapomeňte na východy z místnosti/vchody do místnosti. Ověřte si dostupnost nové místnosti.
19.9.2.
Úkol 2: změna příkazu „napoveda“ na „pomoc“
Cílem tohoto úkolu je uvědomit si, do kterých tříd je potřeba zasáhnout při změně názvu příkazu či při přidávání příkazů. Vlastní úprava je triviální – jsou potřeba 2+1 úpravy: ♦ upravit řetězec v seznamu příkazů ve třídě SeznamPrikazu (řádek 25), ♦ upravit řetězec v metodě zpracujPrikaz ve třídě Hra (řádek 98),
Projekt Adventura
strana 203
♦ v případě příkazu nápověda je ještě potřeba upravit část úvodního textu ve třídě Hra (řádek 66). Kdybychom zapomněli udělat jednu z prvních dvou úprav nebo nezapsali na obě místa v kódu stejné řetězce, nebude příkaz fungovat. Aby se tomuto zabránilo (a případně i ušetřilo pár bytů vnitřní paměti), je vhodné psát řetězec pouze jednou. Existují dvě řešení: ♦ Použít výčtový typ – tuto variantu necháváme na čtenářích. ♦ Použít pojmenované konstanty, ve třídě SeznamPrikazu by se na začátku nadefinovaly pojmenované konstanty: private Set<String> platnePrikazy; public static final String PRIKAZ_JDI=”jdi”; public static final String PRIKAZ_KONEC=”konec”; public static final String PRIKAZ_NAPOVEDA=”napoveda”; /** * Konstruktor */ public SeznamPrikazu() { platnePrikazy = new TreeSet<String>(); platnePrikazy.add(PRIKAZ_JDI); platnePrikazy.add(PRIKAZ_KONEC); platnePrikazy.add(PRIKAZ_NAPOVEDA); }
ve třídě Hra by příslušná část metody zpracujPrikaz() mohla vypadat následovně: if (platnePrikazy.jePlatnyPrikaz(prikaz.getSlovoPrikazu())){ String povel = prikaz.getSlovoPrikazu(); if (povel.equals(SeznamPrikazu.PRIKAZ_NAPOVEDA)) { textKVypsani = napoveda(); } else if (povel.equals(SeznamPrikazu.PRIKAZ_JDI)) { textKVypsani = jdi(prikaz); } else if (povel.equals(SeznamPrikazu.PRIKAZ_KONEC)) { textKVypsani = konec(prikaz); } }
Po této úpravě již stačí změnit vlastní slovo příkazu pouze na jednom místě – ve třídě SeznamPrikazu. Práci s příkazy lze rozšířit několika směry: ♦ podpora alternativních slov pro stejnou akci (např. aby uživatel mohl použít jako slovo „napoveda“, tak slovo „pomoc“), ♦ podpora různých jazykových verzí – zde by bylo vhodné nastudovat podporu pro internacionalizaci a lokalizaci začleněnou do Javy, ♦ přiřazení akcí k vlastním příkazům, tj. aby součástí definice příkazu bylo nejen vlastní slovo příkazu, ale i akce, která se má po zadání tohoto příkazu provést – tato problematika přesahuje rozsah skript.
Projekt Adventura
19.9.3.
strana 204
Úkol 3: doplnění vítězství
Pod vítězstvím rozumíme, že po dosažení místnosti „kancelář“ by hra měla skončit a vypsat příslušný závěrečný text. Základní cyklus ve třídě RizeniHry vypadá následovně: while (!hra.konecHry()) { String radek = prectiString(); Prikaz prikaz = new Prikaz(radek); System.out.println(hra.zpracujPrikaz(prikaz)); } System.out.println(hra.vratEpilog());
tj. v rámci cyklu se čtením vstupního řádku a zpracováním tohoto příkazu se testuje, zda není konec hry (volá se metoda konecHry(), která při konci hry vrací hodnotu true, jinak hodnotu false). Pokud je konec hry, vypíše se závěrečné hlášení (metoda vratEpilog()). My potřebujeme, aby po dosažení místnosti kancelář metoda konecHry() vracela hodnotu true a následně by metoda vratEpilog() měla vrátit odpovídající hlášení (odlišné od situace, kdy uživatel zadal příkaz konec). Metoda konecHry() ve třídě Hra vrací hodnotu logické proměnné konecHry – tj. po dosažení místnosti kancelář je potřeba nastavit tuto proměnnou na hodnotu true. Míst, kde to lze nastavit je více, asi nejvhodnější je umístit potřebný kód na konec metody zpracujPrikaz() – na řádek 114:
}
if (aktualniMistnost == kancelar) { konecHry = true; } return textKVypsani;
Aby tento kód fungoval, je potřeba si uvědomit, že nyní proměnnou kancelar potřebujeme ve dvou metodách – v metodě zalozMistnosti() a v metodě zpracujPrikaz() – je potřeba z této proměnné udělat datový atribut: ♦ na začátek třídy se doplní deklarace (včetně modifikátoru přístupu private): private Mistnost kancelar;
♦ v metodě zalozMistnosti() se musí deklarace zrušit (pokud to neuděláte, tak Vás překladač neupozorní na možnou chybu, ale aplikace nebude fungovat). Vrácení vítězného textu lze řešit přes podmínku v metodě vratEpilog(): public String vratEpilog() { if (aktualniMistnost == kancelar) { return "Gratuluji, dosáhli jste kanceláře. } else { return "Dik, že jste si zahráli. Ahoj."; } }
Ahoj.";
Vhodnější je využít pro závěrečný řetězec datový atribut, který by metoda vratEpilog() vracela. Datový atribut by byl deklarován na začátku třídy Hra: private String epilog="Dík, že jste si zahráli.
Ahoj.";
V metodě zpracujPrikaz() by se nastavil alternativní text: if (aktualniMistnost == kancelar) { konecHry = true; epilog="Gratuluji, dosáhli jste kanceláře. } return textKVypsani;
Ahoj.";
Projekt Adventura
strana 205
A metoda vratEpilog() by byla jednoduchá: public String vratEpilog() { return epilog; }
19.9.4.
Úkol 4: doplnění věcí do místností
Věci by měly být uloženy v místnostech, v jedné místnosti může být více věcí. Některé věci lze přenášet, některé ne. Uživatel má možnost věci v místnosti sbírat a ukládat je do batohu (příkaz „seber“), dále může vybrat věc v batohu a uložit ji do místnosti (příkaz „polož“). Uživatel též musí mít možnost vypsat seznam věcí v místnosti – při vstupu do místnosti se vypíše vedle popisu místnosti i seznam věcí v místnosti. Dále bude mít uživatel k dispozici příkaz „inventar“, který vypíše obsah batohu. Doplnění věcí do místností lze rozdělit do následujících částí: ♦ návrh třídy Vec, která představuje vzor pro jednotlivé věci ve hře, ♦ doplnění třídy Mistnost o metodu, která umožní vkládat věci do místnosti, metodu, která umožní vybrat věc z místnosti a rozšíření metody dlouhyVypis() o vypsání seznamu věcí v místnosti, ♦ inicializace jednotlivých věcí ve hře a jejich umístění do místností, ♦ doplnění uživatelského příkazu seber, pro sebrání věci z místnosti do batohu, ♦ doplnění hry o batoh, batoh by měl být představován samostatnou třídou (teoreticky poté bude možno vytvořit více batohů), batoh by měl mít omezenou kapacitu, ♦ doplnění uživatelského příkazu poloz, který přesune věc z batohu do místnosti, ♦ doplnění uživatelského příkazu inventar, který vypíše obsah batohu. My si zde ukážeme pouze první čtyři části, poslední tři necháváme na čtenáře. Návrh třídy Vec Třída Vec představuje vzor pro jednotlivé věci, které se budou používat v programu. Z našeho zadání vyplývá, že u věci potřebujeme dva datové atributy: ♦ název věci – použijeme typ String, ♦ informaci o tom, zda je věc přenositelná – použijeme proměnnou typu boolean. Hodnoty těchto datových atributů bude vhodné nastavit při vytvoření instance třídy Vec – přes parametry konstruktoru. Věci nezajišťují nějaké zvláštní činnosti ani není potřeba měnit hodnotu datových atributů, proto postačuje doplnit třídu Vec o metody, které budou vracet hodnoty datových atributů: ♦ metodu, která bude vracet název věci, ♦ metodu, která bude vracet informaci o tom, zda je věc přenositelná. 1 /** 2 * Trida Vec - popisuje jednotlivou věc ve hře 3 * 4 * Tato třída je součástí jednoduché textové hry. 5 * 6 * "Vec" reprezentuje jednu věc ve scénáři hry. Věc může být 7 * přenositelná (tj. hráč některé věci může vzít a některé ne). 8 * 9 *@author Lubos Pavlicek, Jarmila Pavlickova 10 *@version 3.0 11 *@created květen 2005 12 */ 13
Projekt Adventura
strana 206
14 class Vec { 15 private String nazev; 16 private boolean prenositelna; 17 18 /** 19 * Vytvoření věci se zadaným názvem 20 * 21 *@param nazev Jméno věci, jednoznačný identifikátor, 22 pokud možno jedno slovo 23 *@param prenositelna Parametr určuje, zda je věc 24 přenositelná hráčem 25 */ 26 public Vec(String nazev, boolean prenositelna) { 27 this.nazev = nazev; 28 this.prenositelna = prenositelna; 29 } 30 31 /** 32 * Vrací název věci. 33 * 34 * @return název věci 35 */ 36 public String getNazev() { 37 return nazev; 38 } 39 40 /** 41 * Vrací informaci o tom, zda je věc přenositelná ve hře. 42 * 43 * @return true, pokud je věc přenositelná, jinak false 44 */ 45 public boolean jePrenositelna() { 46 return prenositelna; 47 } 48 }
Doplnění místností o věci Při doplnění místností o věci se rozšíří datové atributy a metody třídy Mistnost. Ve třídě Mistnost přibude datový atribut, do kterého se budou ukládat věci umístěné v místnosti. Pro uložení je vhodné použít některou z datových struktur, do kterých lze vložit více prvků (věcí). V úvahu připadají seznamy (List), množiny (Set), mapy (Map) i pole (array). Každá z těchto struktur má své výhody a nevýhody: ♦ seznam (List) – do místnosti lze uložit více stejných věcí (věcí se stejným jménem), což nemusí být žádoucí, ♦ množina (Set) – do místnosti lze vložit věc se stejným názvem pouze jednou, předpokladem použití je, že třída Vec bude implementovat metody equals() a hashCode(), při vkládání je potřeba též dávat pozor, aby se věci neztrácely (viz kapitola o datových strukturách), ♦ mapa (Map) – do místnosti lze vložit věc se stejným názvem pouze jednou, lze zajistit jednodušší vyhledávání věci než v případě použití množiny, ♦ pole (array) – nevýhodou pole je, že se musí předem určit maximální počet věcí, které lze uložit do nějaké místnosti, vlastní kód metod bude delší než v ostatních případech, při dobrém naprogramování bude nejrychlejší.
Projekt Adventura
strana 207
My použijeme pro uložení věcí seznam (List), konkrétně ArrayList. Následuje deklarace seznamu věcí na začátku třídy Mistnost: private List seznamVeci;
Inicializace datového atributu by měla být v konstruktoru: seznamVeci = new ArrayList();
Instance třídy Mistnost musí podporovat tyto činnosti v souvislosti s věcmi: ♦ vložení věci do místnosti – metoda vlozVec(), která bude mít jako parametr věc, která se má vložit do místnosti (do seznamu věcí v místnosti), ♦ zjištění, zda je v místnosti uložena nějaká věc – metoda obsahujeVec(), která bude mít jako parametr název věci a bude vracet hodnotu true či false, ♦ vybrání věci z místnosti – metoda vyberVec() vybere věc z místnosti a předá ji jako návratovou hodnotu (pokud věc v místnosti neexistuje či není přenositelná, vrací hodnotu null), ♦ vypsání seznamu věcí v místnosti – seznam věcí připojíme k výpisu informací o místnosti v metodě dlouhyVypis(), pro vytvoření textové položky obsahující jména všech věcí v místnosti vytvoříme samostatnou privátní metodu seznamVeci(). Následuje kód prvních tří metod – vlozVec(), obsahujeVec() a vyberVec(): public void vlozVec(Vec neco) { seznamVeci.add(neco); } public boolean obsahujeVec(String nazevVeci) { for ( Vec neco : seznamVeci ) { if (neco.getNazev().equals(nazevVeci)) { return true; } } return false; } public Vec vyberVec(String nazevVeci) { Vec vybranaVec = null; for ( Vec neco : seznamVeci ) { if (neco.getNazev().equals(nazevVeci)) { vybranaVec=neco; } } if (vybranaVec != null) { if (vybranaVec.jePrenositelna()) { seznamVeci.remove(vybranaVec); } else { vybranaVec=null; } } return vybranaVec; }
Všimněte si, že metoda vyberVec() vrací hodnotu null ve dvou situacích: ♦ v místnosti příslušná věc neexistuje, ♦ věc v místnosti existuje, ale není přenositelná. Testování přenositelnosti přímo v této metodě je znakem dobrého zapouzdření objektu.
Projekt Adventura
strana 208
Metoda vyberVec() při úspěšném vybrání věci z místnosti tuto věc v seznamu věcí v místnosti zruší. Vypsání věci v místnosti znamená rozšíření metody dlouhyVypis() a vytvoření privátní metody seznamVeci(), která vytvoří řetězec obsahující jména věcí v místnosti: private String seznamVeci() { String seznam = ""; for ( Vec neco : seznamVeci ) { seznam = seznam + neco.getNazev()+" "; } return seznam; } public String dlouhyPopis() { return "Jsi v mistnosti/prostoru " + popis + ".\n" + seznamVychodu() + "\n" + "Veci: " + seznamVeci(); }
Inicializace věcí v místnostech Inicializaci věcí je vhodné umístit do stejného místa, kde se inicializují místnosti – do metody zalozMistnosti() ve třídě Hra. V následujícím kódu se vytvářejí tři věci a vkládají se do místností: // vkládání věcí do místností Vec hamburger = new Vec("hamburger",true); bufet.vlozVec(hamburger); bufet.vlozVec(new Vec("stul",false)); kancelar.vlozVec(new Vec("sponka",true));
V případě hamburgeru je deklarována proměnná hamburger, vytvořena instance a poté vložena do místnosti, na kterou odkazuje proměnná bufet. V případě ostatních věcí je vše v jednom řádku. Druhý způsob se používá častěji, první varianta (s hamburgerem) se používá, pokud se dále potřebujeme odkazovat na tuto instanci. V situaci, kdy k věci potřebujeme přistupovat i z dalších metod, by se použil datový atribut s modifikátorem private. Pozor! Uvedený kód musí být zařazen až za vytvoření instancí místnosti – nejdříve je potřeba vytvořit místnosti a teprve poté do nich umísťovat věci. Doplnění příkazu seber Doplnění příkazu se skládá ze tří částí: ♦ Doplnění příkazu do seznamu platných příkazů ve třídě SeznamPrikazu. Toto je jednoduché, v konstruktoru třídy SeznamPrikazu stačí doplnit další příkaz. ♦ Doplnění rozskoku v metodě zpracujPrikaz() ve třídě Hra. V metodě zpracujPrikaz() se na základě zadaného příkazu volá příslušná metoda. Vlastní doplnění je také triviální. ♦ Doplnění metody seber() do třídy Hra. Struktura metody je podobná, jako u metody jdi(): * metoda vrací textový řetězec, který se má zobrazit uživateli na obrazovce, * na začátku se zjišťuje, zda uživatel zadal jméno věci, která se má sebrat v místnosti, * pokud je věc v místnosti (volání aktualniMistnost.obsahujeVec()), zkusí se sebrat věc z místnosti. V případě, že věc není přenositelná, nepodaří se ji sebrat a metoda vrátí příslušné chybové hlášení. Pokud se podaří sebrat věc, měla by se uložit do batohu – tato část kódu chybí, doplní se při programování batohu.
Projekt Adventura
strana 209
/** * Příkaz "seber". Zkouší se sebrat věc z místnosti a uložit do * batohu. Pokud věc v místnosti je a je přenositelná, uloží se * do batohu. *@param prikaz příkaz, jako druhý parametr obsahuje jméno * věci, která se má sebrat. */ private String seber(Prikaz prikaz) { if (!prikaz.maDruheSlovo()) { return "Co mám sebrat? Musíš zadat jméno věci"; }
}
String nazevVeci = prikaz.getDruheSlovo(); if (aktualniMistnost.obsahujeVec(nazevVeci)) { Vec pozadovanaVec = aktualniMistnost.vyberVec(nazevVeci); if (pozadovanaVec == null) { return nazevVeci+" se nedá přenášet"; } else { // DOPROGRAMOVAT ULOZENI VECI DO BATOHU return nazevVeci+" jsi vzal z místnosti a " + "uložil do batohu "; } } else { return nazevVeci + " není v místnosti"; }
19.10. Domácí úkol 1. Nakreslete sekvenční diagram komunikace mezi instancemi tříd Hra, Prikaz a SeznamPrikazu při volání metod hraj() a zpracujPrikaz() v instanci třídy Hra. 2. Nakreslete sekvenční diagram komunikace mezi instancemi při provádění metody jdi() ve třídě Hra. 3. Upravte hru tak, aby vracela nápovědu pro jednotlivé příkazy, tj. aby se po napsání příkazu napoveda jdi vypsala nápověda k příkazu jdi. Texty, které se budou vypisovat k jednotlivým příkazům, by měly být ve třídě SeznamPrikazu – místo množiny Set pro uložení seznamu příkazů použijte implementaci rozhraní Map – jako klíč uložte příkaz, jako hodnotu vlastní text nápovědy. Bude potřeba též do této třídy doplnit metodu pro získání nápovědy. 4. Zkuste upravit hru tak, aby bylo možné používat skloňování v příkazech, např. „jdi do chodby“, „seber sponku“. Názvy místností a věcí by se měly vypisovat v prvním pádě. Pro řešení tohoto zadání je potřeba ukládat do instancí tříd Mistnost a Vec více názvů – v 1. a ve 4. pádě.
Projekt Adventura
strana 210
5. Napište třídu, která bude simulovat vstupy od uživatele. Tato třída nahradí třídu RizeniHry, po spuštění metody by se měla vypsat komunikace hry, jako kdyby vstup vkládal uživatel. Ve třídě bude uložena např. následující posloupnost příkazů: jdi chodba napoveda jdi kancelar konec
Projekt Domácí mediotéka
strana 211
20.Projekt Domácí mediotéka 20.1. Základní popis, zadání úkolu V projektu Domácí mediotéka (Dome) se jednoduchým způsobem evidují CD a videa. Projekt je velmi jednoduchý (tj. v praxi nepoužitelný), bude použit pro vysvětlení dědičnosti a polymorfismu. V tomto projektu budeme řešit tyto úkoly: ♦ omezit duplicity ve třídách Video a CD – vytvoří se předek, který bude obsahovat společné datové atributy a metody, ♦ omezit duplicity ve třídě Evidence – pouze jeden seznam, který bude obsahovat videa i CD, ♦ doplnit evidenci o knihy, ♦ použití rozhraní (interface), které budou implementovat třídy Video a CD. Tento projekt má následující cíle: ♦ ukázat základní použití dědičnosti a polymorfismu.
20.2. Struktura tříd Projekt Dome se na začátku skládá z následujících tříd (obrázek z BlueJ):
Obrázek 20.1 Diagram tříd projektu Dome převzatý z BlueJ Instance třídy Evidence obsahuje seznamy evidovaných videí a CD, má metody pro přidání videa, pro přidání CD a pro vypsání seznamu evidovaných videí a CD. O každém videu se eviduje titul, režisér, délka videa v minutách. O každém CD se eviduje titul, autor (umělec), počet skladeb, délka v minutách. U obou typů lze ještě zadávat komentář (metody setKomentar() a getKomentar()) a údaj o vlastnictví konkrétního titulu (metody setVlastnim() a getVlastnim()).
Projekt Domácí mediotéka
Obrázek 20.2 Diagram tříd projektu Dome včetně datových atributů a metod
20.3. Výpis kódu třídy Evidence 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
import java.util.ArrayList; import java.util.List; /** * Třída slouží pro evidenci CD a videa. Seznam všech uložených * CD a videí může být vypsán do textového okna. * Tato verze neukládá data na disk ani neposkytuje funkce pro * vyhledání nějakého titulu. * * @author Michael Kolling and David J. Barnes * @author Luboš Pavlíček * @version 2005-jul-31 */ public class Evidence { private List cds; private List