Programovanie (1) v C/C++
1-INF-127, ZS 2024/25
2018/19 Programovanie (2) v Jave
- Letný semester, úvodné informácie
- Letný semester, pravidlá
- Letný semester, test a skúška
- GraphGUI (knižnica ku skúške)
- Letný semester, projekt
Týždeň 18.-24.2. Úvod do Javy |
Prednáška 24 · Cvičenia 13 · DÚ5 |
Týždeň 25.2.-3.3. Úvod do objektovo-orientovaného programovania, JavaDoc |
Prednáška 25 · Cvičenia 14 · DÚ6 |
Týždeň 4.-10.3. Dedenie, polymorfizmus, modifikátory, rozhrania |
Prednáška 26 · Cvičenia 15 |
Týždeň 11.-17.3. Výnimky, generické programovanie |
Prednáška 27 · Cvičenia 16 · DÚ7 |
Týždeň 18.-24.3. Collections, anonymné triedy, lambda výrazy |
Prednáška 28 · Cvičenia 17 |
Týždeň 25.-30.3. Testovanie, úvod k JavaFX (cvičenie nebude) |
Prednáška 29 · DÚ8 |
Týždeň 1.-7.4. JavaFX – grafický návrh aplikácie, programovanie riadené udalosťami |
Prednáška 30 · Cvičenia 18 |
Týždeň 8.-14.4. JavaFX – zložitejšie ovládacie prvky |
Prednáška 31 · Cvičenia 19 · DÚ9 |
Týždeň 15.-21.4. Záver JavaFX, reprezentácia grafov, prehľadávanie do hĺbky |
Prednáška 32 · Cvičenia 20 |
Týždeň 22.-28.4. (v pondelok Veľká noc, cvičenia v stredu budú) |
Cvičenia 21 |
Týždeň 29.4.-5.5. Prehľadávanie do šírky, ohodnotené grafy, najdlhšia cesta |
Prednáška 33 · DÚ10 |
Týždeň 6.-12.5. Informácie ku skúške, knižnica na skúšku, orientované grafy, topologické triedenie |
Prednáška 34 |
Týždeň 13.-19.5. Maximálna klika, zhrnutie, OOP v C++ |
Prednáška 35 · Cvičenia 22 |
Obsah
- 1 Letný semester, úvodné informácie
- 2 Letný semester, pravidlá
- 3 Letný semester, test a skúška
- 4 GraphGUI
- 5 Letný semester, projekt
- 6 Prednáška 24
- 6.1 Úvod do predmetu
- 6.2 Hello world
- 6.3 Väčší program
- 6.4 Základy jazyka Java
- 7 Cvičenia 13
- 8 Prednáška 25
- 9 Cvičenia 14
- 10 Prednáška 26
- 10.1 Oznamy
- 10.2 Opakovanie: triedy a objekty
- 10.3 Konvencie pomenúvania identifikátorov
- 10.4 Dedenie
- 10.5 Prekrývanie metód a polymorfizmus
- 10.6 Abstraktné triedy a metódy
- 10.7 Hierarchia tried a trieda Object
- 10.8 Rozhrania
- 10.9 Prehľad niektorých modifikátorov tried, premenných a metód
- 10.10 Aritmetický strom s využitím dedenia
- 10.11 Odkazy
- 11 Cvičenia 15
- 12 Prednáška 27
- 13 Cvičenia 16
- 14 Prednáška 28
- 14.1 Oznamy
- 14.2 Opakovanie: generické programovanie
- 14.3 Úvod do Java Collections
- 14.4 Prechádzanie cez prvky Collection
- 14.5 Použitie Map
- 14.6 Dôležité metódy z uložených objektov
- 14.7 Algoritmy
- 14.8 Collections: Zhrnutie
- 14.9 Vnorené a anonymné triedy
- 14.10 Metóda forEach v Iterable, lambda výrazy
- 15 Cvičenia 17
- 16 Prednáška 29
- 17 Prednáška 30
- 17.1 Oznamy
- 17.2 Grafický návrh scény: jednoduchá kalkulačka
- 17.3 Programovanie riadené udalosťami
- 17.4 Časovač: pohybujúci sa kruh
- 18 Cvičenia 18
- 19 Prednáška 31
- 19.1 Oznamy
- 19.2 Zložitejšie ovládacie prvky a aplikácie s viacerými oknami: jednoduchý textový editor
- 19.2.1 Základ aplikácie
- 19.2.2 Ovládací prvok TextArea
- 19.2.3 Vlastnosti a spracovanie ich zmeny
- 19.2.4 Hlavné ponuky (MenuItem, Menu a MenuBar)
- 19.2.5 Kontextové ponuky (ContextMenu)
- 19.2.6 Priradenie udalostí k jednotlivým položkám ponúk
- 19.2.7 Previazanie vlastností
- 19.2.8 Jednoduché dialógy (Alert)
- 19.2.9 Ďalšie typy jednoduchých dialógov
- 19.2.10 Zatvorenie hlavného okna aplikácie „krížikom”
- 19.2.11 Otvárací a ukladací dialóg (FileChooser)
- 19.2.12 Vlastné dialógy (aplikácie s viacerými oknami)
- 19.2.13 Ovládacie prvky ListView a RadioButton
- 19.2.14 Skupiny RadioButton-ov (ToggleGroup)
- 19.2.15 Dokončenie dialógu na výber fontu
- 19.2.16 Cvičenia
- 19.2.17 Textový editor: kompletný zdrojový kód
- 20 Cvičenia 19
- 21 Prednáška 32
- 21.1 Oznamy
- 21.2 Viacoknové aplikácie v JavaFX: jednoduchý príklad
- 21.3 Grafy: úvod
- 21.3.1 Orientované a neorientované grafy
- 21.3.2 Vybrané aplikácie grafov
- 21.3.3 Reprezentácia grafov
- 21.3.4 Graf ako abstraktný dátový typ: rozhranie Graph
- 21.3.5 Orientované grafy pomocou zoznamov susedov: trieda AdjListsGraph
- 21.3.6 Orientované grafy pomocou matice susednosti: trieda AdjMatrixGraph
- 21.3.7 Neorientované grafy: triedy AdjListsUndirectedGraph a AdjMatrixUndirectedGraph
- 21.3.8 Vytvorenie grafu
- 21.3.9 Porovnanie reprezentácií grafov
- 21.3.10 Ďalšie varianty grafov
- 21.4 Prehľadávanie neorientovaného grafu do hĺbky
- 22 Cvičenia 20
- 23 Cvičenia 21
- 24 Prednáška 33
- 25 Prednáška 34
- 26 Prednáška 35
- 27 Cvičenia 22
Letný semester, úvodné informácie
Predmet 1-INF-166 Programovanie (2) v Jave nadväzuje na predmet 1-INF-127 Programovanie (1) v C/C++. Oba sú určené študentom prvého ročníka bakalárskych študijných programov Informatika a Bioinformatika.
Rozvrh
- Prednášky: pondelok 9:00-10:30 v F1-247
- Cvičenia: streda 8:10-9:40 v I-H6
Vyučujúci
- doc. Mgr. Broňa Brejová, PhD., miestnosť M-163,
- RNDr. Peter Kostolányi, PhD., miestnosť M-227,
- Konzultácie po dohode e-mailom
Kontaktný e-mail
- Ak nemáte otázku na konkrétnu osobu, odporúčame vyučujúcich kontaktovať pomocou spoločnej adresy e-mailovej adresy .
- Odpovie vám ten z nás, kto má na starosti príslušnú otázku alebo kto má práve čas.
Ciele predmetu
- prehĺbiť a rozšíriť zručnosti v algoritmickom uvažovaní, písaní a ladení programov z predchádzajúceho semestra
- oboznámiť sa so základnými programovými a dátovými štruktúrami jazyka Java
- zvládnuť základy objektovo-orientovaného programovania a tvorby programov s grafickým užívateľským rozhraním
- oboznámiť sa so základnými algoritmami na prácu s grafmi
Literatúra
- Predmet sa nebude striktne riadiť žiadnou učebnicou. Prehľad preberaných tém a stručné poznámky nájdete na stránke predmetu, doporučujeme Vám si na prednáškach a cvičeniach robiť vlastné poznámky.
- Pri štúdiu Vám môžu pomôcť knihy o jazyku Java, o programovaní všeobecne a o algoritmoch preberaných na prednáške. Tu je výber z vhodných titulov, ktoré sú k dispozícii na prezenčné štúdium vo fakultnej knižnici:
- Bruce Eckel: Thinking in Java. 4th edition Prentice-Hall, 2006. Signatúra I-INF-E-2
- Staršie 2. vydanie v českom preklade v knižnici so signatúrou D-INF-E-1b, 3. vydanie v angličtine online, doporučujeme však radšej 4. vydanie
- Michael T. Goodrich, Roberto Tamassia: Data Structures and Algorithms in Java. John Wiley & Sons, 2006. Signatúra D-INF-G-5
- Robert Sedgewick: Algorithms in Java, Parts 1-4, Fundamentals, Data Structures, Sorting, Searching, Addison-Wesley, 2003. Signatúra D-INF-S-1/I-IVa
- Robert Sedgewick: Algorithms in Java, Part 5, Graph Algorithms Addison-Wesley, 2004. Signatúra D-INF-S-1/V (I)
- Bruce Eckel: Thinking in Java. 4th edition Prentice-Hall, 2006. Signatúra I-INF-E-2
- Tutoriál k jazyku Java a referenčná príručka k štandardným knižniciam
Priebeh semestra
- Na prednáškach budeme preberať obsah predmetu. Prednášky budú dve vyučovacie hodiny do týždňa.
- Cvičenia budú dve vyučovacie hodiny do týždňa v počítačovej učebni a ich cieľom je aktívne si precvičiť učivo. Niektoré cvičenia budú začínať rozcvičkou, ktorú budete riešiť na počítači a odovzdávať na testovači. Ďalšou časťou cvičenia je precvičovanie príkladov k predchádzajúcim prednáškam.
- Domáce úlohy navrhujeme tak, aby Vám ich riešenie pomohlo osvojiť si a precvičiť si učivo, čím sa okrem iného pripravujete aj na záverečnú skúšku. Okrem tohto sú za domáce úlohy body do záverečného hodnotenia. Najviac sa naučíte, ak sa Vám domácu úlohu podarí samostatne vyriešiť, ale ak sa vám to napriek vášmu úsiliu nedarí, neváhajte sa spýtať o pomoc vyučujúcich. Možno s malou radou od nás sa Vám podarí úlohu spraviť. Treba však na domácej úlohe začať pracovať v predstihu, aby ste nás v prípade problémov stihli kontaktovať.
- Cieľom vyučujúcich tohto predmetu je vás čo najviac naučiť, ale musíte aj vy byť aktívni partneri. Ak Vám na prednáške alebo cvičení nie je niečo jasné, spýtajte sa. Môžete nám klásť tiež otázky počas našich konzultačných hodín alebo emailom. Ak sa dostanete do väčších problémov s plnením študijných povinností, poraďte sa s vyučujúcimi alebo s tútorom, ako tieto problémy riešiť.
- 40% známky dostávate za prácu cez semester, preto netreba nechávať štúdium učebnej látky až na skúškové obdobie.
- Pozor, pravidlá sa líšia od zimného semestra. Očakávame, že budete riešiť príklady z cvičení, aj keď nie sú bodované. Na rozcvičke by ste už mali mať príslušné učivo zvládnuté. Domácich úloh bude o niečo viac ako v zimnom semestri. Naopak testy budú iba dva.
Letný semester, pravidlá
Známkovanie
- 20% známky je na základe rozcvičiek, ktoré sa píšu na niektorých cvičeniach
- 20% známky je za domáce úlohy
- 30% známky je za písomné testy
- 30% známky je za praktickú skúšku pri počítači
- 10% bonusových percent je za nepovinný projekt
Pozor, body získavané za jednotlivé príklady nezodpovedajú priamo percentám záverečnej známky. Body za každú formu známkovania sa preváhujú tak, aby maximálny získateľný počet zodpovedal váham uvedených vyššie. Úlohy označené ako bonusové sa nerátajú do maximálneho počtu získateľných bodov v danej aktivite.
Stupnica
- Na úspešné absolvovanie predmetu je potrebné splniť nasledovné tri podmienky:
- Získať aspoň 50% bodov v celkovom hodnotení
- Získať aspoň 50% z z celkového súčtu písomiek
- Získať aspoň 50% zo skúšky
- Ak niektorú z týchto troch podmienok nesplníte, dostávate známku Fx.
- V prípade úspešného absolvovania predmetu získate známku podľa percent v celkovom hodnotení takto:
- A: 90% a viac, B:80...89%, C: 70...79%, D: 60...69%, E: 50...59%
Cvičenia a rozcvičky
- Rozcvičky budú na niektorých cvičeniach. Začiatok rozcvičky bude na začiatku cvičenia, čas do termínu odovzdania bude spravidla v rozsahu 30-90 minút. To, na ktorých cvičeniach budú rozcvičky, budeme priebežne oznamovať na prednáškach resp. na stránke predmetu. Očakávame rozcvičku približne každé dva týždne.
- Riešenia rozcvičky odovzdávajte na testovači. Pri bodovaní vezmeme do úvahy výsledky testovača, budeme však pozerať aj na ďalšie aspekty vášho riešenia (správnosť, dodržanie zadania, štýl). Nedokončené riešenia môžu dostať čiastočné body.
- Rozcvičku je potrebné riešiť individuálne.
- Počas rozcvičky je potrebná prítomnosť na cvičení, t.j. v počítačovej učebni.
- Pri rozcvičke môžete hľadať informácie na stránkach predmetu, v dokumentácii k jazyku, prípadne aj v ďalších existujúcich internetových alebo papierových zdrojoch týkajúcich sa všeobecne programovaniu v jazyku Java. Je však zakázané počas rozcvičky komunikovať s ďalšími osobami okrem vyučujúcich, či už osobne alebo elektronicky. Tiež je zakázané zdieľať svoje riešenia s inými osobami alebo cielene vyhľadávať existujúce riešenia zadanej úlohy.
Nerozcvičkové príklady
- Okrem rozcvičiek budú na cvičeniach zverejnené aj ďalšie príklady na precvičenie učiva. Rozcvička bude väčšinou z učiva, ktoré sa už precvičovalo na niektorom z predchádzajúcich cvičení.
- Niektoré nerozcvičkové príklady môžu byť na testovači za malý počet bonusových bodov (pripočítajú sa k bodom z rozcvičiek). Tieto môžete riešiť a odovzdávať aj vo dvojiciach.
- Ďalšie príklady sú nebodované, neodovzdávajú sa na testovač. Vaše riešenie si musíte otestovať sami, prípadne sa spýtajte cvičiacich, ak máte otázky.
Domáce úlohy
- Domáce úlohy budú vypisované v priemere raz za dva týždne. Maximálny počet bodov za domácu úlohu bude uvedený v zadaní a bude spravidla 10-20 bodov podľa náročnosti úlohy.
- Domáce úlohy treba odovzdať na testovači do termínu určeného v zadaní. Neskoršie odovzdané úlohy nebudú akceptované.
- Program, ktorý odovzdáte ako domácu úlohu by mal byť skompilovateľný a spustiteľný na testovači. Budeme kontrolovať správnosť celkovej myšlienky, správnosť implementácie, ale aj štýl.
- Programy, ktoré nebudú správne bežať na testovacích vstupoch, nezískajú plný počet bodov, dajte preto pozor na všetky pokyny uvedené v zadaní (presný formát vstupu a výstupu, mená súborov a podobne).
Nepovinný projekt
- Za nepovinný projekt môžete získať 10% bonus k vašej výslednej známke (musíte však stále splniť všetky tri podmienky ukončenia predmetu).
- Projekt robia dvojice, výnimočne aj jednotlivci.
- Projekt sa bude odovzdávať v prvom týždni skúškového obdobia.
- Témy projektov a podrobnejšie pravidlá nájdete na zvláštnej stránke.
- Na získanie bodov za projekt musí byť vaša práca dostatočne rozsiahla a kvalitná, v opačnom prípade získate 0 bodov.
Písomné testy
- Prvý test sa bude približne v strede semestra, druhý na konci semestra alebo začiatku skúškového obdobia.
- Dĺžka testu bude približne 60 minút.
- Pri teste nemôžete používať žiadne pomocné materiály (písomné ani elektronické) okrem povoleného ťaháku v rozsahu jedného listu formátu A4 s ľubovoľným obsahom na oboch stranách.
- Predbežné termíny:
- Prvý test streda 3.4. o 18:10
Skúška
- Na skúške budete riešiť 2 úlohy pri počítači v celkovom trvaní 2-3 hodiny.
- Na skúške nemôžete používať žiadne pomocné materiály okrem povoleného ťaháku v rozsahu jedného listu formátu A4 s ľubovoľným obsahom na oboch stranách. Nebude k dispozícii ani internet. Budete používať rovnaké programátorské prostredie ako na cvičeniach.
- Po skončení skúšky sa koná krátky ústny pohovor s vyučujúcimi, počas ktorého sa prediskutujú programy, ktoré ste odovzdali a uzavrie sa vaša známka.
Opravné termíny
- Každý test má jeden opravný termín (je súčasťou priebežného hodnotenia)
- Ak sa zúčastníte opravného termínu, strácate body z predchádzajúceho termínu, aj keby ste na opravnom získali menej bodov.
- Opakovanie skúšky sa riadi študijným poriadkom fakulty. Máte nárok na dva opravné termíny (ale len v rámci termínov, ktoré sme určili).
- Ak po skúške pri počítači máte nárok na známu E alebo lepšiu, ale chceli by ste si známku ešte opraviť, musíte sa dohodnúť so skúšajúcimi pred zapísaním známky do indexu.
- Ak po skúške pri počítači ešte opravujete písomku, je potrebné prísť uzavrieť a zapísať známku v termíne určenom vyučujúcimi.
- Ak sa zo závažných dôvodov (napr. zdravotných, alebo konflikt s inou skúškou) nemôžete zúčastniť termínu skúšky alebo písomky, dajte o tom vyučujúcim vedieť čím skôr.
Opisovanie
- Máte povolené sa so spolužiakmi a ďalšími osobami rozprávať o domácich úlohách a stratégiách na ich riešenie. Kód, ktorý odovzdáte, musí však byť vaša samostatná práca. Je zakázané opisovať kód z literatúry alebo z internetu a ukazovať svoj kód spolužiakom. Domáce úlohy môžu byť kontrolované softvérom na detekciu plagiarizmu.
- Počas rozcvičiek, testov a skúšok môžete používať iba povolené pomôcky a nesmiete komunikovať s žiadnymi osobami okrem vyučujúcich.
- Ak nájdeme prípady opisovania alebo nepovolených pomôcok, všetci zúčastnení študenti získajú za príslušnú domácu úlohu alebo test nula bodov (t.j. aj tí, ktorí dali spolužiakom odpísať). Navyše budú prípady opisovania podstúpené na riešenie disciplinárnej komisii fakulty.
Neprítomnosť
- Účasť na cvičeniach veľmi silne doporučujeme a v prípade neprítomnosti stratíte body za rozcvičky.
- V prípade ochorenia alebo iných závažných dôvodov neprítomnosti poraďte s vyučujúcimi o možných riešeniach. Treba tak spraviť čím skôr, nie až spätne cez skúškové. Môžeme vyžadovať potvrdenku od lekára.
Test pre pokročilých
- V druhom týždni semestra sa bude konať nepovinný test pre pokročilých.
- Ak na test prídete a napíšete ho na menej ako 50%, nezískate žiadne výhody (ako keby ste na test ani neprišli).
- V opačnom prípade získate plný počet bodov za k=(x+10)/20 rozcvičiek, kde x je percento bodov z testu a delenie je celočíselné. Napríklad za 50-69% z testu dostanete plný počet bodov z 3 rozcvičiek. Body vám budú započítané za prvých k rozcvičiek a nie je možné ich presúvať na iné termíny rozcvičiek.
- Navyše si môžete body z testu pre pokročilých nechať uznať ako body z písomných testov. Máte však aj možnosť písať testy so spolužiakmi (buď budete písať obidva alebo ani jeden)
Letný semester, test a skúška
Na túto stránku budeme postupne pridávať informácie týkajúce sa druhého testu a praktickej skúšky pri počítači v letnom semestri. Odporúčame tiež si preštudovať pravidlá predmetu.
Termíny, zapisovanie
Termíny skúšok
- Štvrtok 23.5. 9:00 v H6 (riadny)
- Utorok 4.6. 9:00 v H6 (riadny alebo 1. opravný)
- V strede júna 1. alebo 2. opravný, dátum upresníme neskôr
- Koncom júna 2. opravný termín, dátum upresníme neskôr
Termíny druhého testu
- 20. mája o 16:30 v posluchárni A
- Počas skúškového bude opravný termín, dohodneme po teste (pravdepodobne druhý týžden skúškového)
Prípadné konflikty s dátumami písomky alebo skúšok nám dajte vedieť čím skôr
K zapisovaniu na skúšky
- Na termín skúšky sa zapisujte v systéme AIS.
- Prihlasovanie/odhlasovanie na skúšku najneskôr 24 hodín pred skúškou
- Ak by ste sa potrebovali prihlásiť / odhlásiť neskôr, pošlite email na prog@... adresu
- Na skúšku môžete ísť, aj keď ešte nemáte úspešne absolvovaný test (ale kým nespravíte test, nezapíšeme vám známku).
- Celkovo budú štyri termíny, každý sa môže zúčastniť na najviac troch z nich, ďalšie termíny neplánujeme pridávať
Skúška pri počítači
- Doneste si ťahák 1 list A4, ISIC, index, písacie potreby
- Príďte pár minút pred začiatkom termínu pred príslušnú učebňu, kde sa dozviete pokyny a rozsadenie do miestností
- Skúška 2 a 3/4 hodiny práca pri počítačoch.
- Prostredie ako minulý semester (Linux v učebniach, skúškové konto, nebude internet)
- Aby ste mali šancu úspešne ukončiť predmet, musíte získať aspoň polovicu bodov.
- Hodnotíme správnosť myšlienky, správnosť implementácie, v menšej miere štýl
- Nezáleží na rýchlosti programu, pokiaľ v rozumnom čase zbehne aspoň na malých vstupoch
- Testovač nebude testovať správnosť, dostatočne si program otestujte sami.
Na skúške pri počítači dostanete k dispozícii program GraphGUI
- Pozrite si ho vopred pred skúškou
- Do programu budete pridávať riešenia dvoch úloh
- Tieto úlohy bude možné riešiť nezávisle na sebe a budú mať rovnakú váhu, nemusia však byť pre vás rovnako ťažké
- V úlohe A bude treba doprogramovať niečo do grafického prostredia v JavaFX
- V úlohe B bude treba naprogramovať nejaký grafový algoritmus metódou prehľadávania s návratom
- Odovzdávate iba súbory Editor.java (úloha A) a GraphAlgorithm.java (úloha B), celé vaše riešenie by teda malo byť v týchto súboroch.
- Ak kvôli ladeniu meníte iné časti programu, ubezpečte sa, že odovzdané súbory pracujú aj s pôvodnou verziou programu GraphGUI.
- Ak sa bude verzia GraphGUI na skúške líšiť od tej vopred zverejnenej, zmeny budú popísané v zadaní a nebudú veľmi rozsiahle (napr. pridanie zopár užitočných metód)
Na skúške budete mať prístup aj k dokumentácii k Jave, ktorá je stiahnuteľná tu, resp. prezerateľná tu a tu.
- Nebude možné v dokumentácii vyhľadávať, naučte sa v nej preto pred skúškou orientovať navigáciou z farebnej tabuľky na úvodnej stránke.
- Táto dokomentácia tiež neobsahuje tutoriály, len popisy jednotlivých tried a ich metód
Poobede po skúške vyhodnotenie u prednášajúcich, zapisovanie známok
- Príďte, aj ak ste skúšku nespravili, môžeme vám poradiť, čo robiť inak na opravnom termíne.
- Ak z vážnych dôvodov nemôžete poobede prísť, dohodnite si stretnutie v inom čase (už pred skúškou alebo hneď po nej)
Ukážkový príklad, úloha A
Do súboru Editor.java doprogramujte, aby sa po stlačení tlačidla Run Editor otvorilo editovacie okienko, v ktorom je pre každý vrchol jeden ovládací prvok s číslom vrcholu a používateľ môže pre každý vrchol nastaviť, aby sa jeho farba zmenila na zelenú. Ovládacie prvky majú byť umiestnené pod sebou a na spodku bude ovládací prvok Button s nápisom OK, ktorý po stlačení označené vrcholy prefarbí. Ak bude okno zavreté bez stlačenia OK, zmeny sa nevykonajú. Pomôcky:
- Na zmenu farby použite metódu setColorName("green") rozhrania Vertex.
- Ako vhodný Layout aplikácie odporúčame GridPane
- Odporúčané ovládacie prvky sú RadioButton (zaujímavé metódy setSelected, isSelected), pripadne ListView (vhodný SelectionModel je SelectionMode.MULTIPLE a jeho metódy getSelectedIndices() resp. getSelectedItems()).
Ukážkový príklad, úloha B
Do súboru GraphAlgorithm.java naprogramujte hľadanie najmenšej dominujúcej množiny v grafe pomocou prehľadávania s návratom.
- Dominujúca množina je taká množina vrcholov X, že každý vrchol grafu je buď v X, alebo susedí s nejakým vrcholom v X.
- Napríklad hrany sú rovné chodby a vrcholy križovatky. Strážnik stojaci vo vrchole má pod kontrolou túto križovatku aj všetky s ňou susedné. Chceme s použitím čo najmenšieho počtu strážnikov mať pod kontrolou všetky vrcholy grafu.
- Vrcholy patriace do nájdenej dominujúcej množiny prefarbite na zeleno, ostatné vrcholy prefarbite na bielo. Ako výsledok metódy performAlgorithm vráťte počet vrcholov najmenšej dominujúcej množiny prevedený na reťazec bez ďalšieho sprievodného textu.
Druhý test
- Trvanie 60 minút
- Dobre si rozvrhnite čas, niektoré úlohy sú ťažšie, iné ľahšie.
- Aby ste mali šancu úspešne ukončiť predmet, musíte z oboch testov spolu získať aspoň polovicu bodov.
- Doneste si ISIC, písacie potreby, ťahák 1 list A4
- Zakázané sú ďalšie materiály, elektronické pomôcky, opisovanie
Na písomnom teste budú príklady na nasledujúce témy:
- Generické programovanie, základy Collections (ArrayList, LinkedList, HashMap, Iterator), anonymné triedy
- Testovacie vstupy (netreba JUnit, len tvorba vstupov ako takých)
- Grafy (reprezentácia, prehľadávanie grafu do hĺbky a šírky, topologické triedenie orientovaného grafu, úlohy na prehľadávanie s návratom)
- Potrebná je aj znalosť oblastí z prvého testu (OOP, dedenie, výnimky) a všeobecné programátorské zručnosti v jazyku Java (napr. práca s poliami, rekurzia)
Na písomnom teste nebudú príklady na JavaFX
Ukážkové príklady na druný test
V texte nižšie je niekoľko príkladov, ktoré sa svojim charakterom a obtiažnosťou podobajú na príklady, aké budú na záverečnej písomke. Tieto ukážkové príklady sú prevažne vybrané z cvičení a prednášok, na skutočnej písomke však budú nové, zatiaľ nepoužité príklady.
- Príklad 1: Naprogramujte generickú triedu Matrix, ktorá reprezentuje obdĺžnikovú maticu prvkov nejakého neznámeho typu E.
- Napíšte konštruktor, ktorý vytvorí maticu zadaných rozmerov a vyplní ju zadaným prvkom typu E.
- Napíšte metódu get, ktorá vráti prvok matice nachádzajúci sa na zadanom mieste
- Napíšte metódu set, ktorá na zadané miesto v matici zapíše zadaný prvok typu E
- Ak metódam get alebo set užívateľ zadá súradnice mimo matice, hodia výnimku vašej vlastnej triedy MatrixIndexOutOfBoundsException (ktorú tiež naprogramujte)
- Výnimka tejto triedy by v metóde getMessage mala vrátiť reťazec obsahujúci obidve súradnice, ako aj obidva rozmery matice.
- Príklad 2: Napíšte generickú statickú metódu prienik, ktorá dostane dve SortedSet (s tým istým typom prvkov E) a vráti SortedSet obsahujúcu ich prienik, t.j. prvky, ktoré sa nachádzajú v oboch. Funkciu môžete naprogramovať napríklad pomocou iterácie cez jednu vstupnú množinu a volaním contains na druhú. Príkladom triedy implementujúcej rozhranie SortedSet je TreeSet.
- Príklad 3: Uvažujme prehľadávanie grafu do hĺbky, ktoré do poľa whenVisited čísluje vrcholy v poradí, v akom boli navštívené. Odsimulujte algoritmus na grafe vpravo a zistite, v akom poradí budú vrcholy navštívené, ak začneme prehľadávať vo vrchole 0. Predpokladajte, že adjVertices vracia susedov v poradí podľa čísla vrcholu.
- Príklad 4: Odsimulujte prácu prehľadávania do šírky na grafe vyššie, ak začneme prehľadávať vo vrchole 0. Predpokladajte, že adjVertices vracia susedov v poradí podľa čísla vrcholu. Uveďte, v akom poradí boli vrcholy vložené do queue a vypíšte výsledné polia dist a prev.
- Príklad 5: Nájdite všetky topologické usporiadania orientovaného grafu na obrázku vpravo.
- Príklad 6: Napíšte funkciu static int countIsolated(Graph g), ktorá v grafe g spočíta počet izolovaných vrcholov, teda takých, ktoré nemajú žiadnych susedov. Graph je interface z prednášok s metódami getNumberOfVertices, getNumberOfEdges, addEdge, existsEdge, adjVertices.
- Príklad 7: Navrhnite 5 testov pre metódu s hlavičkou a špecifikáciou uvedenou nižšie. Pre každý test uveďte obsah vstupných parametrov a a x, ako aj aký výstup by metóda mala vrátiť a stručne slovne popíšte význam testu. Pokryte obvyklé aj okrajové prípady.
/** Z pola a vyhodi prvy vyskyt objektu rovneho x * pricom rovnost sa testuje metodou equals. * Vsetky dalsie prvky posunie o jedno dolava a na koniec * pola da null. * Vrati true, ak bolo pole modifikovane, inak false. * Ak a je null alebo x je null, vyhodi java.lang.NullPointerException */ public static boolean remove(Object[] a, Object x) {
GraphGUI
Program GraphGUI ku skúške
Na skúške pri počítači budete pracovať s programom GraphGUI. Tento program aj jeho dokumentáciou dostanete k dispozícii na skúške, odporúčame vám ale sa s ňou oboznámiť vopred.
- zip
- JavaDoc dokumentácia
- Ak objavíte chybu, dajte čím skôr vedieť B. Brejovej
Do programu graphgui budete pridávať riešenia dvoch úloh
- Tieto úlohy bude možné riešiť nezávisle na sebe a budú mať rovnakú váhu, nemusia však byť pre vás rovnako ťažké
- V úlohe A bude treba doprogramovať nové dialógové okno do grafického prostredia
- V úlohe B bude treba riešiť grafový problém metódou prehľadávania s návratom
- Odovzdávate iba súbory Editor.java (úloha A) a GraphAlgorithm.java (úloha B), celé vaše riešenie by teda malo byť v týchto súboroch.
- Ak kvôli ladeniu meníte iné časti programu, ubezpečte sa, že odovzdané súbory pracujú aj s pôvodnou verziou programu graphgui.
- Pokiaľ v programe pre daný termín skúšky spravíme zmeny oproti vopred zverejnenej verzii, vysvetlíme ich v zadaní. Veľká väčšina programu však bude identická so zverejnenou.
Používanie programu
Po spustení GraphGui sa objaví grafický panel na editovanie neorientovaného grafu
- Editor má tri módy: Add, Delete a Select
- V móde Add kliknutie na prázdne miesto pridá vrchol, postupné kliknutie na dva vrcholy ich spojí hranou
- V móde Delete môžete zmazať hranu alebo vrchol kliknutím na ne
- V móde Select môžete označiť hranu alebo vrchol kliknutím na ne, kliknutím na prázdnu plochu označenie zrušíte
- Označený vrchol má hrubší rámik, označená hrana je tiež hrubšia
- V pravom paneli sa vypisujú vlastnosti označeného vrcholu a hrany, dajú sa tam aj editovať
- V ľavom paneli sú okrem tlačidiel na výber módu aj tlačidlá na spustenie riešenia úlohy A a B
- Program je možné ovládať aj zadávaním textových príkazov do príkazového riadku v spodnej časti okna, zoznam príkazov je v položke Help
- Graf (vrátane vyznačenej hrany a vrchola) je možné ukladať do súboru a naopak otvárať zo súboru.
Dôležité súčasti knižnice
Graph, Vertex, Edge
- Rozhranie pre prácu s neorientovaným grafom.
- Umožňuje pridávať aj mazať vrcholy aj hrany.
- Vrcholy a hrany sú objekty typu Vertex a Edge.
- Vrcholy aj hrany si pamätajú hodnotu (value) typu int, ktorá môže predstavovať napr. vzdialenosť dvoch miest, počet obyvateľov mesta a pod.
- Vrcholy a hrany majú priradenú aj určitú farbu, ktorou sa vyfarbujú. Tato farba je uložená ako reťazec, napr. "white", "black", prípadne "#00ffff"
- Vrcholy sú číslované 0,...,N-1, kde N je počet vrcholov, a v algoritmoch je možné k nim pristupovať buď cez tieto čísla (id) alebo cez samotné objekty typu Vertex. Po zmazaní vrchola sa id ostatných vrcholov môžu zmeniť.
- Vrcholy majú uložené aj údaje o svojich súradniciach x a y, čo sú reálne čísla od 0 do 1.
- Vo vašich častiach programu pristupujte k grafu len pomocou metód z rozhraní Graph, Vertex a Edge.
interface Graph { int getNumberOfVertices(); int getNumberOfEdges(); Collection<Vertex> getVertices(); Collection<Edge> getEdges(); Vertex getVertex(int id); Collection<Vertex> adjVertices(Vertex vertex); Collection<Integer> adjVertexIds(int id); Collection<Edge> adjEdges(Vertex vertex); Collection<Edge> adjEdges(int id); Vertex addVertex(double x, double y); void removeVertex(Vertex vertex); Edge addEdge(Vertex vertex1, Vertex vertex2); Edge addEdge(int id1, int id2); void removeEdge(Edge edge); void removeEdge(Vertex vertex1, Vertex vertex2); void removeEdge(int id1, int id2); Edge findEdge(Vertex vertex1, Vertex vertex2); Edge findEdge(int id1, int id2); void clear(); void print(PrintStream out, boolean full); void read(Scanner scanner); void deselectEdge(); void deselectVertex(); void selectVertex(Vertex vertex); void selectEdge(Edge edge); Vertex getSelectedVertex(); Edge getSelectedEdge(); } interface Edge { Vertex getFirst(); // koncový vrchol s menším id int getFirstId(); Vertex getSecond(); // koncový vrchol s väčšším id int getSecondId(); boolean isIncident(Vertex vertex); boolean isIncident(int id); Vertex getOtherEnd(Vertex vertex); // pre jeden zadaný koniec hrany vráť druhý int getOtherEnd(int id); int getValue(); void setValue(int value); String getColorName(); void setColorName(String colorName); void setColorRgb(int red, int green, int blue); } interface Vertex { int getId(); Collection<Vertex> adjVertices(); Collection<Integer> adjVertexIds(); Collection<Edge> adjEdges(); Edge findEdge(Vertex vertex); int getValue(); void setValue(int value); double getX(); void setX(double x); double getY(); void setY(double y); String getColorName(); void setColorName(String colorName); void setColorRgb(int red, int green, int blue); }
GraphImplementation, VertexImplementation, EdgeImplementation
Pomocné triedy reprezentujúce graf a jeho súčasti
- Implementujú rozhrania popísané vyššie, notifikujú grafické prvky o zmenách v grafe.
- Vo vašich častiach programu nevolajte priamo metódy týchto tried, používajte len rozhrania uvedené vyššie.
GraphGUI, Controller, GraphPane, GraphDrawing, State, layout.fxml
Triedy implementujúce samotné grafické rozhranie. Tieto triedy nemáte na skúške meniť ani používať ich časti, takže sú tu uvedené len pre zaujímavosť.
- GraphGUI obsahuje metódu main, ktorá naštartuje aplikáciu
- Controller obsahuje funkčné prvky okrem samotnej plochy s grafom (tlačidlá, menu, príkazový riadok, editovanie nastavení vrchola a hrany)
- Súbor layout.fxml obsahuje rozloženie ovládacích prvkov na ploche a ich základné vlastnosti v FXML formáte
- GraphDrawing vykresľuje graf do plochy pomocou kruhov, čiar a textov, stará sa o nastavovanie súradníc, farby a pod. podľa toho ako sa graf mení
- GraphPane obsahuje funkčné časti práce s grafom - reakcie programu na klikanie do grafickej plochy
- State je pomocná trieda obsahujúca stav programu (Add/Delete/Edit) a samotný graf
GraphAlgorithm
Trieda, ktorá má obsahovať implementáciu požadovaného grafového algoritmu z úlohy B.
- Používajte iba metódy rozhraní Graph, Vertex, Edge.
- Nemeňte hlavičky public metódy ani konštruktora, môžete si však pridať do triedy ďalšie premenné, metódy, prípadne aj pomocné triedy.
Konštruktor dostane graf, ktorý si uloží.
- Podľa potreby pridajte inicializáciu vašich ďalších premenných.
Metóda performAlgorithm má vykonať samotný algoritmus
- podľa pokynov v zadaní môže modifikovať graf
- napr. často je úlohou nejako prefarbiť vrcholy alebo hrany
- výsledok výpočtu vráti ako String
- použite formát požadovaný v zadaní
Ukážkový príklad tejto triedy, ktorý nájde všetkých susedov označeného vrchola a preferbí ich na oranžovo. Vráti správu so zoznamom čísel týchto vrcholov.
- Na skúške budete mať riešiť nejaký problém metódou prehľadávania s návratom (backtracking). Na prednáške sme videli problém najdlhšej cesty, uvidíme ešte maximálnu kliku
public class GraphAlgorithm { //PREMENNÉ TRIEDY, UPRAVTE SI PODĽA POTREBY private final Graph graph; // KONŠTRUKTOR: NEMEŇTE HLAVIČKU, TELO UPRAVTE PODĽA POTREBY public GraphAlgorithm(Graph graph) { this.graph = graph; } // METÓDA performAlgorithm: NEMEŇTE HLAVIČKU, TELO UPRAVTE PODĽA POTREBY public String performAlgorithm() { Vertex selected = graph.getSelectedVertex(); // ak bol nejaky vrchol vybrany if (selected != null) { String result = ""; for (Vertex other : selected.adjVertices()) { other.setColorName("orange"); if (!result.isEmpty()) { result = result + " "; } result = result + other.getId(); } return result; } return "no vertex selected"; } }
Editor, EditorException
Trieda, ktorá má obsahovať implementáciu úlohy A na skúške.
- V tejto úlohe budete mať spraviť dialógové okno, v ktorom sa budú dať meniť určité aspekty grafu predpísaným spôsobom
- Úlohou teda môže byť určitým spôsobom pridávať alebo uberať vrcholy či hrany, nastavovať im súradnice, hodnoty alebo farby
- Konštruktor dostane odkaz na graf (typu Graph), môže si ho uložiť a prípadne inicializovať ďalšie premenné
- Potom program zavolá metódu edit, v ktorej sa má vykresliť dialógové okno. Keď užívateľ ukončí prácu s týmto oknom (spravidla stlačením gombíka Ok), vaša časť programu skontroluje správnosť zadaných údajov a ak sú správne, zmení graf príslušným spôsobom (volaním metód z rozhraní Graph, Vertex a Edge)
- Ak sú údaje nesprávne, upovedomí používateľa okienkom s chybovou hláškou (napr. použitím už hotovej metódy showError) a nechá používateľa ďalej meniť hodnoty
- Ak je to v zadaní špecifikované, váš kód môže za stanovených okolností vyhodiť výnimku typu EditorException
- Iné výnimky by v ňom vznikať nemali, resp. by mali byť odchytené ešte v rámci vášho kódu
V tomto príklade sa v prípade, že je označená hrana, zobrazí dialóg, ktorým sa dá meniť farba tejto hrany podľa jej red, green a blue zložiek.
- ak nie je označená hrana, metóda edit vyhodí výnimku
public class Editor { // POMOCNÁ TRIEDA PRE UKÁŽKOVÝ PRÍKLAD, MEŇTE PODĽA POTREBY class MyStage extends Stage { Edge edge; MyStage(Edge edge) { this.edge = edge; GridPane pan = new GridPane(); MyStage dialog = this; Color c = Color.web(edge.getColorName()); final TextField rText = new TextField((int)(c.getRed() * 255) + ""); final TextField gText = new TextField((int)(c.getGreen() * 255) + ""); final TextField bText = new TextField((int)(c.getBlue() * 255) + ""); final Label rLabel = new Label("Red (0-255): "); final Label gLabel = new Label("Green (0-255): "); final Label bLabel = new Label("Blue (0-255): "); Button ok = new Button("OK"); pan.add(rLabel, 0, 0); pan.add(rText, 1, 0); pan.add(gLabel, 0, 1); pan.add(gText, 1, 1); pan.add(bLabel, 0, 2); pan.add(bText, 1, 2); pan.add(ok, 1, 3); ok.setOnAction((ActionEvent event) -> { int r, g, b; try { r = Integer.parseInt(rText.getText()); g = Integer.parseInt(gText.getText()); b = Integer.parseInt(bText.getText()); } catch (Exception e) { showError("Bad color value (should be a number)"); return; } try { if (r >= 0 && g >= 0 && b >= 0 && r < 256 && g < 256 && b < 256) { edge.setColorRgb(r, g, b); dialog.close(); } else { showError("Bad color value (should be in range 0-255)"); } } catch (Exception e) { showError("Color could not be set"); } }); Scene sc = new Scene(pan); this.setScene(sc); } private void showError(String message) { Alert error = new Alert(AlertType.ERROR, message); error.setHeaderText(null); error.initOwner(this); error.showAndWait(); } } // PREMENNÉ TRIEDY, UPRAVTE SI PODĽA POTREBY private Graph graph; // KONŠTRUKTOR: NEMEŇTE HLAVIČKU, TELO UPRAVTE PODĽA POTREBY public Editor(Graph graph) { this.graph = graph; } // METÓDA edit: NEMEŇTE HLAVIČKU, TELO UPRAVTE PODĽA POTREBY public void edit() throws EditorException { Edge edge = graph.getSelectedEdge(); if (edge == null) { throw new EditorException("No edge selected"); } MyStage dialog = new MyStage(edge); dialog.initStyle(StageStyle.UTILITY); dialog.initModality(Modality.APPLICATION_MODAL); dialog.showAndWait(); } }
Letný semester, projekt
- Súčasťou bodovania je aj nepovinný projekt, za ktorý môžete dostať 10% bonus.
- Je to príležitosť vyskúšať si písanie väčšieho programu, pričom v projekte máte väčšiu voľnosť ako pri domácich úlohách
- Projekty, ktoré nebudú spĺňať podmienky uvedené nižšie, budú hodnotené 0 bodmi, nemá teda význam odovzdávať nedokončené programy
Požiadavky na projekt
- Projekt musí byť na jednu z tém nižšie. Témy však nepopisujú presné požiadavky, poskytujú len námety, z ktorých si môžete vybrať a prípadne ich ďalej rozšíriť.
- Projekt musí byť napísaný v Jave a spustiteľný v Netbeans v učebniach na fakulte a napísaný prehľadne s dostatkom komentárov.
- Používať môžete len štandardné javovské knižnice (a prípadne JUnit). Okrem toho samozrejme môžete používať kód poskytnutý v rámci predmetu. Zvyšok programu by mal byť z väčšej časti napísaný vami. Ak použijete nejaké úryvky kódu z internetu alebo iných zdrojov, v komentároch jasne uveďte zdroj.
- Program by mal poskytovať grafické užívateľské prostredie, mal by byť príjemne ovládateľný a mal by sa vyrovnať aj s neobvyklým správaním používateľa (nemal by padať na chyby, okrem naozaj závažných neobvyklých situácií ako nedostatok pamäte).
- Váš program by mal byť schopný načítavať spracovávané dáta zo súboru a po ich prípadnej zmene ich opäť do súboru uložiť. Formát súboru si môžete zvoliť, program by však nemal padať, ak dostane súbor v zlom formáte alebo s nezmyselnými dátami.
Projekt spĺňajúci tieto požiadavky získa aspoň 5% bonusových bodov, pričom je možné získať až 10% v závislosti od náročnosti a kvality vypracovania projektu.
Termíny a odovzdávanie
- V prípade, že chcete robiť projekt, treba si vybrať jednu z ponúkaných tém do štvrtka 2.5.2019 22:00
- Doporučujeme však toto rozhodnutie spraviť čím skôr, aby ste na projekt mali dosť času, koncom semestra býva veľa práce na všetkých predmetoch.
- Do uvedeného termínu odovzdajte na testovači súbor .txt, ktorý bude obsahovať, ktorú tému ste si vybrali a meno jedného alebo dvoch študentov, ktorí budú na tejto téme robiť. Stačí, ak sa na projekt prihlási jeden z členov dvojice.
- Samotný projekt je potrebné odovzdať klasicky ako domácu úlohu do utorka 21.5. 22:00 na testovači.
- Odovzdajte v jednom zazipovanom adresári:
- podadresár obsahujúci zdrojový kód a ďalšie potrebné súbory, napr. bitmapy,
- podadresár obsahujúci niekoľko príkladov vstupných súborov,
- stručný popis projektu vo formáte txt, v ktorom vymenujte, aké možnosti váš program používateľovi poskytuje.
- Projekt je tiež potrebné prísť predviesť vyučujúcim v dopredu oznámených termínoch (tiež pravdepodobne počas 1. týždňa skúškového obdobia).
Témy projektov
- Rozšírte DÚ5 (budova) o grafické rozhranie, ktoré zobrazí jednotlivé podlažia budovy, pričom voľné políčka, steny a výťahy zvýrazní rôznymi farbami. Používateľovi umožní plán budovy editovať a zisťovať dosiahnuteľnosť z rôznych štartovacích pozícií. Pri editovaní plánu sa pokúste ponúknuť paletu viacerých nástrojov uľahčujúcich prácu (napr. pridanie výťahu cez všetky poschodia, pridanie určitého množstva náhodne romiestnených stien a podobne).
- Rozšírte DÚ6 (Sudoku) o grafické rozhranie umožňujúce používateľovi hrať túto hru v rozšírenej verzii popísanej v zadaní. Vymyslite vhodný spôsob, ako graficky zobraziť skupiny políčok. Napríklad vedľa hernej plochy môže byť zoznam skupín a kliknutím na konkrétnu skupinu sa zmení pozadie políčok tejto skupiny. Do programu môžete zakomponovať aj algoritmus riešenie Sudoku z DÚ - program môže napríklad na požiadanie ako pomôcku ukázať správnu hodnotu vybraného políčka.
- Rozšírte DÚ7 (roboti) o grafické rozhranie, ktoré zobrazí hernú plochu a dovolí používateľovi vytvoriť alebo editovať postupnosť robotov aj s ich nastaveniami (čas príchodu, typ, inicializačný parameter) a potom simuluje roboty a vypisuje aktuálne štatistiky úspešných a mŕtvych robotov. Môžete prípadne naprogramovať aj editor hernej plochy. Môžete tiež do hry pridať ďalšie typy robotov alebo políčok.
- Rozšírte DÚ9 (hra) o ďalšie možnosti. Dorobte napríklad editor hracej plochy, ktorá nemusí byť vyplnená hracími kameňmi, ale môže mať niektoré políčka zablokované. Namiesto čísel môžu byť kamene označené rôznymi farbami, pričom užívateľovi sa zobrazuje aktuálne rozloženie kameňov a cieľový obrazec z farebných kameňov. Ak má viacero kameňov rovnakú farbu, pokladáme ich za voľne zameniteľné.
Prednáška 24
Úvod do predmetu
Ciele predmetu
- prehĺbiť a rozšíriť zručnosti v algoritmickom uvažovaní, písaní a ladení programov z predchádzajúceho semestra
- oboznámiť sa so základnými programovými a dátovými štruktúrami jazyka Java
- zvládnuť základy objektovo-orientovaného programovania a tvorby programov s grafickým užívateľským rozhraním
- oboznámiť sa so základnými algoritmami na prácu s grafmi
Technické detaily
- Budeme používať verziu Java SE 8 (nie najnovšiu Java SE 11)
- Budeme naďalej používať systém Netbeans, ale
- Pre Javu je lepšie prispôsobený ako pre C/C++
- Netbeans čisto pre Javu by mal byť jednoduchšie nainštalovateľný pod Windows
- Ak viete ako, môžete používať aj v iné prostredia, napr. Eclipse, prípadne textový editor. Pozor, na skúške len štandardné Linuxové prostredie v učebniach.
Literatúra
- Knihy na prezenčné štúdium vo fakultnej knižnici:
- B. Eckel: Thinking in Java. 4th edition Prentice-Hall, 2006. Signatúra I-INF-E-2
- Staršie 2. vydanie v českom preklade v knižnici so signatúrou D-INF-E-1b, 3. vydanie v angličtine online, doporučujeme však radšej 4. vydanie
- M. T. Goodrich, R. Tamassia: Data Structures and Algorithms in Java. John Wiley & Sons, 2006. Signatúra D-INF-G-5
- R. Sedgewick: Algorithms in Java, Parts 1-4, Fundamentals, Data Structures, Sorting, Searching, Addison-Wesley, 2003. Signatúra D-INF-S-1/I-IVa
- R. Sedgewick: Algorithms in Java, Part 5, Graph Algorithms Addison-Wesley, 2004. Signatúra D-INF-S-1/V (I)
- B. Eckel: Thinking in Java. 4th edition Prentice-Hall, 2006. Signatúra I-INF-E-2
- Dokumentácia k jazyku Java od firmy Oracle (používame verziu Java SE 8):
- Tutoriál jazyka Java
- referenčná príručka k štandardným knižniciam
Pravidlá na tento semester
- Presné znenie
- Zmeny oproti minulému semestru:
- Týždenne iba jedna prednáška a jedny cvičenia
- Trochu viac domácich úloh, niektoré budú väčšie, vyžadujú priebežnú prácu
- Bude nepovinný bonusový projekt za 10%. Témy na projekt a bližšie informácie oznámime neskôr. Odporúčame robiť vo dvojiciach.
- Rozcvička na cvičeniach cca raz za dva týždne, ostatné úlohy na cvičeniach sa neodovzdávajú a nebodujú
- Občas môžu byť ďalšie príklady za malý počet bonusových bodov
- Rozcvička bude z učiva, ktoré sa už cvičilo na predchádzajúcich cvičeniach, mali by ste teda na ňu byť pripravení
- Na rozcvičkách môžu byť aj témy z minulého semestra (práca s poľami, zoznamami, stromami, rekurzia,...), ale v Jave
- Dva písomné testy, prvý 3.4.
- Na skúške treba mať celkovo aspoň polovicu bodov, ale nemusí byť jeden príklad celý dobre
- Test pre pokročilých bude podobne ako v zimnom semestri, domáce úlohy sú ale pre všetkých (nedá sa použiť Rýchlostné programovanie)
Odporúčania
- Neopisujte
- Pracujte na DÚ priebežne, nie tesne pred termínom
- Ak niečomu nerozumiete alebo potrebujete poradiť s DÚ, pýtajte sa
- Využite cvičenia na precvičenie učebnej látky
Začiatok semestra
- Prvá úloha už zverejnená, termín odovzdania budúcu stredu
- Druhá úloha zverejnená budúci týždeň
- V stredu prvé cvičenia, bude aj malý bonusový príklad
- Prvá rozcvička budúci týždeň
- Test pre pokročilých budúci týždeň
- Prihlásenie sa a hlasovanie o čase [1] do stredy
Hello world
V Netbeans
Vytvorenie projektu:
- V menu zvolíme New Project
- Na prvej obrazovke zvolíme Categories: Java a Projects: Java Application
- Na ďalšej obrazovke Project name: hello a Create Main Class: hello.Hello
- Do súboru Hello.java napíšeme text:
package hello; public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); } }
- Potom spúšťame podobne ako program v jazyku C++
V Linuxe na príkazovom riadku
Ak chcete Javu skúsiť bez použitia Netbeans:
- Vytvoríme adresár hello, v ňom súbor Hello.java s rovnakým obsahom ako vyššie
- Kompilácia javac hello/Hello.java (vznikne súbor hello/Hello.class)
- Spustenie java hello.Hello
- Pozor, meno adresára musí sedieť s menom balíčka (hello), meno súboru s menom triedy (Hello)
- Ak vynecháme riadok package hello, môžeme mať súbor Hello.java priamo v aktuálnom adresári.
Väčší program
- Ukážme si teraz väčší program, v ktorom bude aj načítanie vstupu, polia a rekurzia.
- Je to javová verzia C++ programu na generovanie variácií bez opakovania z minulého semestra.
- Jednotlivé jazykové konštrukty použité v programe rozoberieme nižšie v texte.
Najskôr v C++:
#include <iostream> using namespace std; void vypis(int a[], int k) { for (int i = 0; i < k; i++) { cout << a[i]; } cout << endl; } void generuj(int a[], bool bolo[], int i, int k, int n) { /* v poli a dlzky k mame prvych i cifier, * v poli bolo mame zaznamenane, ktore cifry su uz pouzite, * chceme vygenerovat vsetky moznosti * poslednych k-i cifier */ if (i == k) { vypis(a, k); } else { for (int x = 0; x < n; x++) { if (!bolo[x]) { a[i] = x; bolo[x] = true; generuj(a, bolo, i + 1, k, n); bolo[x] = false; } } } } int main(void) { int k, n; cin >> k >> n; int *a = new int[k]; bool *bolo = new bool[n]; for (int i = 0; i < n; i++) { bolo[i] = false; } generuj(a, bolo, 0, k, n); delete[] a; delete[] bolo; }
- A teraz v Jave:
package hello; import java.util.Scanner; public class Hello { static void vypis(int[] a) { for (int x : a) { System.out.print(" " + x); } System.out.println(); } static void generuj(int[] a, boolean[] bolo, int i, int n) { /* v poli a dlzky k mame prvych i cifier, * v poli bolo mame zaznamenane, ktore cifry su uz pouzite, * chceme vygenerovat vsetky moznosti * poslednych k-i cifier */ if (i == a.length) { vypis(a); } else { for (int x = 0; x < n; x++) { if (!bolo[x]) { a[i] = x; bolo[x] = true; generuj(a, bolo, i + 1, n); bolo[x] = false; } } } } public static void main(String[] args) { int k, n; Scanner s = new Scanner(System.in); k = s.nextInt(); n = s.nextInt(); int[] a = new int[k]; boolean[] bolo = new boolean[n]; for (int i = 0; i < n; i++) { bolo[i] = false; } generuj(a, bolo, 0, n); } }
Základy jazyka Java
Primitívne typy, polia a referencie
Primitívne typy (podobné na C/C++)
- int: 32-bitové číslo so znamienkom, hodnoty v rozsahu -2,147,483,648..2,147,483,647 (ďalšie celočíselné typy byte, short, long)
- double: 64-bitové desatinné číslo s pohyblivou desatinnou čiarkou (a 32-bitový float)
- boolean: hodnota true alebo false
- char: 16-bitový znak v kódovaní Unicode (podporuje teda napr. slovenskú diakritiku)
Lokálne premenné treba inicializovať, inak kompilátor vyhlási chybu:
int y; System.out.println(y); // variable y might not have been initialized
V poliach a v objektoch kompilátor inicializuje premenné na 0, null, resp. false.
Polia
- Polia v Jave vedia svoju dĺžku, nemusíme ju ukladať v ďalšej premennej
- Pole musíme alokovať príkazom new:
double[] a; // deklarujeme premennú typu pole desatinných čísel, zatiaľ ma neinicializovanú hodnotu a = new double[3]; // alokujeme pole troch desatinných čísel for (int i = 0; i < a.length; i++) { // do poľa uložíme čísla 0..2 a[i] = i; }
- Alebo mu môžeme priradiť počiatočné hodnoty: double[] a = {0.0, 1.0, 2.0};
- Java kontroluje hranice polí, napr. System.out.println(a[3]); spôsobí chybu počas behu programu: Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3
Referencie
- Každá premenná v Jave obsahuje buď hodnotu primitívneho typu alebo referenciu.
- Referencia, podobne ako smerník v C, predstavuje adresu v pamäti.
- Referencia môže ukazovať na pole alebo objekt, ale nie na primitívny typ.
- Nefunguje smerníková aritmetika.
- Referencie môžu mať hodnotu null, ak neukazujú na žiadnu pamäť.
- Na jedno pole alebo objekt môžeme mať viac referencií:
double[] a = {0.0, 1.0, 2.0}; double[] b = a; // skopíruje referenciu na to iste pole do b a[1]+=2; // zmeníme pole, na ktoré ukazujú a aj b System.out.println(b[1]); // vypíše 3.0 a = new double[2]; // a a b teraz ukazujú na rôzne polia
- V Jave nemusíme polia odalokovať, program to spraví sám, keď už na nich nie je žiadna referencia (garbage collection)
Operátory, cykly, podmienky
- Operátory podobne ako C/C++, napr. aritmetické +, -, *, /, %, priradenie =, +=,..., ++, --, logické !, &&, ||, porovnávanie ==, !=, >=,...
- Pozor, pri referenciách operátor == testuje, či ukazujú na tú istú pamäť, nie či je v tej pamäti tá istá hodnota
- Podmienky if, else, switch rovnako ako v C
- Cykly for, while, do .. while podobne ako v C, podobne break a continue
Navyše Java má cyklus for, ktorý ide cez všetky hodnoty v poli aj bez indexovej premennej
- Tu vidíme dva spôsoby ako vypísať obsah poľa
double[] a = {0.0, 1.0, 2.0}; for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } for (double x : a) { System.out.println(x); }
- Pozor, takýto cyklus sa nedá použiť na zmenenie hodnôt v poli:
for (double x : a) { x = 0; // nemení pole, iba lokálnu premennú x }
Funkcie (statické metódy) a ich parametre
- Ak chceme písať menší program bez vlastných objektov, ako sme robili v C, použijeme statické metódy umiestnené v jednej triede
- Pred každé meno metódy okrem návratového typu píšeme slovo static
- Pred main píšeme aj slovo public, aby bola viditeľná aj mimo aktuálneho balíčku.
- Návratový typ funkcie main je void, argumenty sú v poli reťazcov (nie je tam meno programu ako v C)
package pocet; public class Pocet { public static void main(String[] args) { System.out.println("Pocet argumentov: " + args.length); } }
- Parametre funkcií sa odovzdávajú hodnotou
- Ak ide o primitívny typ, funkcii sa skopíruje jeho hodnota
- Ak ide o referenciu na pole alebo objekt, funkcii sa skopíruje táto referencia, funkcia môže teda meniť tento objekt alebo pole
- Nedá sa teda napísať funkcia swap, ktorá vymení obsah dvoch premenných
- Tu je ilustratívny príklad:
static void pokus(int[] a, int x) { a[1] = 5; // zmena v poli, na ktoré ukazuje a aj b a = new int[3]; // a ukazuje na nové pole, b na staré System.out.println(a[1]); // vypíše 0 x = 6; // zmena x, y sa nemení } public static void main(String[] args) { int[] b = {1, 2, 3}; int y = 4; pokus(b, y); System.out.println(b[1]); // vypíše 5 System.out.println(y); // vypíše 4 }
- Návratový typ môže byť void, primitívny typ alebo referencia
- Príkaz return ako v C
Práca s maticami
- V poli môžeme mať aj referencie na iné polia, dostávame tak viacrozmerné matice, podobne ako v C-čku.
- Deklarácia 3-rozmerného poľa: int[][][] a;
- Ak sú všetky rozmery známe, môžeme ho jedným príkazom alokovať, napr. a=new int[2][3][4];
- Môžeme však spraviť napr. trojuholníkovú maticu, v ktorej má každý riadok inú dĺžku:
package hello; public class Hello { static void vypis(int[][] a) { /* vypiseme cisla v matici a na konzolu */ for (int[] riadok : a) { for (int x : riadok) { System.out.print(" " + x); } System.out.println(); } } public static void main(String[] args) { int[][] a = new int[3][]; for (int i = 0; i < a.length; i++) { a[i] = new int[i+1]; for (int j = 0; j < a[i].length; j++) { a[i][j] = i * j; } } vypis(a); } }
Výstup:
0 0 1 0 2 4
- Podobne 3-rozmerné pole s rôzne veľkými podmaticami a riadkami:
static void vypis(int[][][] a) { /* vypiseme cisla v 3D poli a na konzolu */ for (int[][] matica : a) { for (int[] riadok : matica) { System.out.print("["); for (int x : riadok) { System.out.print(" " + x); } System.out.print(" ] "); } System.out.println(); } } public static void main(String[] args) { int[][][] a = new int[3][][]; for (int i = 0; i < a.length; i++) { a[i] = new int[i + 1][]; for (int j = 0; j < a[i].length; j++) { a[i][j] = new int[j + 1]; for (int k = 0; k < a[i][j].length; k++) { a[i][j][k] = i * j * k; } } } vypis(a); }
Výstup:
[ 0 ] [ 0 ] [ 0 1 ] [ 0 ] [ 0 2 ] [ 0 4 8 ]
Reťazce
- Reťazec je objekt triedy String, po vytvorení sa nedá meniť
- Text medzi úvodzovkami je považovaný za String
- Inicializácia konštantným reťazcom: String greeting = "Hello world!";
- Operátor + konkatenuje (zliepa) reťazce. Ak je jeden operand reťazec, iné typy konvertuje na String:
int x=1; String str = "Hodnota x: " + x;
Prístup k reťazcu:
- dĺžka sa počíta metódou length() a i-ty znak metódou charAt(i)
String str = "Ahoj!"; int len = str.length(); // dlzka retazca for (int i = 0; i < len; i++) { System.out.println(i + ". znak: " + str.charAt(i)); }
Výstup:
0. znak: A 1. znak: h 2. znak: o 3. znak: j 4. znak: !
- Porovnanie reťazcov na rovnosť metódou equals (Pozor, porovnanie == testuje, či ide o to isté miesto v pamäti)
String str1 = "abc"; // reťazec abc String str2 = str1; // referencia na ten istý reťazec String str3 = str1 + ""; // vznikne nový reťazec abc if (str1 == str2) { // true, lebo to isté miesto System.out.println("str1==str2"); } if (str1 == str3) { // false, lebo rôzne miesta System.out.println("str1==str3"); } if (str1.equals(str3)) { // true, lebo zhodné reťazce System.out.println("str1.equals(str3)"); }
- Veľa ďalších metód, pozri dokumentáciu
- Ak potrebujete reťazec meniť, napr. k nemu postupne pridávať, môžete použiť StringBuilder
- Rýchlejšie ako stále vyrábať nové reťazce pomocou operátora + (pre spájanie malého počtu častí stačí String)
- Napr. dva spôsoby ako vytvoriť reťazec abeceda obsahujúci písmená a..z:
// Pomocou String, postupne vytvorí 27 rôznych String-ov String abeceda = ""; for (char c = 'a'; c <= 'z'; c++) { abeceda = abeceda + c; // vytvorí nový String, naplní ho novým obsahom }
// Pomocou StringBuilder, vytvorí jeden StringBuilder a jeden String StringBuilder buffer = new StringBuilder(); for (char c = 'a'; c <= 'z'; c++) { buffer.append(c); // modifikuje objekt buffer } String abeceda = buffer.toString(); // vytvorí nový String
Vstup, výstup, súbory
- Java má rozsiahle knižnice, uvádzame len návod na základnú prácu s textovými súbormi.
- Vo väčšine prípadov potrebujeme triedy z balíčku java.io, takže si ich môžeme naimportovať všetky: import java.io.*;
- Trieda Scanner je v balíčku java.util, použijeme teda import java.util.Scanner;
- V prípade, že pri práci so súbormi nastane nečakaná chyba, Java použije mechanizmus výnimiek (exception)
- O výnimkách sa budeme učiť neskôr, nateraz len do metódy main (a prípadne ďalších metód) pridáme upozornenie, že výnimka môže nastať:
- public static void main(String[] args) throws java.io.IOException { ... }
Písanie na konzolu
- System.out.print(retazec)
- System.out.println(retazec) - pridá koniec riadku
- Reťazec môžeme vyskladať z viacerých častí rôznych typov pomocou +
- Formátovanie podobné na printf v C-čku: System.out.format("%.1f%n", 3.15); vypíše číslo na jedno desatinné miesto, t.j. 3.2 a koniec riadku podľa operačného systému.
- Viac detailov v dokumentácii alebo v tutoriáli
Čítanie z konzoly
- Objekt System.in je typu FileInputStream a podporuje iba čítanie jednotlivých bajtov resp. polí bajtov
- Lepšie sa pracuje, ak ho použijeme ako súčasť objektu, ktorý vie tieto bajty spracovať do riadkov, čísel a pod.
- Trieda BufferedReader umožňuje čítať celý riadok naraz ale aj znak po znaku. Tu je príklad jej použitia:
package hello; import java.io.*; // potrebujeme triedy z balíčka java.io public class Hello { public static void main(String[] args) throws java.io.IOException { // musíme pridať oznam, že môže vzniknúť výnimka - chyba pri čítaní BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while (true) { // načítame riadok do reťazca String line = in.readLine(); // skončíme, keď užívateľ zadá prázdny riadok alebo keď prídeme na koniec vstupu (null) if (line == null || line.equals("")) { break; } // vypíšeme načítany riadok System.out.println("Napísali ste riadok \"" + line + "\""); } System.out.println("Končíme..."); } }
Príklad behu programu:
Ahoj Napísali ste riadok "Ahoj" 1 2 3 Napísali ste riadok "1 2 3" Končíme...
- Metóda readLine() teda číta celý riadok (odstráni znak pre koniec riadku), metóda read() číta jeden znak (na konci súboru vráti -1)
- Trieda Scanner rozkladá vstup na slová oddelené bielymi znakmi (medzery, konce riadku a pod.) a prípadne ich premieňa na čísla.
- Príklad programu, ktorý vypisuje slová načítané od užívateľa, kým nezadá END alebo neskončí vstup
package hello; import java.util.Scanner; public class Hello { public static void main(String[] args) { Scanner s = new Scanner(System.in); // inicializujeme Scanner int num = 0; while (s.hasNext()) { // kym neskonci vstup String word = s.next(); // nacitame slovo if (word.equals("END")) { // skoncili sme ak najdeme END break; } System.out.println("Slovo " + num + ": " + word); // vypiseme slovo num++; } } }
Príklad behu programu:
Ahoj Slovo 0: Ahoj a b c END Slovo 1: a Slovo 2: b Slovo 3: c
- Metóda nextInt() vráti ďalšie slovo konvertované na int (pozri program s rekurziou vyššie). Či nasleduje číslo, si môžeme vopred overiť metódou hasNextInt(). Podobne nextDouble().
Práca so súbormi
Čítanie zo súboru funguje podobne ako čítanie z konzoly, iba inak inicializujeme použitý objekt:
- Scanner vytvoríme príkazom Scanner s = new Scanner(new File("vstup.txt"));
- File reprezentuje súbor s určitým menom, potrebujeme pridať import java.io.File; alebo import java.io.*;
- BufferedReader vytvoríme príkazom BufferedReader in = new BufferedReader(new FileReader("vstup.txt"));
- Scanner aj BufferedReader umožňujú zavrieť súbor metódou close()
Písanie do súboru môžeme robiť napr. triedou PrintStream
- Otvorenie súboru: PrintStream out = new PrintStream("vystup.txt");
- Potom používame staré známe metódy print, println, format ako pri System.out (napr. out.println("Ahoj"))
- Na konci zavoláme out.close()
- Tento spôsob otvárania súborov existujúci obsah premaže
- Ak chceme pridávať na koniec súboru, použijeme PrintStream out = new PrintStream(new FileOutputStream("vystup.txt",true));
Matematika a pseudonáhodné čísla
- V triede Math nájdete rôzne matematické konštanty a funkcie
- Napr. Math.PI, Math.cos(x), Math.min(x,y), Math.pow(x,y), ...
- Triedy na prácu s veľkými číslami a ďalšie matematické funkcie nájdete v balíčku java.math
Pseudonáhodné čísla
- Math.random() vygeneruje double z intervalu [0,1)
- Väčšie možnosti poskytuje trieda Random v balíčku java.util (generuje celé čísla, bity), umožňuje nastaviť počiatočnú hodnotu
Cvičenia 13
Cieľom dnešného cvičenia je vyskúšať si prácu v Jave, precvičiť si prácu s poliami, vstupom a výstupom a odovzdať malý bonusový príklad na testovač
- Na testovači máte to isté heslo ako minulý semester. Ak si ho nepamätáte, povedzte cvičiacim alebo napíšte na prog@fmph.uniba.sk
- Bonusový príklad môžete robiť do 22:00 (v budúcnosti môžu byť prípadné bonusové príklady limitované na dobu cvičenia)
Budúci týždeň bude na začiatku cvičenia rozcvička z tohtotýždňového učiva
Príklad 1: Spúšťanie programu Hello world
Skúste spustiť program Hello world v Netbeans, na príkazovom riadku alebo v inom prostredí
V Netbeans
Vytvorenie projektu:
- V menu zvolíme New Project
- Na prvej obrazovke zvolíme Categories: Java a Projects: Java Application
- Na ďalšej obrazovke Project name: hello a Create Main Class: hello.Hello
- Do súboru Hello.java napíšeme text:
package hello; public class Hello { public static void main(String[] args) { System.out.println("Hello World!"); } }
- Potom spúšťame podobne ako program v jazyku C++
V Linuxe na príkazovom riadku
Ak chcete Javu skúsiť bez použitia Netbeans:
- Vytvoríme adresár hello, v ňom súbor Hello.java s rovnakým obsahom ako vyššie
- Kompilácia javac hello/Hello.java (vznikne súbor hello/Hello.class)
- Spustenie java hello.Hello
- Pozor, meno adresára musí sedieť s menom balíčka (hello), meno súboru s menom triedy (Hello)
- Ak vynecháme riadok package hello, môžeme mať súbor Hello.java priamo v aktuálnom adresári.
Príklad 2: Matice a vstup v Jave
- Vyriešte príklad na testovači, môžete zaňho získať bonusový bod
Príklad 3: Práca so súbormi
- Napíšte program, ktorý načítava súbor vstup.txt znak po znaku pomocou funkcie read triedy BufferedReader a kopíruje ho do súboru vystup.txt
- Program si sami otestujte - skúste meniť obsah vstupného súboru a skontrolujte výstupný.
- Potom program zmeňte tak, aby keď ide vo vstupe niekoľko rovnakých znakov za sebou, vypíšete z nich iba jeden. Takže napr. pre vstup aabbbccdaa vypíše abcda
Príklad 3: Náhodné čísla
- Pozrite si dokumentáciu k triede Random v balíčku java.util a napíšte program, ktorý odsimuluje 10 hodov kockou, teda vypíše 10 náhodných celých čísel od 1 po 6.
- Napíšte program, ktorý odsimuluje 10 hodov nevyváženou mincou, pri ktorej v každom hode s pravdepodobnosťou 80% padne hlava a s pravdepodobnosťou 20% prípadoch padne znak. Pomôcka: Ak sa chceme rozhodovať medzi dvoma vecami s určitou pravdepodobnosťou x, môžeme vygenerovať náhodné desatinné číslo z intervalu [0,1) a ak je toto náhodné číslo menej ako x, zvolíme jednu možnosť a ak viac ako x, zvolíme druhú.
Prednáška 25
Oznamy
- Test pre pokročilých bude dnes o 14:00 (pre prihlásených)
- Na cvičení v stredu (27.2.) bude prvá rozcvička (z učiva z minulej prednášky / cvičení)
- V druhej časti cvičenia nebodované príklady na precvičenie dnešného učiva, dôležité nezanedbať
- DÚ5 odovzdávajte do stredy 27.2. 22:00 - dobrá príprava na rozcvičku.
- Nová DÚ6 má termín odovzdania 13.3., ale nenechávajte si ju na poslednú chvíľu.
Objektovo orientované programovanie (OOP)
- Java je objektovo-orientovaný jazyk a teda skoro všetko v Jave je objekt
- Základným pojmom OOP je trieda (class)
- Trieda je typ združujúci niekoľko hodnôt, podobne ako struct v C
- Navyše ale trieda obsahuje metódy (funkcie), ktoré s týmito hodnotami pracujú
- Objekty sú inštancie triedy
- Napríklad trieda Zlomok môže mať položky citatel a menovatel a konkrétnou inštanciou, objektom je napríklad zlomok s čitateľom 2 a menovateľom 3 vytvorený v programe.
Viac o objektoch nájdete v tutoriáli
Napríklad v Cčku by jednoduchý zásobník int-ov implementovaný pomocou poľa a funkcia pop, ktorá z neho vyberie prvok, mohli vyzerať takto:
struct Stack { int *data; int pocet; }; int pop(Stack &s) { s.pocet--; return s.data[s.pocet]; }
Keď to prepíšeme ako triedu v Jave, vyzerá to podobne, ale:
- slovo struct sa nahradí slovom class
- metóda pop sa presunie do vnútra definície triedy
- metóda pop nedostane zásobník ako argument a k jeho položkám pristupuje priamo ich menami, t.j. napr. data a pocet
public class Stack { int data[]; int pocet; int pop() { pocet--; return data[pocet]; } }
Metódy sa potom volajú pre konkrétny zásobník, napr.
Stack s; // tu pridu prikazy na vytvorenie a naplnenie zasobnika int x = s.pop() // vyberie prvok zo zasobnika s
V Cčku by sme písali
Stack s; // tu pridu prikazy na vytvorenie a naplnenie zasobnika int x = pop(s);
Ak máme premennú s typu Stack, k jej premenným a metódam pristupujeme pomocou operátora .
- napr. s.pop(), s.pocet
- Java nemá operátor ->
- Ale pozor, premenná s typu Stack je referencia
- Po príkaze Stack t = s; premenné s a t ukazujú na to isté miesto v pamäti, na ten istý zásobnik
- Čo by podobný príkaz spravil v Cčku? V tomto prípade asi nie to, čo chceme...
Konštruktor a vznik objektov
V Cčku sme pre zásobník mali metódu init, ktorá inicializovala hodnoty pre prázdný zásobník, napr. takto:
void init(Stack &s) { s.data = new int[MAXN]; s.pocet = 0; }
Objekty sa inicializujú špeciálnou metódou, konštruktorom
- Názov konštruktora je názov triedy. Teda konštruktor triedy Stack bude metóda Stack()
- Konštruktor nemá v hlavičke návratovú hodnotu, môže však mať parametre.
public class Stack { Stack() { data = new int[MAXN]; pocet = 0; } ... }
Príkaz Stack s; vytvorí referenciu s, ktorá je však zatiaľ neinicializovaná, t.j. nikam neukazuje a Java nám ju nedovolí použiť.
- Mohli by sme ju nastaviť na null
Na vytvorenie nového objektu použijeme príkaz new:
s = new Stack();
Príkaz new
- dynamicky alokuje pamäť pre objekt
- zavolá konštruktor objektu
- vráti referenciu - pamäťovú adresu objektu
Viac detailov neskôr
Kontrola prístupu, modifikátory
Trieda a jej súčasti môžu byť odniekiaľ prístupné a odinakiaľ nie. Na úpravu prístupových práv používame modifikátory.
- modifikátor private: premenná/metóda je prístupná iba z metód príslušnej triedy
- keď nepoužijeme modifikátor: trieda/premenná/metóda je prístupná z balíčka (package), kde sa nachádza
- modifikátor protected: podobne ako bez modifikátora, rozdiel uvidíme pri dedení
- modifikátor public: trieda/premenná/metóda je prístupná ľubovoľne
Mená súborov, main:
- public trieda musí byť v súbore nazvanom po tejto triede, ale môžu tam s ňou byť aj ďalšie (pomocné) triedy, ktoré nie sú public
- spustiteľná metóda main musí byť public a umiestnená v public triede
O ďalších modifikátoroch, napr. abstract, static, final, sa dozvieme neskôr
Zapuzdrenie (encapsulation)
- Jedným z hlavných princípov OOP je zapuzdrenie
- Dáta a k nim prislúchajúce metódy zabalíme do triedy
- Kód mimo triedy by k dátam objektu mal pristupovať iba pomocou poskytnutých metód
- Väčšinou teda premenným nastavíme modifikátor private alebo protected a pomocným metódam tiež
- Public metódy triedy tvoria našu ponuku pre používateľov triedy
- Ak zmeníme vnútornú implementáciu triedy, ale zanecháme rovnaké public metódy a ich správanie, používateľov triedy by to nemalo ovplyvniť
- Napríklad v triede Stack sa môžeme rozhodnúť namiesto poľa použiť spájaný zoznam, čím potrebujeme preprogramovať triedu Stack, ale program, ktorý ju používa, sa meniť nemusí
- Zapuzdrenie umožňuje rozdeliť väčší projekt na pomerne nezávislé časti s dobre definovaným rozhraním
public class Stack { public static final int MAXN = 100; private int data[]; private int pocet; public Stack() { data = new int[MAXN]; pocet = 0; } public int pop() { pocet--; return data[pocet]; } public void push(int x) { data[pocet] = x; pocet++; } public boolean isEmpty() { return pocet==0; } }
Get a set metódy
Nakoľko premenné v triedach sú väčšinou private, niektoré triedy ponúkajú nepriamy prístup cez get a set metódy, napr.
class Contact { private String name; private String email; private String phone; public String getName() { return name; } public String getEmail() { return email; } public void setEmail(String newEmail) { email = newEmail; } public String getPhone() { return phone; } public void setPhone(String newPhone) { phone = newPhone; } }
- get a set metódy nerobíme mechanicky pre všetky premenné, iba pre tie, ktoré je rozumné sprístupniť mimo triedu
- ak poskytneme iba get metódu, premenná je zvonku v podstate read-only
- v set metódach môžeme kontrolovať, či je zadaná hodnota rozumná (napr. menovateľ zlomku nemá byť 0)
- get a set metódy nemusia presne korešpondovať s premennými a teda môže sa nám podariť ich zachovať aj po zmene vnútornej reprezentácie
- napr. ak getAngle a setAngle berú uhol v stupňoch, ale rozhodneme sa ho ukladať radšej v radiánoch, môžeme do týchto metód naprogramovať konverziu
class SomeGeometricObject { private double angle; // uhol v radianoch public double getAngle() { return angle * 180.0 / Math.PI; } public void setAngle(double x) { angle = x * Math.PI / 180.0; } }
- namiesto mechanicky vytvorených set a get metód sa pokúste vytvoriť rozumné metódy, ktoré súvisia s využitím triedy. Napríklad trieda stack má metódy push a pop, nie set metódu na premennú top.
Ďalšie detaily
Premenná this
V rámci metód triedy premenná this je referencia na konkrétny objekt, na ktorom bola metóda zavolaná.
Napr. ak zavoláme s.pop(), tak vo vnútri metódy pop premenná this ukazuje na s.
- this.premenna je to isté ako premenna
- this.metoda(...) to isté ako metoda(...)
Jedno využitie this je poslanie objektu ako argumentu inej metóde, napr.
public static emptyStack(Stack s) { while(!s.empty()) { s.pop(); } }
V triede Stack potom môžeme mať napr. metódu
public empty() { emptyStack(this); }
Samozrejme logickejšie by bolo naprogramovať vyprázdnenie zásobníka priamo v triede a nie volať externé metódy.
Premenná this sa tiež hodí, ak sa argument metódy volá rovnako ako premenná triedy. Vtedy sa pomocou this vieme dostať k premennej a bez this k argumentu
class Contact { private String email; /** nastav novú emailovú adresu */ public void setEmail(String email) { this.email = email; } }
Viac metód s tým istým menom: overloading
Trieda môže mať niekoľko metód s tým istým menom, ale rôznymi typmi alebo počtom parametrov. Kompilátor vyberie tú, ktorá sa najlepšie hodí použitiu. Napr.
class Contact { private String email; public void setEmail(String email) { this.email = email; } public void setEmail(String username, String domain) { email = username + "@" + domain; } } Contact c = new Contact(); c.setEmail("jozkomrkvicka@gmail.com"); // prva metoda c.setEmail("jozkomrkvicka", "gmail.com"); // druha metoda
Overloading sa dá použiť aj na konštruktory:
class Node { private int data; private Node next; public Node() {} public Node(int data) { this.data = data; } public Node(Node next) { this.next = next; } public Node(int data, Node next) { this.data = data; this.next = next;} public int getData() { return data;} public void setData(int data) { this.data = data;} public Node getNext() { return next;} public void setNext(Node next) {this.next = next;} }
Detaily inicializácie objektov
- príkaz new najskôr inicializuje jednotlivé premenné (na 0, false, null) alebo na hodnotu, ktorú zadáme
class Node { private int data = -1; private Node next; // bude inicializovaný na null }
- až potom spúšťa konštruktor
- prvý riadok konštruktora môže volať iný konštruktor tej istej triedy pomocou this(...) - často s menším alebo väčším počtom parametrov
class Node { private int data; private Node next; public Node(int data, Node next) { this.data = data; this.next = next;} public Node(int data) { this(data, null) } ... }
- V prípade, že nedefinujeme pre triedu žiaden konštruktor, bude automaticky vygenerovaný konštruktor bez parametrov
- tento inicializuje premenné na defaultné hodnoty
- defaultný konštruktor je vytvorený iba ak žiaden iný konštruktor neexistuje
- Ďalšie detaily na prednáške o dedení
Nie všetko v Jave je objekt
Opakovanie:
- Ako sme videli na minulej prednáške, každá premenná obsahuje buď hodnotu primitívneho typu (int, double, bool, char a pod.) alebo referenciu
- Referencia môže ukazovať na objekt alebo pole
- Pole môže obsahovať primitívne typy alebo referencie na iné objekty/polia
Wrapper
- Ku každému primitívnemu typu existuje aj zodpovedajúca trieda (wrapper), napr. Integer, Double, ... (viac pozri [2])
- Java medzi primitívnymi typmi a týmito triedami podľa potreby automaticky konvertuje (viac neskôr)
Polia
- Polia sú špeciálny typ objektov, viď napr. premennú a.length, ale aj ďalšie metódy (neskôr)
Javadoc
- Javadoc je systém na vytváranie dokumentácie
- Javadoc komentár začína /** a končí ako klasický komentár */ pričom každý riadok začína *
- Javadoc komentáre sa umiestnia pred triedu, premennú alebo metódu, ktorú chceme popísať
- Prvý riadok Javadoc komentára resp. po prvú bodku je stručný slovný popis. Ďalej pokračujú rôzne podrobnosti.
- Javadoc poskytuje rôzne tag-y [3]
Program Javadoc vie na základe kódu a Javadoc komentárov vygenerovať dokumentáciu (napr. v html formáte)
- dá sa spustiť cez Netbeans v časti Run, Generate Javadoc
- automaticky vytvára dokumentáciu iba k public položkám (keďže tie tvoria rozhranie, API k iným triedam)
- vo vlastnostiach aplikácie časť Documenting sa dá nastavovať
Viď príklad Javadocu v triede nižšie.
Binárny vyhľadávací strom
Príklad binárneho vyhľadávacieho stromu s pomocnou triedou Node a triedou BinarySearchTree.
- Trieda Node obsahuje pomocné metódy a rekurzívne funkcie
- Trieda BinarySearchTree skrýva tieto implementačné detaily pred používateľom, pričom ponúka možnosť pridať prvok a vypísať všetky prvky v utriedenom poradí
/** Trieda reprezentujúca jeden vrchol binárneho vyhľadávacieho stromu. * Každý vrchol v strome obsahuje dáta typu int a referenciu na ľavý a * pravý podstrom. Pre každý vrchol platí, že že všetky vrcholy v jeho * ľavom podstrome majú hodnotu menšiu ako on a všetky vrcholy v * pravom podstrome väčšiu. */ class Node { /** Dáta typu int uložené vo vrchole */ private int data; /** Referencia na ľavé dieťa alebo null ak neexistuje */ private Node left; /** Referencia na pravé dieťa alebo null ak neexistuje */ private Node right; /** Konštruktor, ktorý vytvorí nový list * so zadanou hodnotou <code>data</code>. * @param data Dáta uložené v novom vrchole. */ public Node(int data) { this.data = data; } /** Metóda vráti dáta uložené vo vrchole. * @return dáta uložené vo vrchole */ public int getData() { return data; } /** Metóda, ktorá do stromu vloží nový vrchol <code>newNode</code>. * * @param newNode Nový vrchol vložený do stromu. Mal by byť listom. */ public void addNode(Node newNode) { if (newNode.data <= this.data) { if (left == null) { left = newNode; } else { left.addNode(newNode); } } else { if (right == null) { right = newNode; } else { right.addNode(newNode); } } } /** Metóda, ktorá vypíše hodnoty uložené vo vrcholoch podstromu * v inorder poradí, každý na jeden riadok. */ public void printInorder() { if (this.left != null) left.printInorder(); System.out.println(data); if (this.right != null) right.printInorder(); } } /** Trieda reprezentujúca binárny vyhľadávací strom, ktorý má v každom * vrchole dáta typu int. Strom umožňuje pridávať nové dáta a * vypísať dáta v utriedenom poradí. */ public class BinarySearchTree { /** Premenná obsahujúca koreň stromu, alebo null, ak je strom prázdny. */ private Node root; /** Konštruktor vytvorí prázdny strom. */ public BinarySearchTree() { } /** Metóda do stromu pridá novú hodnotu <code>data</code>. * Malo by ísť o hodnotu, ktorá sa ešte v strome nenachádza. * @param data Nová hodnota pridaná do stromu. */ public void add(int data) { Node p = new Node(data); if (root == null) { root = p; } else { root.addNode(p); } } /** Metóda vypíše všetky hodnoty v strome v utriedenom poradí, * každú na jeden riadok. */ public void printSorted() { if (root != null) { root.printInorder(); } } /** Metóda je ukážkou použitia binárneho vyhľadávacieho stromu. * Do stromu vloží tri čísla a potom ich vypíše. */ public static void main(String args[]) { BinarySearchTree t = new BinarySearchTree(); t.add(2); t.add(3); t.add(1); t.printSorted(); } }
Pomocné triedy
Nakoniec dva typy pomocných tried, ktoré môžeme použiť na obídenie obmedzení javovských funkcií (metód).
Odovzdávanie parametrov hodnotou
- Všetky parametre sa v Jave odovzdávajú hodnotou - teda vytvorí sa lokálna kópia parametra a jej zmenou nedocielime zmenenie pôvodnej premennej
- Ak je ale parametrom referencia, nakopíruje sa adresa a môžeme teda meniť obsah pamäte, kam ukazuje
- Ak by sme teda parameter chceli meniť, podobne ako pri odovzdávaní premenných referenciou v C, môžeme si vytvoriť wrapper class, ktorý danú hodnotu obalí a umožní k nej pristúpiť cez referenciu
- Knižničné wrapper triedy ako Integer nemôžeme použiť, lebo tie tiež neumožňujú meniť hodnotu už vytvoreného objektu
class MyInteger { private int x; // data public MyInteger(int x) { this.x = x; } // konštruktor public int getValue() { return x; } // získanie hodnoty public void setValue(int x) { this.x = x;} // nastavenie hodnoty } static void swap(MyInteger rWrap, MyInteger sWrap) { // vymeň hodnoty vo vnútri objektov int t = rWrap.getValue(); rWrap.setValue(sWrap.getValue()); sWrap.setValue(t); }
Návratová hodnota
Návratová hodnota metódy je buď void, primitívny typ alebo referencia
- ak teda chceme vrátiť niekoľko hodnôt, musíme si spraviť triedu, ktorá ich spája do jedného celku
static class Delenie { public int podiel; public int zvysok; public Delenie(int podiel, char zvysok) { this.podiel = podiel; this.zvysok = zvysok; } } static Delenie vydel(int a, int b) { Delenie vysledok = new Delenie(a / b, a % b); return vysledok; } public static void main(String[] args) { Delenie vysledok = vydel(7, 3); System.out.println(vysledok.podiel + " " + vysledok.zvysok); }
V oboch prípadoch je ale lepšie skúsiť navrhnúť metódy tak, aby neboli takéto pomocné triedy potrebné.
Zhrnutie
- Trieda obsahuje niekoľko premenných (ako struct v C), ale aj metódy, ktoré s týmito premennými pracujú
- Objekt alebo inštancia triedy je konkrétna hodnota v pamäti
- Dôležitou metódou je konštruktor, ktorý inicializuje premenné objektu
- Prístup k premenným a metódam triedy môžeme obmedziť modifikátormi public, private, protected
- Je vhodné použiť princíp zapuzdrenia, kde väčšina premenných je private a trieda navonok ponúka iba logickú sadu metód
Cvičenia 14
Na začiatku cvičenia riešte individuálne rozcvičku zadanú na testovači. Potom riešte ďalšie príklady z tohto cvičenia, ktoré nie sú bodované, takže ich môžete riešiť aj v skupinkách. Cieľom cvičenia je precvičiť si vytváranie a modifikovanie tried.
Polynómy
Navrhnite triedu Polynomial, ktorá bude reprezentovať polynómy jednej premennej s celočíselnými koeficientami. Tieto koefienty je v triede vhodné ukladať do poľa. Napríklad polynóm x3-2x+7 môžeme rozpísať ako 1*x3 + 0*x2 + (-2)*x1 + 7 * x0 a teda jeho koeficienty sú čísla 1,0,-2,7. Tie si uložíme do poľa tak, aby na indexe i bol koeficient pri xi, vznikne nám teda pole s prvkami {7,-2,0,1}.
Pridajte takéto pole do triedy ako premennú a implementujte metódy popísané nižšie. Kvôli testovaniu nájdete na spodku tejto stránky kostru programu s metódou main. Odkomentuje vždy volania funkcií, ktoré ste už implementovali.
- Implementujte niekoľko konštruktorov:
- konštruktor bez parametrov, ktorý vytvorí nulový polynóm
- konštruktor s dvoma celočíselnými parametrami a a i, ktorý vytvorí polynóm Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle ax^i}
- konštruktor, ktorý dostane pole a vytvorí polynóm, ktorého koeficienty budú prvky tohto poľa
- Implementujte metódu public String toString() ktora zapíše koeficienty polynómu do reťazca vo vami vybranom formáte. Túto metódu volajú príkazy System.out.println("volaco: " + polynom) na konverziu polynómu na reťazec a preto sa vám zíde pri testovaní programu.
- Implementujte metódu getCoefficient(int i), ktorá vráti koeficient pri člene Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle x^i} . Metóda by mala sptrávne fungovať pre každé nezáporné i, pričom pre hodnoty väčšie ako stupeň polynómu bude vracať hodnotu 0.
- Implementujte metódu add(Polynomial p), ktorá vráti nový polynóm, ktorý bude súčtom tohto polynómu a polynómu p.
Ak vám na cvičení zostane čas, môžete navrhnúť a implementovať ďalšie funkcie vhodné na prácu s polynómami, napr. počítanie hodnoty polynómu pre určité x, načítanie polynómu zo vstupu, výpočet stupňa polynómu, ďalšie konštruktory a pod.
Stromy
Na prednáške 25 je implementovaný binárny vyhľadávací strom pomocou tried Node a BinarySearchTree. Pridajte do triedy BinarySearchTree nasledujúce dve metódy, pričom podľa potreby pridajte aj metódy do triedy Node. Snažte sa čo najviac zachovať zapuzdrenie tried.
- Metóda boolean contains(int data) zistí, či je v strome vrchol s hodnotou data. Inšpirujte sa metódou add.
- Metóda int depth() vráti hĺbku stromu. Ak je strom prázdny, vráti -1.
Kostra programu k cvičeniu s polynómami
package polynomial; public class Polynomial { // TODO: VASE METODY A PREMENNE SEM public static void main(String[] args) { int[] coeff = {1,2,3,-2}; // TODO: POSTUPNE ODKOMENTUJTE IMPLEMENTOVANE METODY // // test konstruktorov // Polynomial a = new Polynomial(); // Polynomial b = new Polynomial(2,3); // Polynomial c = new Polynomial(coeff); // // vypisanie polynomov // System.out.println("Polynom a: " + a); // System.out.println("Polynom b: " + b); // System.out.println("Polynom c: " + c); // // koeficent pri x^3 v c // System.out.println("Koeficent pri x^3 v c: " + c.getCoefficient(3)); // System.out.println("Koeficent pri x^5 v c: " + c.getCoefficient(5)); // // scitanie polynomov d = b+c; // Polynomial d = b.add(c); // System.out.println("Polynom b+c: " + d); } }
Prednáška 26
Oznamy
- Ďalšiu domácu úlohu treba odovzdať do stredy 13. marca, 22:00.
Opakovanie: triedy a objekty
- Objekt je predovšetkým súborom rôznych dát a metód na manipuláciu s nimi. Na objekty sa odkazuje pomocou ich identifikátorov, ktoré sú referenciami na ich „pamäťové adresy”.
- Každý objekt je inštanciou nejakej triedy (class). Triedu možno chápať ako „vzor”, podľa ktorého sa vytvárajú objekty. Trieda tiež reprezentuje typ jej objektov.
- Trieda sa teda podobá na struct z jazykov C a C++ v tom, že môže obsahovať niekoľko hodnôt rôznych typov. Ide však o omnoho bohatší koncept – môže obsahovať metódy (funkcie) na prácu s dátami uloženými v inštancii danej triedy, umožňuje nastaviť viditeľnosť jednotlivých položiek pomocou modifikátorov, atď.
- Konštruktory sú špeciálne metódy triedy slúžiace na vytvorenie objektu (inštancie triedy) podľa „vzoru”, ktorý táto trieda poskytuje. Obyčajne sa využívajú najmä na inicializáciu dát.
- Základným princípom objektovo orientovaného programovania je zapuzdrenie (angl. encapsulation): spojenie dát a súvisiaceho kódu.
- Trieda väčšinou navonok ukazuje iba vhodne zvolenú časť metód.
- Premenné a pomocné metódy sú skryté – je ich tak možné meniť bezo zmeny kódu využívajúceho triedu.
Konvencie pomenúvania identifikátorov
V jazyku Java existujú konvencie ohľadom odporúčaných mien tried, premenných, metód, atď. Najdôležitejšie z nich sú nasledujúce dve:
- Mená tried by mali začínať veľkým písmenom (napr. String). Pri viacslovných názvoch sa veľkým písmenom začína každé zo slov (napr. ArrayList).
- Mená metód a premenných by naopak mali začínať malým písmenom (napr. print). Pri viacslovných názvoch sa prvé slovo začína malým písmenom a zvyšné slová veľkým písmenom (napr. toString). Výnimkou sú samozrejme konštruktory, ktoré musia mať rovnaký názov, ako ich trieda.
Štandardné knižnice jazyka Java tieto (a mnohé ďalšie) konvencie rešpektujú. Dodržiavanie aspoň základných konvencií voľby pomenovaní je silno odporúčané, nakoľko značne uľahčuje čitateľnosť kódu.
Dedenie
Trieda môže byť podtriedou inej triedy. Napríklad trieda Pes môže byť podtriedou všeobecnejšej triedy Zviera: každý objekt, ktorý je inštanciou triedy Pes je potom súčasne aj inštanciou triedy Zviera. Tento vzťah medzi triedami vyjadrujeme kľúčovým slovom extends v definícii triedy: píšeme teda napríklad
class Pes extends Zviera { ... }
Hovoríme tiež, že trieda Pes dedí od triedy Zviera.
Dedenie umožňuje vyhnúť sa nutnosti písať podobný kód viackrát. Namiesto niekoľkých podobných tried s podobnými metódami možno vytvoriť ich nadtriedu a spoločné časti kódu presunúť tam.
Príklad
Uvažujme triedy reprezentujúce rôzne geometrické útvary, ktoré môžeme posúvať v rovine. Takto by mohli vyzerať časti tried bez dedenia:
class Rectangle { int x, y; // suradnice laveho horneho rohu int width, height; // vyska a sirka public void move(int deltaX, int deltaY) { x += deltaX; y += deltaY; } // sem mozu prist dalsie metody pre obdlznik } class Circle { int x, y; // suradnice stredu int radius; // polomer public void move(int deltaX, int deltaY) { x += deltaX; y += deltaY; } // sem mozu prist dalsie metody pre kruh }
Teraz to isté s dedením – spoločné premenné a metódy presunieme do spoločnej nadtriedy Shape:
class Shape { int x, y; // suradnice vyznacneho bodu utvaru (roh, stred, ...) public void move(int deltaX, int deltaY) { x += deltaX; y += deltaY; } } class Rectangle extends Shape { int width, height; // vyska a sirka // sem mozu prist metody pre obdlznik } class Circle extends Shape { int radius; // polomer // sem mozu prist metody pre kruh }
V rámci triedy možno používať aj premenné a metódy definované v nadtriede, ako keby boli jej vlastné. Výnimkou sú premenné a metódy s modifikátorom private a v prípade, že trieda a jej nadtrieda nepatria do rovnakého balíčka, aj premenné a metódy bez modifikátora (o tom neskôr). Napríklad v metódach triedy Circle tak môžeme používať premenné x, y, ako aj metódu move.
class Rectangle extends Shape { int width, height; // vyska a sirka public Rectangle(int x, int y, int height, int width) { this.x = x; this.y = y; this.height = height; this.width = width; } } class Circle extends Shape { int radius; // polomer public Circle(int x, int y, int radius) { this.x = x; this.y = y; this.radius = radius; } public void print() { System.out.println("Stred: (" + x + "," + y + "). Polomer: " + radius + "."); } }
- Ak máme objekt deklarovaný ako Circle c, môžeme napríklad zavolať metódy c.move(1,1) alebo c.print(), prípadne použiť premenné c.x, c.y, c.radius (hoci v praxi je väčšinou žiadúce premenné v triede skryť modifikátorom private).
Dedenie a typy
- Premenná typu Shape môže obsahovať referenciu na objekt triedy Shape alebo jej ľubovoľnej podtriedy:
Circle c = new Circle(0,0,5); Shape s = c; // toto je korektne priradenie // c = s; // toto neskompiluje, kedze s nemusi byt kruh c = (Circle) s; // po pretypovani to uz skompilovat pojde; program ale moze padnut, ak s nie je instanciou Circle alebo null // Istejsi pristup je teda najprv overit, ci je s skutocne instanciou triedy Circle: if (s instanceof Circle) { c = (Circle) s; }
- Vďaka tejto črte možno rôzne typy útvarov spracúvať tým istým kódom. Napríklad nasledujúca funkcia dostane pole útvarov (môžu v ňom byť útvary rôznych typov) a posunie každý z nich o daný vektor (deltaX, deltaY):
static void moveAll(Shape[] shapes, int deltaX, int deltaY) { for (Shape x : shapes) { x.move(deltaX, deltaY); } }
- Cvičenie: čo vypíše nasledujúci kód?
Shape[] shapes = new Shape[2]; shapes[0] = new Rectangle(0,0,1,2); shapes[1] = new Circle(0,0,1); moveAll(shapes, 2, 2); for (Shape x : shapes) { if (x instanceof Circle) { System.out.println("Je to kruh."); Circle c = (Circle) x; c.print(); } if (x instanceof Shape) { System.out.println("Je to utvar."); } }
Dedenie a konštruktory
- Typickou úlohou konštruktora je správne nainicializovať objekt.
- Pri dedení si väčšinou každá trieda inicializuje „svoje” premenné.
- Napríklad krajší spôsob realizácie konštruktorov pre geometrické útvary je nasledovný: Shape inicializuje x a y, pričom napríklad Circle nechá inicializáciu x a y na Shape a inicializuje už len radius.
- Prvý príkaz konštruktora môže pozostávať z volania konštruktora predka pomocou kľúčového slova super (z angl. superclass, t.j. nadtrieda).
class Shape { int x, y; public Shape(int x, int y) { this.x = x; this.y = y; } // zvysok triedy Shape } class Rectangle extends Shape { int width, height; public Rectangle(int x, int y, int height, int width) { super(x,y); this.height = height; this.width = width; } // zvysok triedy Rectangle } class Circle extends Shape { int radius; public Circle(int x, int y, int radius) { super(x,y); this.radius = radius; } // zvysok triedy Circle }
- Ak nezavoláme konštruktor predka ručne, automaticky sa zavolá konštruktor bez parametrov, t.j. super(). To môže pri kompilovaní vyústiť v chybu v prípade, keď nadtrieda nemá definovaný konštruktor bez parametrov (či už explicitne jeho implementáciou, alebo implicitne tým, že sa neuvedie implementácia žiadneho konštruktora nadtriedy). Napríklad v horeuvedenom príklade je teda volanie konštruktora nadtriedy nutnou podmienkou úspešnej kompilácie.
- Výnimkou je prípad, keď sa na prvom riadku volá iný konštruktor tej istej triedy pomocou this(...) – vtedy sa volanie konštruktora nadtriedy nechá na práve zavolaný konštruktor.
Prekrývanie metód a polymorfizmus
Podtrieda môže prekryť (angl. override) niektoré zdedené metódy, aby sa chovali inak ako v predkovi.
Napríklad môžeme mať útvar Segment (úsečka), ktorý je zadaný dvoma koncovými bodmi a v metóde move treba posunúť oba. Metódu z predka môžeme zavolať pomocou super.move, ale nemusí to byť na prvom riadku a nemusí byť použitá vôbec:
class Segment extends Shape { int x2, y2; Segment(int x, int y, int x2, int y2) { super(x,y); this.x2 = x2; this.y2 = y2; } @Override public void move(int deltaX, int deltaY) { super.move(deltaX, deltaY); // volanie metody v predkovi x2 += deltaX; y2 += deltaY; } }
Anotácia @Override je nepovinná, ale odporúčaná. Ide o informáciu pre kompilátor, ktorou sa vyjadruje snaha o prekrytie zdedenej metódy. Ak sa v predkovi nenachádza metóda s rovnakou hlavičkou, kompilátor vyhlási chybu. Tým sa dá predísť obzvlášť nepríjemným chybám.
S prekrývaním metód súvisí polymorfizmus, pod ktorým sa v programovaní (hlavne pri OOP) rozumie schopnosť metód chovať sa rôzne:
- S určitou formou polymorfizmu sme sa už stretli, keď sme mali viacero metód s rovnakým menom, avšak s rôznymi typmi parametrov (tzv. preťažovanie metód, angl. overloading).
- Pri dedení sa navyše môže metóda chovať rôzne v závislosti od triedy, ku ktorej táto metóda patrí.
- To, ktorá verzia metódy sa zavolá, záleží od toho, akého typu je objekt, nie akého typu je referencia naň.
Shape s = new Segment(0,0,1,-5); s.move(1,1); // zavola prekrytu metodu z triedy Segment s = new Circle(0,0,1); s.move(1,1); // zavola metodu z triedy Shape, lebo v Circle nie je prekryta Shape[] shapes = new Shape[3]; // vypln pole shapes //... for(Shape x : shapes) { x.move(deltaX, deltaY); // kazdy prvok sa posuva svojou metodou move, ak ju ma }
Vo všeobecnosti sa pri volaní o.f(par1,...,parn) pre objekt o typu T aplikuje nasledujúci princíp:
- Ak má trieda T svoju implementáciu metódy f s vhodnými parametrami, vykoná sa táto verzia metódy.
- V opačnom prípade sa vhodná verzia metódy f hľadá v nadtriede triedy T, v prípade neúspechu v nadtriede nadtriedy T, atď.
Polymorfizus môže byť schovaný aj hlbšie – neprekrytá metóda z predka môže vo svojom tele volať prekryté metódy, čím sa jej správanie mení v závislosti od typu objektu:
class SuperClass { void doX() { System.out.println("doX in Super"); } void doXTwice() { doX(); doX(); } } class SubClass extends SuperClass { void doX() { System.out.println("doX in Sub"); } } // v metode main: SuperClass a = new SubClass(); a.doXTwice(); // vypise 2x doX in Sub
Zmysluplnejší príklad bude poskytovať metóda printArea v príklade nižšie.
Abstraktné triedy a metódy
Aby sa metóda chovala v určitej skupine tried polymorfne, musí byť definovaná v ich spoločnej nadtriede. V tejto nadtriede však nemusí existovať jej zmysluplná implementácia.
- Uvažujme napríklad metódu area(), ktorá zráta plochu geometrického útvaru.
- Pre triedy Rectangle, Circle, resp. Segment je implementácia takejto metódy zrejmá. Zmysluplná implementácia v ich spoločnej nadtriede Shape by však bola prinajmenšom problematická.
Vzniknutú situáciu možno riešiť nasledovne:
- Metódu area() v triede Shape, ako aj triedu Shape samotnú, označíme za abstraktnú modifikátorom abstract.
- Abstraktná metóda pozostáva iba z hlavičky bez samotnej implementácie.
- Abstraktná trieda je trieda, ktorá môže obsahovať abstraktné metódy. Zo zrejmých dôvodov z nej nemožno tvoriť inštancie (napríklad v našom príklade by tieto inštancie „nevedeli, čo robiť” pri volaní metódy area()). Abstraktná trieda slúži iba na dedenie, stále však môže byť typom referencie na objekt.
- Podtriedy abstraktnej triedy, ktoré nie sú abstraktné, musia implementovať všetky abstraktné metódy svojho predka.
Príklad:
abstract class Shape { // ... public abstract double area(); public void printArea() { System.out.println("Plocha je " + area() + "."); } } class Rectangle extends Shape { // ... @Override public double area() { return width * height; } } class Circle extends Shape { // ... @Override public double area() { return Math.PI * radius * radius; } } class Segment extends Shape { // ... @Override public double area() { return 0; } }
Napríklad program
public static void main(String[] args) { Shape[] shapes = new Shape[3]; shapes[0] = new Rectangle(0,0,1,2); shapes[1] = new Circle(0,0,1); shapes[2] = new Segment(1,1,2,2); for (Shape x : shapes) { x.printArea(); } }
potom vypíše nasledujúci výstup:
Plocha je 2.0. Plocha je 3.141592653589793. Plocha je 0.0.
Hierarchia tried a trieda Object
- V Jave môže každá trieda dediť iba od jednej triedy (na rozdiel napríklad od C++, kde je možné dedenie od viacerých tried).
- Dedenie je však možné „viacúrovňovo”:
class Pes extends Zviera { } class Civava extends Pes { // hierarchia tried nemusi verne zodpovedat realite }
- Všetky triedy sú automaticky potomkami triedy Object; tá sa tiež považuje za priamu nadtriedu tried, ktoré explicitne nerozširujú žiadnu triedu.
- Trieda Object obsahuje metódy (napr. toString()), ktoré je často užitočné prekrývať.
Príklad:
- Nasledujúci kus kódu je o niečo elegantnejším spôsobom vypisovania geometrických útvarov, než pomocou metódy Circle.print:
class Circle extends Shape { // ... @Override public String toString() { return "Stred: (" + x + "," + y + "). Polomer: " + radius + "."; } // ... } // ... // V metode main: Circle c = new Circle(0, 0, 1); System.out.println(c.toString()); System.out.println(c); // ekvivalentne predchadzajucemu volaniu // ...
Rozhrania
Rozhranie (interface) je podobným konceptom ako abstraktná trieda. Existuje však medzi nimi niekoľko rozdielov, z ktorých najpodstatnejšie sú tieto:
- Rozhranie nemôže obsahovať konštruktory, ani iné ako finálne premenné.
- Rozhranie slúži predovšetkým ako zoznam abstraktných metód – kľúčové slovo abstract tu netreba uvádzať. Naopak implementované nestatické metódy musia byť označené kľúčovým slovom default.
- Kým od tried sa dedí pomocou kľúčového slova extends, rozhrania sa implementujú pomocou kľúčového slova implements. Rozdiel je predovšetkým v tom, že implementovať možno aj viacero rozhraní. Jedno rozhranie môže navyše rozširovať iné (dopĺňať ho o ďalšie požadované funkcie): v takom prípade používame kľúčové slovo extends.
- Všetky položky v rozhraní sa chápu ako verejné (public).
Príklad použitia:
interface Stack { void push(int item); int pop(); } interface Printable { void print(); } class LinkedStack implements Stack, Printable { static class Node { private int data; private Node next; public Node(int data, Node next) { this.data = data; this.next = next; } public int getData() { return data; } public Node getNext() { return next; } } private Node top; @Override public void push(int item) { Node p = new Node(item, top); top = p; } @Override public int pop() { if (top == null) { return -1; } int result = top.getData(); top = top.getNext(); return result; } @Override public void print() { Node p = top; while (p != null) { System.out.print(p.getData() + " "); p = p.getNext(); } System.out.println(); } } class ArrayStack implements Stack, Printable { private int[] a; private int n; public ArrayStack() { a = new int[100]; n = 0; } @Override public void push(int item) { a[n] = item; n++; } @Override public int pop() { if (n <= 0) { return -1; } n--; return a[n]; } @Override public void print() { for (int i = 0; i <= n-1; i++) { System.out.print(a[i] + " "); } System.out.println(); } } class Blabol implements Printable { @Override public void print() { System.out.println("Blabla"); } } public class InterfaceExample { static void fillStack(Stack stack) { stack.push(10); stack.push(20); } static void printTwice(Printable what) { what.print(); what.print(); } public static void main(String[] args) { LinkedStack s1 = new LinkedStack(); Stack s2 = new ArrayStack(); Blabol b = new Blabol(); fillStack(s1); fillStack(s2); printTwice(s1); //printTwice(s2); // s2 je Stack a nevie, ze sa vie vypisat printTwice((ArrayStack) s2); printTwice(b); } }
Prehľad niektorých modifikátorov tried, premenných a metód
Modifikátory prístupu:
- public: triedy, rozhrania a ich súčasti prístupné odvšadiaľ.
- (žiaden modifikátor): viditeľnosť len v rámci balíčka (package).
- protected: viditeľnosť v triede, jej podtriedach a v rámci balíčka.
- private: viditeľnosť len v danej triede.
Iné modifikátory:
- abstract: neimplementovaná metóda alebo trieda s neimplementovanými metódami.
- final:
- Ak je trieda final, nedá sa z nej ďalej dediť.
- Ak je metóda final, nedá sa v podtriede prekryť.
- Ak je premenná alebo parameter final, ide o konštantu, ktorú nemožno meniť.
- static:
- Statické premenné a metódy sa týkajú celej triedy, nie konkrétnej inštancie.
- Statické triedy vo vnútri inej triedy nie sú viazané na jej konkrétnu inštanciu.
Aritmetický strom s využitím dedenia
V minulom semestri sme upozorňovali na návrhový nedostatok pri realizácii aritmetického stromu: niektoré položky uložené v struct-och sa využívali len v niektorých uzloch stromu (hodnoty iba v listoch a operátory iba vo vnútorných uzloch). Tomuto sa vieme vyhnúť pomocou dedenia.
- Jednotlivé typy vrcholov budú podtriedy abstraktnej triedy Node
- Namiesto použitia príkazu switch na typ vrchola tu prekryjeme potrebné funkcie, napríklad evaluate.
abstract class Node { public abstract int evaluate(); } abstract class NularyNode extends Node { } abstract class UnaryNode extends Node { Node child; public UnaryNode(Node child) { this.child = child; } } abstract class BinaryNode extends Node { Node left; Node right; public BinaryNode(Node left, Node right) { this.left = left; this.right = right; } } class Constant extends NularyNode { int value; public Constant(int value) { this.value = value; } @Override public int evaluate() { return value; } @Override public String toString() { return Integer.toString(value); } } class UnaryMinus extends UnaryNode { public UnaryMinus(Node child){ super(child); } @Override public int evaluate() { return -child.evaluate(); } @Override public String toString() { return "(-" + child.toString() + ")"; } } class Plus extends BinaryNode { public Plus(Node left, Node right) { super(left,right); } @Override public int evaluate() { return left.evaluate() + right.evaluate(); } @Override public String toString() { return "(" + left.toString() + "+" + right.toString() + ")"; } } public class Expressions { public static void main(String[] args) { Node expr = new Plus(new UnaryMinus(new Constant(2)), new Constant(3)); System.out.println(expr); System.out.println(expr.evaluate()); } }
Odkazy
Cvičenia 15
Aritmetický strom
- Na testovači je bonusový príklad [4]
Progression
- Nižšie je uvedený kód abstraktnej triedy Progression, ktorá predstavuje celočíselnú postupnosť a dokáže jeden za druhým generovať jej členov, pričom si v premennej index pamätá index posledne vygenerovaného členu. Jediná public metóda tejto triedy je print, ktorá vypíše zadaných počet prvkov na obrazovku.
- Všimnite si, že premenná index je pre podtriedy tejto triedy "read-only", lebo jej hodnotu môžu zistiť pomocou metódy getIndex, ale nemôžu ju meniť.
- Napíšte triedu ArithmeticProgression, ktorá bude podtriedou Progression a bude reprezentovať aritmetickú postupnosť, ktorá je v konštruktore zadaná nultým prvkom a rozdielom medzi dvoma nasledujúcimi prvkami.
- Ak do main dáme Progression ap = new ArithmeticProgression(1, 3); ap.print(10);, program by mal vypísať 1 4 7 10 13 16 19 22 25 28
- Stačí implementovať konštruktor a currentValue()
- Napíšte triedu FibonacciProgression, ktorá bude reprezentovať Fibonacciho postupnosť, ktorá má pre účely tohto cvičenia nultý prvok 1, prvý prvok 1 a každý ďalší prvok je súčtom predchádzajúcich dvoch. Prvý prvok sa dá tiež reprezentovať ako súčet nultého a fiktívneho mínus prvého s hodnotou nula.
- Ak do main dáme Progression fp = new FibonacciProgression(); fp.print(10);, program by mal vypísať 1 1 2 3 5 8 13 21 34 55.
- Implementujte konštruktor, currentValue, firstvalue, nextValue
- Nižšie nájdete aj implementáciu triedy ProgressionSum, ktorá reprezentuje postupnosť, ktorá vznikla ako súčet dvoch postupností, ktoré dostane v konštruktore.
- Ak do main dáme Progression ps = new ProgressionSum(fp, fp); ps.print(10);, chceli by sme dostať dvojnásobok Fibonacciho postupnosti, teda 2 2 4 6 10 16 26 42 68 110. Nie je to však tak. Prečo? Ako prebieha volanie nextValue() pre premennú triedy ProgressionSum? Aké všetky metódy sa volajú a v akom poradí?
- Zmeňte časť metódy main s vytvorením postupnosti ps tak, aby program mal požadované správanie.
Trieda Progression
package prog; /** Trieda reprezentujuca celociselnu postupnost. */ public abstract class Progression { /** Aktualny index prvku postupnosti. */ private int index; /** Konstruktor */ protected Progression() { index = 0; } /** Vrati aktualny index prvku postupnosti */ protected int getIndex() { return index; } /** Vrati hodnotu postupnosti pre aktualny index */ protected abstract int currentValue(); /** Restartuje index na 0 a vrati nulty prvok. */ protected int firstValue() { index = 0; return currentValue(); } /** Zvysi index o 1 a vrati aktualny prvok. */ protected int nextValue() { index++; return currentValue(); } /** Vypise prvych n prvkov postupnosti. */ public void print(int n) { System.out.print(firstValue()); for (int i = 1; i < n; i++) { System.out.print(" " + nextValue()); } System.out.println(); } public static void main(String[] args) { } }
Trieda ProgressionSum
class ProgressionSum extends Progression { Progression p1, p2; ProgressionSum(Progression p1, Progression p2) { this.p1 = p1; this.p2 = p2; } @Override protected int currentValue() { return p1.currentValue() + p2.currentValue(); } @Override protected int nextValue() { p1.nextValue(); p2.nextValue(); return super.nextValue(); } @Override protected int firstValue() { p1.firstValue(); p2.firstValue(); return super.firstValue(); } }
Prednáška 27
Oznamy
- DÚ6 do stredy 13.3. 22:00
- DÚ7 zvarejnená zajtra (OOP, dedenie). Začnite na nej pracovať skôr, nenechávajte si ju na poslednú chvíľu.
- Pozor, v stredu na cvičení (13.3.) sa bude písať rozcvička na tému OOP a dedenie
- Ďalšia rozcvička 20.3. (bude zahŕňať aj dnešné učivo)
- 27.3. cvičenia nebudú kvôli rektorskému voľnu
Výnimky
- Počas behu programu môže dôjsť k rôznym chybám a neobvyklým situáciám, napr.
- neexistujúci súbor, zlý formát súboru
- málo pamäte pri alokovaní polí, objektov
- adresovanie mimo hraníc poľa, delenie nulou, ...
- Doteraz sme v našich cvičných programoch ignorovali chyby
- Programy určené pre užívateľov a kritické programy, ktorých zlyhanie by mohlo spôsobiť škody, by sa s takýmito situáciami mali vedieť rozumne vyrovnať
- Ošetrovanie chýb bez požitia výnimiek
- Do návratového kódu funkcie musíme okrem samotnej hodnoty zakomponovať aj ohlasovanie chýb
- Po každom príkaze, ktorý mohol spôsobiť chybu, musíme existenciu chyby otestovať a vyrovnať sa s tým
- Vedie to k neprehľadným programom
Malý príklad s načítaním poľa
Príklad: pseudokód funkcie, ktorá načíta zo súboru číslo n, naalokuje pole a načíta do poľa n čísel:
funkcia readArray { otvor subor vstup.txt if (nepodarilo sa otvorit) { return chybovy kod } nacitaj cislo n if (nepodarilo sa nacitat n) { zatvor subor return chybovy kod } alokuj pole a velkosti n if (nepodarilo sa alokovat pole) { zatvor subor return chybovy kod } for (int i=0; i<n; i++) { nacitaj cislo a uloz do a[i] if (nepodarilo sa nacitat) { zatvor subor odalokuj pole return chybovy kod } } zatvor subor return naalokovane pole, beh bez chyby
- Premiešané príkazy, ktoré niečo robia a ktoré ošetrujú chyby
- Ľahko môžeme zabudnúť odalokovať pamäť alebo zavrieť súbor
- Volajúca funkcia musí analyzovať chybový kód, môže potrebovať rozlišovať napr. problémy so súborom a s pamäťou
- Chyba môže nastať aj pri zatváraní súboru
Jednoduché použite výnimiek v Jave
- Prepíšme náš predchádzajúci príklad s výnimkami
static int[] readArray(String filename) { Scanner s = null; int[] a = null; try { s = new Scanner(new File(filename)); int n = s.nextInt(); a = new int[n]; for (int i = 0; i < n; i++) { a[i] = s.nextInt(); } s.close(); return a; } catch (Exception e) { if (s != null) { s.close(); } e.printStackTrace(); return null; } }
- Využívame konštrukty try a catch.
- Do try bloku dáme príkazy, z ktorých niektorý môže zlyhať.
- Ak niektorý zlyhá a vyhodí výnimku, okamžite sa ukončí vykonávanie bloku try a pokračuje sa blokom catch. V bloku catch túto výnimku spracujeme, v našom prípade len debugovacím výpisom.
- Ak sa podarilo čísla načítať do poľa, metóda vráti pole, inak vráti null
Ako všelijako môže zlyhať
Rôzne príklady, ako môže táto metóda zlyhať:
- Príkazu na inicializáciu Scannera pošleme meno neexistujúceho súboru:
java.io.FileNotFoundException: vstup.txt (No such file or directory) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.<init>(FileInputStream.java:137) at java.util.Scanner.<init>(Scanner.java:653) at prog.Prog.readArray(Prog.java:17) at prog.Prog.main(Prog.java:10)
- V súbore sú nečíselné údaje:
java.util.InputMismatchException at java.util.Scanner.throwFor(Scanner.java:857) at java.util.Scanner.next(Scanner.java:1478) at java.util.Scanner.nextInt(Scanner.java:2108) at java.util.Scanner.nextInt(Scanner.java:2067) at prog.Prog.readArray(Prog.java:18) at prog.Prog.main(Prog.java:10)
- Ak nie je dosť pamäte na pole a (toto ani nie je Exception, ale Error, takže náš catch to nezachytil, pozri ďalej)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at prog.Prog.readArray(Prog.java:19) at prog.Prog.main(Prog.java:10)
- Ak je číslo n v súbore záporné
java.lang.NegativeArraySizeException at prog.Prog.readArray(Prog.java:19) at prog.Prog.main(Prog.java:10)
- Súbor končí skôr ako sa načíta n čísel
java.util.NoSuchElementException at java.util.Scanner.throwFor(Scanner.java:855) at java.util.Scanner.next(Scanner.java:1478) at java.util.Scanner.nextInt(Scanner.java:2108) at java.util.Scanner.nextInt(Scanner.java:2067) at prog.Prog.readArray(Prog.java:21) at prog.Prog.main(Prog.java:10)
- Dali by sa vyrobiť aj ďalšie prípady (napr. filename==null)
- V dokumentácii sa o každej metóde dočítame, aké výnimky produkuje za akých okolností
Rozpoznávanie typov výnimiek
- Možno by náš program mal rôzne reagovať na rôzne typy chýb, napr.:
- Chýbajúci súbor: vypýtať si od užívateľa nové meno súboru
- Zlý formát súboru: ukázať užívateľovi, kde nastala chyba, požiadať ho, aby ju opravil alebo zadal nové meno súboru
- Nedostatok pamäte: program vypíše, že operáciu nie je možné uskutočniť vzhľadom na málo pamäte
- Toto vieme spraviť, lebo výnimky patria do rôznych tried dedených z triedy Exception (prípadne z vyššej triedy Throwable)
- K jednému príkazu try môžeme mať viacero príkazov catch pre rôzne triedy výnimiek, každý chytá tú triedu a jej podtriedy
- Pri danej výnimke sa použije najvrchnejší catch, ktorý sa na ňu hodí
- Po blokoch try a catch môže nasledovať blok finally, ktorý sa vykoná vždy, bez ohľadu na to, či nastala výnimka a či sa nám ju podarilo odchytiť nejakým príkazom catch
- V tomto bloku môžeme napr. pozatvárať otvorené súbory a pod.
Jednoduchý príklad, ktorý vypíše rôzne hlášky pre rôzne typy chýb:
static int[] readArray(String filename) { Scanner s = null; int[] a = null; try { s = new Scanner(new File(filename)); int n = s.nextInt(); a = new int[n]; for (int i = 0; i < n; i++) { a[i] = s.nextInt(); } return a; } catch (FileNotFoundException e) { System.err.println("Subor nebol najdeny"); return null; } catch(java.util.NoSuchElementException e) { System.err.println("Zly format suboru"); return null; } catch(OutOfMemoryError e) { System.err.println("Nedostatok pamate"); return null; } catch(Throwable e) { System.err.println("Neocakavana chyba pocas behu programu"); return null; } finally { if (s != null) { s.close(); } } }
- catch pre java.util.NoSuchElementException chytí aj InputMismatchException, ktorá je jej podtriedou, takže zahŕňa prípady keď súbor nečakane končí, aj keď v ňom nie sú číslené dáta
- do tejto kategórie by sme chceli zaradiť aj prípad, kedy je n záporné, ale ten skončí na všeobecnej Throwable
- to vyriešime tým, že hodíme vlastnú výnimku (viď nižšie)
Prehľad tried z tohto príkladu, plus niektorých ďalších, ktoré sa často vyskytujú:
Object | |-- Throwable | |-- Error vážne systémové problémy | | | |-- VirtualMachineError | | | |-- OutOfMemoryError | |-- Exception | |-- IOException | | | |-- FileNotFoundException | |-- RuntimeException | |-- IndexOutOfBoundsException | |-- NegativeArraySizeException | |-- NoSuchElementException | | | |-- InputMismatchException | |-- NullPointerException
Hádzanie výnimiek, vlastné triedy výnimiek
- Výnimku vyhodíme príkazom throw, pričom musíme vytvoriť objekt nejakej vhodnej triedy, ktorá podtriedou Throwable
- V našom príklade pre záporné n môžeme vyhodiť objekt triedy java.util.NoSuchElementException, ktorý sa spracuje rovnako ako iné chyby s formátom súboru
int n = s.nextInt(); if(n<0) { throw new java.util.NoSuchElementException(); }
- Nie je to však elegantné riešenie, lebo táto trieda reprezentuje iný typ udalosti
- Môžeme si vytvoriť aj vlastnú triedu, ktorá v premenných môže mať uložené podrobnejšie informácie o chybe, ktorá nastala.
- Väčšinou to bude podtrieda triedy Exception
static class WrongFormatException extends Exception { private String filename; public WrongFormatException(String filename) { this.filename = filename; } @Override public String getMessage() { return "Zly format suboru " + filename; } }
Propagácia a zreťazenie výnimiek
- Ak vznikne výnimka v príkaze, ktorý nie je vo vnútri bloku try-catch, alebo ak jej typ nie je zachytený žiadnym príkazom catch, hľadá sa ďalší blok try-catch, napr. vo volajúcej metóde
- Ak výnimku nikto nechytí, program skončí s chybovým výpisom zásobníka
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at prog.Prog.readArray(Prog.java:19) at prog.Prog.main(Prog.java:10)
- Pri spracovaní výnimky v bloku catch je možné hodiť novú výnimku (trebárs vhodnejšieho typu)
- Metóda musí deklarovať všetky výnimky, ktoré hádže, alebo ktoré v nej môžu vzniknúť a ich nechytá
- Neplatí pre výnimky triedy RuntimeException a jej podtried a pre Throwable, ktoré nie sú výnimka (ale napr. Error)
Nasledujúci program si pýta meno súboru, až kým nenájde súbor, ktorý vie načítať
- V metóde readArray spracuje chyby týkajúce sa formátu súboru a hodí novú výnimku typu WrongFormatException.
- V metóde main spracuje WrongFormatException a FileNotFoundException tak, že sa znovu pýta meno súboru.
- Iné nečakané udalosti, napr. málo pamäte, koniec vstupu od užívateľa a pod. spôsobia ukončenie programu s chybovou hláškou.
public static void main(String[] args) { boolean fileRead = false; Scanner s = new Scanner(System.in); int[] a = null; while (!fileRead) { try { System.out.println("Zadaj meno suboru: "); String filename = s.next(); a = readArray(filename); fileRead = true; System.out.println("Dlzka pola je " + a.length); } catch (WrongFormatException e) { System.out.println(e.getMessage()); } catch (FileNotFoundException e) { System.out.println("Subor nebol najdeny."); } catch(Throwable e) { System.out.println("Neocakavana chyba."); System.exit(1); } } } static int[] readArray(String filename) throws WrongFormatException, FileNotFoundException { Scanner s = null; int[] a = null; try { s = new Scanner(new File(filename)); int n = s.nextInt(); if (n < 0) { throw new WrongFormatException(filename); } a = new int[n]; for (int i = 0; i < n; i++) { a[i] = s.nextInt(); } return a; } catch (java.util.NoSuchElementException e) { throw new WrongFormatException(filename); } finally { if (s != null) { s.close(); } } }
Zhrnutie
- Keď vznikne neočakávaná udalosť, môžeme ju signalizovať vyhodením výnimky pomocou príkazu throw.
- Výnimka je objekt triedy, ktorá je podtriedou Throwable
- Pri ošetrovaní sa nájde a vykoná najbližší vyhovujúci try ... catch blok obkolesujúci príkaz throw, v tej istej alebo niektorej volajúcej metóde, ďalej sa pokračuje za týmto blokom
- Blok finally sa vykoná vždy, keď je aj keď nie je výnimka a aj ak sa výnimku nepodarilo chytiť. Slúži na zatváranie súborov a iné upratovacie práce.
- Niektoré typy neodchytených výnimiek treba deklarovať v hlavičke funkcie.
Ďalšie informácie
- Dokumentácia k triedam Throwable, Exception
- Kapitola z tutoriálu
Generické programovanie
- V minulom semestri sme videli rôzne abstraktné dátové typy a dátové štruktúry, napr. zásobník, rad, slovník, spájaný zoznam,...
- V každej sme museli zadefinovať, akého typu dáta bude obsahovať
- Ak teda potrebujeme zásobník intov aj zásobník reťazcov, museli sme všetky funkcie písať dvakrát, čo prináša problémy
Zásobník dát typu Object
- Dedenie nám prináša jedno riešenie tohto problému - zadefinovať typ dát ako Object a nakoľko všetky triedy sú podtriedami tejto triedy, môžeme ich v zásobníku skladovať
class Node { private Object data; private Node next; public Node(Object data, Node next) { this.data = data; this.next = next; } public Object getData() { return data; } public Node getNext() { return next; } } class Stack { private Node front; public void push(Object data) { Node p = new Node(data, front); front = p; } public Object pop() { Object res = front.getData(); front = front.getNext(); return res; } }
Teraz môžeme do zásobníka dávať rôzne veci:
Stack s = new Stack(); s.push(null); s.push("Hello world!"); // String je potomok Object s.push(new int[4]); // pole sa tiez vie tvarit ako objekt int x = 4; s.push(x); // kompilator vytvori objekt typu Integer
Ale pozor, keď vyberáme zo zásobníka, majú typ Object, musíme ich teda pretypovať:
int y = (Integer)s.pop(); // ok int z = (Integer)s.pop(); // java.lang.ClassCastException
Pre pretypovaní teda môže dôjsť k chybe počas behu programu, radšej sme, keď chybu objaví kompilátor.
Zásobník ako generická trieda
- Lepšie riešenie je pomocou generického programovania (existuje aj v C++, viď prednáška 23)
- Zadefinujeme parametrický typ class Stack <T>, kde T je parameter reprezentujúci typ objektov, ktoré do zásobníka budeme dávať.
- V definícii triedy namiesto konkrétneho typu (napr. Object), použijeme parameter T
- Keď vytvárame nový zásobník, špecifikujeme typ T: Stack<Integer> s = new Stack<Integer>();
- Potom do neho môžeme vkladať objekty triedy Integer a jej podtried
class Node <T> { private T data; private Node <T> next; public Node(T data_, Node<T> next_) { data = data_; next = next_; } public T getData() { return data; } public Node <T> getNext() { return next; } } class Stack <T> { private Node<T> front; public void push(T data) { Node<T> p = new Node<T>(data, front); front = p; } public T pop() { T res = front.getData(); front = front.getNext(); return res; } }
Použitie zásobníka:
Stack<Integer> s = new Stack<Integer>(); s.push(new Integer(4)); s.push(5); Integer y = s.pop(); int z = s.pop();
V tom istom programe môžeme vytvoriť zásobníky veľa rôznych typov.
Skratka:
- namiesto Stack<Integer> s = new Stack<Integer>(); stačí písať Stack<Integer> s = new Stack<>();
- kompilátor z kontextu určí, že v <> má byť Integer
Generické metódy
Aj jednotlivé metódy môžu mať typový parameter, ktorý sa píše pred návratový typ.
Statická metóda, ktorá dostane zásobník s prvkami typu T a vyprázdni ho.
static <T> void emptyStack(Stack<T> s) { while(!s.isEmpty()) { s.pop(); } }
Stack<String> s = new Stack<String>(); s.push("abc"); Prog.<String>emptyStack(s); // alebo len emptyStack(s);
Statická metóda, ktorá dostane pole s prvkami typu E a naplní ho referenciami na prvok e.
static <E> void fillArray(E[] a, E e) { for(int i=0; i<a.length; i++) { a[i] = e; } }
Integer[] a = new Integer[3]; fillArray(a, 4); int[] b = new int[3]; //fillArray(b, 4); // E musí byť objekt, nie primitívny typ
Generické triedenie, rozhranie Comparable
Ukazovali sme si aj algoritmy na triedenie, aj tie pracovali s poľom konkrétneho typu.
- Triediacu funkciu môžeme spraviť generickú, potrebujeme však prvky porovnávať
- Operátory <, <= atď pracujú len s primitívnymi typmi
- Použijeme teda špeciálnu metódu compareTo špecifikovanú v rozhraní Comparable
- x.compareTo(y) vráti zápornú hodnotu, ak x<y, nulu ak x=y a kladnú hodnotu ak x>y
- potrebujeme, aby prvky poľa boli z triedy, ktorá implementuje toto rozhranie, čo zapíšeme ako <E extends Comparable>
Jednoduché generické triedenie vkladaním:
static <E extends Comparable> void sort(E[] a) { for (int i = 1; i < a.length; i++) { E prvok = a[i]; int kam = i; while (kam > 0 && prvok.compareTo(a[kam - 1]) < 0) { a[kam] = a[kam - 1]; kam--; } a[kam] = prvok; } } public static void main(String[] args) { Integer[] a = {3, 1, 2}; sort(a); for (int i = 0; i < a.length; i++) { System.out.println(a[i]); } }
Java Collections
- Java poskytuje štandardné triedy na mnohé často používané dátové štruktúry, používa generické programovanie
- Tutoriál
- Je dobré tieto triedy poznať a podľa potreby využívať
- Pochopením ich štruktúry si tiež môžeme precvičiť objektovo-orientované programovanie
- Na úvod si ukážeme malú ukážku, viac nabudúce
ArrayList
ArrayList sa podobá na vector z C++ (existuje aj trieda Vector)
- ide o štruktúru reprezentujúcu pole, ktoré rastie podľa potreby
- na koniec poľa pridávame metódou add(prvok), konkrétny prvok adresujeme metódou get(index), meníme cez set(index, hodnota), veľkosť poľa je size()
import java.util.ArrayList; ... ArrayList<Integer> a = new ArrayList<Integer>(); a.add(2); a.add(7); for (int i = 0; i < a.size(); i++) { System.out.println(a.get(i)); // vypiseme vsetky prvky pola a.set(i, -1); // a potom ich prepiseme na -1 }
LinkedList
LinkedList je obojsmerný spájaný zoznam, ktorý môžeme použiť napr. ako zásobník alebo rad
- Vie teda efektívne pridávať a uberať prvky z oboch koncov a tiež prejsť cez všetky prvky zoznamu pomocou iterátora.
- Hľadanie prvku na pozícii i niekde v strede zoznamu je pomalé.
LinkedList<Integer> a = new LinkedList<Integer>(); a.addFirst(2); // to iste ako push a.addLast(7); // to iste ako add for (ListIterator<Integer> it = a.listIterator(); it.hasNext(); ) { System.out.println(it.next()); } a.removeFirst(); // to iste ako pop a.removeLast();
Prehľad Collections
- Dátové štruktúry a algoritmy na základnú prácu so skupinami dát.
- Generické typy - môžeme vytvárať dátové štruktúry pre dáta rôznych typov.
- Definované pomocou rozhraní, jedno rozhranie (interface) môže mať viacero implementácií.
Vybrané triedy:
Rozhranie | Význam | Implementácie |
Collection | skupina objektov | |
- Set | množina, skupina bez opakujúcich sa objektov | HashSet |
-- SortedSet | množina s definovaným usporiadaním prvkov | TreeSet |
- List | postupnosť objektov s určitým poradím | ArrayList, LinkedList |
Map | slovník, asociatívne pole, mapuje kľúče na hodnoty | HashMap |
- SortedMap | slovník s definovaným usporiadaním kľúčov | TreeMap |
V metódach je dobré argumenty definovať najvšeobecnejším vhodným rozhraním alebo triedou.
- Napr. chceme spočítať súčet viacerých Integer-ov:
// tato metoda sa da pouzit iba na ArrayList public static Integer sum(ArrayList<Integer> a) { ... } // tato metoda sa da pouzit na hocijaku Collection (LinkedList, HashSet...) public static Integer sum(Collection<Integer> a) { ... }
Základné operácie pre Collection:
public interface Collection<E> extends Iterable<E> { int size(); boolean isEmpty(); boolean contains(Object element); boolean add(E element); // optional boolean remove(Object element); // optional void clear(); // optional Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); // a dalsie... }
- Metódy add a remove vracajú true, ak sa Collection zmenila a false, ak sa nezmenila.
- Metódy, ktoré menia Collection, sú nepovinné v tom zmysle, že musia byť zadefinované, ale môžu hádzať UnsupportedOperationException
Cvičenia 16
Na začiatku cvičenia riešte individuálne rozcvičku zadanú na testovači. Potom riešte ďalšie príklady z tohto cvičenia, ktoré nie sú bodované, takže ich môžete riešiť aj v skupinkách. Cieľom cvičenia je precvičiť si výnimky a generické programovanie.
Jednoduché výnimky
Nižšie nájdete program z prednášky, ktorý načítava zo súboru číslo n a n čísel do poľa, pričom neexistenciu súboru a jeho zlý formát rieši výnimkami. Od užívateľa opakovane vypýta meno súboru, až kým sa mu nepodarí súbor prečítať.
- Po načítaní čísel do poľa v metóde readArray overte metódou hasNext() triedy Scanner, že sa v súbore už nenachádzajú ďalšie čísla alebo iné nebiele znaky. Ak sa nachádzajú, hoďte tiež WrongFormatException.
- Zmeňte program tak, aby WrongFormatException v konštruktore dostala aj podrobnejší popis chyby formátu, ktorá bude napríklad "Nepodarilo sa načítať počet prvkov n", alebo "Nepodarilo sa načítať prvok i", kde namiesto znaku i dosadíte príslušné poradové číslo prvku, kde nastala chyba. V metóde getMessage potom túto podrobnejšiu správu vráťte.
- Návod: premennú i v metóde readArray zadefinujte už pred príkazom try a inicializujte na -1. V časti catch potom podľa aktuálnej hodnoty i viete zistiť, či sa for cyklus vôbec nezačal alebo na ktorom prvku zhavaroval.
Generická trieda Matrix
Naprogramujte generickú triedu Matrix, ktorá reprezentuje obdĺžnikovú maticu prvkov nejakého neznámeho typu E.
- Prvky matice si v tejto triede uložte do premennej typu ArrayList<ArrayList<E>> (dokumentácia k ArrayList).
- Napíšte konštruktor, ktorý vytvorí maticu zadaných rozmerov a vyplní ju zadaným prvkom typu E.
- Napíšte metódy, ktoré do matice pridajú nový stĺpec a nový riadok, vyplnený zadaným prvkom typu E.
- Napíšte metódu get, ktorá vráti prvok matice nachádzajúci sa na zadanom mieste
- Napíšte metódu set, ktorá na zadané miesto v matici zapíše zadaný prvok typu E
Výnimky v triede Matrix
- Čo sa stane, ak metódu get triedy Matrix zavoláte so súradnicami mimo rozsah matice?
Prepíšte metódy get a set tak, aby pri zlých súradniciach hádzali výnimku vašej vlastnej triedy MatrixIndexOutOfBoundsException.
- Výnimka tejto triedy by v metóde getMessage mala vrátiť reťazec obsahujúci obidve súradnice, ako aj obidva rozmery matice.
- Skúste dva spôsoby implementácie:
- v prvom odchytávajte vzniknuté výnimky a nahraďte ich svojou výnimkou
- v druhom otestujte vhodnosť indexov hneď na začiatku metódy a v prípade potreby vyhoďte vlastnú výnimku
Ešte Matrix, dedenie
Napíšte generickú triedu InfiniteMatrix, ktorá je podtriedou triedy Matrix a líši sa od nej v tom, že ak metóde get dáme súradnice mimo rozsah matice, vráti hodnotu null (a nevyhadzuje výnimku). Je to ako keby sme mali maticu nekonečnej veľkosti vyplnenú null-mi a v malom obdĺžniku s určitým počtom riadkov a stĺpcov máme nejaké uložené hodnoty, ktoré sa môžu líšiť od null.
Výnimky pre Scanner
Vytvorte triedu IntScanner, ktorá v konštruktore dostane meno súboru a okrem konštruktoru obsahuje public metódy hasNextInt, nextInt a close, ktoré robia zhruba to isté ako v triede Scanner. Avšak ak je v súbore nejaký reťazec, ktorý nie je možné interpretovať ako číslo, metóda nextInt vyhodí výnimku vašej novej triedy, ktorá v metóde getMessage vráti reťazec obsahujúci číslo riadku, na ktorom chyba nastala aj reťazec, ktorý nebolo možné ako číslo interpretovať. Trieda Scanner nám neumožňuje zistiť číslo riadku, preto budeme mať v tejto triede tri premenné:
- aktuálne číslo riadku
- premennú typu BufferedReader, v ktorej na začiatku otvoríme zadaný súbor a vždy keď treba, načítame z neho riadok, zvýšime počítadlo a vytvoríme novú inštanciu triedy Scanner, ktorá bude čítať čísla z toho riadku
- premennú triedy Scanner
Zdrojový kód pre prvý príklad
package prog; import java.io.*; import java.util.Scanner; class Prog { static class WrongFormatException extends Exception { private String filename; public WrongFormatException(String filename_) { filename = filename_; } @Override public String getMessage() { return "Zly format suboru " + filename; } } public static void main(String[] args) { boolean fileRead = false; Scanner s = new Scanner(System.in); int[] a = null; while (!fileRead) { try { System.out.print("Zadaj meno suboru: "); String filename = s.next(); a = readArray(filename); fileRead = true; System.out.println("Dlzka pola je " + a.length); } catch (WrongFormatException e) { System.out.println(e.getMessage()); } catch (FileNotFoundException e) { System.out.println("Subor nebol najdeny."); } catch (Throwable e) { System.out.println("Neocakavana chyba."); System.exit(1); } } } static int[] readArray(String filename) throws WrongFormatException, FileNotFoundException { Scanner s = null; int[] a = null; try { s = new Scanner(new File(filename)); int n = s.nextInt(); if (n < 0) { throw new WrongFormatException(filename); } a = new int[n]; for (int i = 0; i < n; i++) { a[i] = s.nextInt(); } return a; } catch (java.util.NoSuchElementException e) { throw new WrongFormatException(filename); } finally { if (s != null) { s.close(); } } } }
Prednáška 28
Oznamy
- DÚ7 do budúcej stredy, neodkladajte na poslednú chvíľu
- Ďalšia rozcvička bude túto stredu (výnimky, generické programovanie)
- O týždeň 27.3. nebudú cvičenia kvôli rektorskému voľnu, zverejníme však nejaké bonusové úlohy
- V stredu 3.4. o 18:10 v posluchárni B bude prvý test.
- Bude pokrývať základy Javy, OOP, dedenie a výnimky (prednášky 24-27). Navyše bude jeden príklad na rekurzívnu prácu so stromami (zopakujte si minulosemestrové prednášky 19-21, ale budeme pracovať v Jave)
Opakovanie: generické programovanie
- V Jave môžeme definovať triedu alebo metódu, ktorá má špeciálny parameter určujúci typ dát, ktoré bude spracovávať, napr. zásobník s prvkami typu T.
class Stack <T> { private Node<T> front; public void push(T data) { Node<T> p = new Node<T>(data, front); front = p; } public T pop() { T res = front.getData(); front = front.getNext(); return res; } }
Použitie zásobníka:
Stack<Integer> s = new Stack<Integer>(); s.push(new Integer(4)); s.push(5); Integer y = s.pop(); int z = s.pop();
Výhody generickej verzie zásobníka:
- V tom istom programe môžeme vytvoriť zásobníky veľa rôznych typov.
- Kompilátor skontroluje, či vkladáme a vyberáme prvky správnych typov.
Úvod do Java Collections
- Dátové štruktúry a algoritmy na základnú prácu so skupinami dát.
- Generické typy - môžeme vytvárať dátové štruktúry pre dáta rôznych typov.
- Definované pomocou rozhraní, jedno rozhranie (interface) môže mať viacero implementácií.
Prehľad Collections
- Dátové štruktúry a algoritmy na základnú prácu so skupinami dát.
- Generické typy - môžeme vytvárať dátové štruktúry pre dáta rôznych typov.
- Definované pomocou rozhraní, jedno rozhranie (interface) môže mať viacero implementácií.
Vybrané triedy:
Rozhranie | Význam | Implementácie |
Collection | skupina objektov | |
- Set | množina, skupina bez opakujúcich sa objektov | HashSet |
-- SortedSet | množina s definovaným usporiadaním prvkov | TreeSet |
- List | postupnosť objektov s určitým poradím | ArrayList, LinkedList |
Map | slovník, asociatívne pole, mapuje kľúče na hodnoty | HashMap |
- SortedMap | slovník s definovaným usporiadaním kľúčov | TreeMap |
V metódach je dobré argumenty definovať najvšeobecnejším vhodným rozhraním alebo triedou.
- Napr. chceme spočítať súčet viacerých Integer-ov:
// tato metoda sa da pouzit iba na ArrayList public static Integer sum(ArrayList<Integer> a) { ... } // tato metoda sa da pouzit na hocijaku Collection (LinkedList, HashSet...) public static Integer sum(Collection<Integer> a) { ... }
Základné operácie pre Collection:
public interface Collection<E> extends Iterable<E> { int size(); boolean isEmpty(); boolean contains(Object element); boolean add(E element); // optional boolean remove(Object element); // optional void clear(); // optional Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); // a dalsie... }
- Metódy add a remove vracajú true, ak sa Collection zmenila a false, ak sa nezmenila.
- Metódy, ktoré menia Collection, sú nepovinné v tom zmysle, že musia byť zadefinované, ale môžu hádzať UnsupportedOperationException
Prechádzanie cez prvky Collection
Použitie cyklu for-each:
public static Integer sum(Collection<Integer> a) { int sum = 0; for(Integer x : a) { sum += x; } return sum; }
- cyklus for-each sa dá použiť na ľubovoľný objekt triedy implementujúcej rozhranie Iterable, ktoré definuje metódu Iterator<T> iterator() (plus ďalšie nepovinné)
Použitie iterátora:
public static Integer sum(Collection<Integer> a) { int sum = 0; for (Iterator<Integer> it = a.iterator(); it.hasNext();) { sum += it.next(); } return sum; }
- a.iterator() vráti objekt it implementujúci rozhranie Iterator
- it.next() vráti ďalší prvok zo skupiny a, alebo hodí NoSuchElementException, ak už ďalší nie je
- it.hasNext() vráti, či ešte je ďalší prvok
- Užitočná predstava je, že iterátor vždy ukazuje na "medzeru" medzi dvoma prvkami (prípadne pred prvým alebo za posledným prvkom)
- next() preskočí do ďalšej medzery a vráti preskočený prvok
- Poradie, v akom prvky navštívime, nie je pre všeobecnú Collection definované
- Iterátor pre SortedSet vracia prvky v utriedenom poradí od najmenšieho po najväčší.
- Iterátor pre List vracia prvky v poradí, v akom sú v postupnosti (poli, zozname)
Pozn: Rozhranie List definuje ListIterator, ktorý rozširuje základný iterátor (pohyb oboma smermi, pridávanie prvkov atď, užitočné pre prácu s LinkListom).
Cvičenie Nasledujúci program má vypísať všetky kombinácie hodnôt zo zoznamov a a b (t.j. A1,A2,B1,B2,C1,C2), ale nefunguje správne a padá na java.util.NoSuchElementException. Prečo? Ako chybu opraviť?
import java.util.*; public class Pokus { public static void main(String[] args) { LinkedList<String> a = new LinkedList<String>(); a.add("A"); a.add("B"); a.add("C"); LinkedList<Integer> b = new LinkedList<Integer>(); b.add(1); b.add(2); for (Iterator<String> i = a.iterator(); i.hasNext(); ) { for (Iterator<Integer> j = b.iterator(); j.hasNext(); ) { System.out.println(i.next() + j.next()); } } } }
Použitie Map
public interface Map<K,V> { V put(K key, V value); // klucu key prirad hodnotu value, vrati predch. hodnotu pre key V get(Object key); // hodnota pre kluc key alebo null V remove(Object key); // zmaz kluc key a jeho hodnotu boolean containsKey(Object key); // obsahuje kluc key? boolean containsValue(Object value); int size(); boolean isEmpty(); void putAll(Map<? extends K, ? extends V> m); void clear(); // Vrátia Set alebo Collection, cez ktorý môžeme iterovať public Set<K> keySet(); public Collection<V> values(); public Set<Map.Entry<K,V>> entrySet(); // Interface pre dvojice vo výsledku entrySet public interface Entry { K getKey(); V getValue(); V setValue(V value); } }
Príklad použitia Map:
- vstup z konzoly rozložíme Scannerom na slová (kým užívateľ nezadá END) a počítame počet výskytov každého slova
import java.util.*; public class Prog { public static void main(String[] args) { Map<String, Integer> map = new HashMap<String, Integer>(); Scanner s = new Scanner(System.in); // inicializujeme Scanner while (s.hasNext()) { // kym neskonci vstup String word = s.next(); // nacitame slovo if (word.equals("END")) { // skoncili sme ak najdeme END break; } Integer freq = map.get(word); if(freq == null) { map.put(word, 1); } else { map.put(word, freq+1); } } System.out.println("Pocet roznych slov: " + map.size()); System.out.println(map); } }
Príklad výstupu:
one two three one two two END Pocet roznych slov:3 {two=3, one=2, three=1}
HashMap vypisuje prvky v ľubovoľnom poradí. Ak typ zmeníme na TreeMap, dostaneme utriedené podľa kľúča:
{one=2, three=1, two=3}
Ak chceme vypísať zoznam slov a ich frekvencií v inom formáte, použijeme for-each alebo iterátor (ďalšie možnosti na konci prednášky)
for(Map.Entry<String, Integer> e : map.entrySet()) { System.out.println("Slovo " + e.getKey() + " sa vyskytuje " + e.getValue() + " krat"); }
for(Iterator<Map.Entry<String,Integer>> it=map.entrySet().iterator(); it.hasNext(); ) { Map.Entry<String,Integer> e = it.next(); System.out.println("Slovo " + e.getKey() + " sa vyskytuje " + e.getValue() + " krat"); }
Slovo two sa vyskytuje 3 krat Slovo one sa vyskytuje 2 krat Slovo three sa vyskytuje 1 krat
Dôležité metódy z uložených objektov
Porovnávanie objektov na rovnosť: equals
- Metódy z Collection contains(Object element), remove(Object element) a ďalšie potrebujú porovnávať objekty na rovnosť.
- Operátor == porovnáva referencie, t.j. či sú dva objekty na tej istej adrese v pamäti
- Collection používa namiesto toho metódu equals(Object obj) definovanú v triede Object
- Metóda equals() v triede Object tiež porovnáva len referencie, ostatné triedy ju môžu prekryť
- Napr. v triedach ako String, Integer,... definovaná na porovnávanie reťazcov, čísel,...
- Rôzne triedy implementujúce Collection tiež väčšinou vedia porovnávať na rovnosť spúšťaním equals na jednotlivé prvky
- Metódy nevieme spúšťať na null, napr. contains(Object o) vracia true práve vtedy, keď nejaký prvok e Collection spĺňa (o==null ? e==null : o.equals(e))
- Prekrytá metóda equals by sa mala správať "rozumne", t.j. byť symetrická, tranzitívna a pod.
Porovnávanie objektov na nerovnosť: Comparable
- SortedMap a SortedSet potrebujú vedieť porovnávať prvky podľa veľkosti
- Používajú metódu compareTo definovanú v rozhraní Comparable (videli sme na minulej prednáške)
- Ak naša trieda neimplementuje toto rozhranie alebo chceme použiť iné usporiadanie, môžeme použiť vlastný komparátor, objekt implementujúci rozhranie Comparator
import java.util.*; public class SetSortedByAbsoluteValue { /** Trieda AbsoluteValueComparator porovnava Integery * podla absolutnej hodnoty */ static class AbsoluteValueComparator implements Comparator<Integer> { public int compare(Integer o1, Integer o2) { Integer x1 = Math.abs(o1); Integer x2 = Math.abs(o2); return x1.compareTo(x2); } } public static void main(String[] args) { AbsoluteValueComparator comp = new AbsoluteValueComparator(); TreeSet<Integer> set = new TreeSet<>(comp); set.add(-3); set.add(0); set.add(7); set.add(-10); for(Integer x : set) { // vypise 0 -3 7 -10 System.out.print(" " + x); } System.out.println(); } }
Hešovacie funkcie: hashCode
- HashSet a HashMap potrebujú vedieť prekódovať ľubovoľný objekt do pozície v poli
- Používajú metódu int hashCode() definovanú v triede Object
- Object jednoducho použije svoju adresu v pamäti ako svoj hashCode
- Štandardné triedy prekrývajú hashCode
- Ak prekryjete equals, treba prekryť aj hashCode, lebo ak sa dva prvky rovnajú v equals, majú mať rovnaký hashCode
static class Name { String givenName; String lastName; @Override public int hashCode () { return givenName.hashCode() + 31*lastName.hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Name other = (Name) obj; return this.givenName.equals(other.givenName) && this.lastName.equals(other.lastName); } }
Algoritmy
- Triedy Collections a Arrays obsahujú statické metódy na prácu s Collections a poliami
- Napr. sort, shuffle (náhodne preusporadaj), reverse, fill, copy, swap, binarySearch, min, max,...
Collections: Zhrnutie
- Collections sú obrovská knižnica a veľmi užitočná
- Neváhajte ich používať v programoch, nájdite si v dokumentácii metódy, ktoré sa vám hodia
- Mali by ste ovládať (s pomocou ťaháka) aspoň základy práce s ArrayList, LinkedList, HashMap a s iterátormi
- Pri spracovaní väčších dát pozor na to, že niektoré metódy sú pomalé
- Pre prácu s Collections môže byť potrebné prekryť niektoré metódy z Object (equals, hashCode)
- Ďalšie metódy z Object, ktoré sa často hodí prekryť sú clone a toString
Vnorené a anonymné triedy
- Java umožňuje definovať triedu v inej triede alebo dokonca v metóde
- To umožnuje ju uložiť tam, kde logicky patrí, kde sa používa a prípadne zamedziť prístup iným častiam programu
- Tu len základný prehľad, viac detailov na Programovaní (3) alebo samoštúdium
- Trieda definovaná v inej triede môže byť statická alebo nestatická
Statická vnorená trieda (static nested class)
- Správa sa podobne ako keby bola definovaná mimo triedy
public class A { // premenné, metódy public static class B { // premenné, metódy } // použitie v triede A: B objekt = new B(); } // použitie v inej triede: A.B objekt = new A.B();
Vnútorná trieda, t.j. nestatická vnorená trieda (inner class)
- Inštancie vnútornej triedy majú prístup k premenným vonkajšej triedy
Lokálna trieda (local class)
- Podobne ako vnútorná trieda, ale definovaná vo vnútri metódy, priamo prístupná (pod svojim menom) len tam
- Ale inštancie sa dajú použiť aj mimo metódy
- V príklade nižšie metóda iterator obsahuje definíciu triedy MyIterator, ktorá implementuje rozhranie Iterator<Integer>
- Metóda iterator vráti objekt triedy MyIterator metóde main a tá ho implicitne použije vo for cykle, aj keď metóda iterator už skončila
import java.util.Iterator; public class MyArray implements Iterable<Integer> { private Integer [] array; /** Konstruktor vytvori pole dlzky n a naplni ho cislami 10,20,... */ public MyArray(int n) { array = new Integer[n]; for (int i = 0; i < n; i++) { array[i] = (i + 1) * 10; } } /** Metoda vrati iterator cez pole */ public Iterator<Integer> iterator() { class MyIterator implements Iterator<Integer> { private int index; // poloha v poli private MyIterator() { // konstruktor iniocializuje index index = 0; } public Integer next() { index++; return array[index - 1]; // mozeme pouzit premennu array z MyArray } public boolean hasNext() { return index < array.length; } } return new MyIterator(); } public static void main(String[] args) { MyArray a = new MyArray(5); for (Integer x : a) { System.out.println(x); } // alebo ekvivalentne: for (Iterator<Integer>it = a.iterator(); it.hasNext();) { System.out.println(it.next()); } } }
Anonymná trieda (anonymous class)
- Definuje sa v nejakej metóde, pričom sa jej ani nepriradí meno, iba sa vytvorí inštancia
- Tu je metóda iterator z predchádzajúceho príkladu prepísaná s anonymnou triedou:
/** Metoda vrati iterator cez pole */ public Iterator<Integer> iterator() { return new Iterator<Integer>() { // zaciatok anonymnej triedy private int index = 0; // poloha v poli, priamo inicializovana public Integer next() { index++; return array[index-1]; } public boolean hasNext() { return index < array.length; } }; // bodkociarka za prikazom return }
- Za príkaz new sme dali meno rozhrania (Iterator<Integer>), za tým prázdne zátvorky pre "konštruktor", potom definíciu triedy a bodkočiarku
- Nie je možné písať konštruktory, ale môžeme inicializovať premenné, napr. private int index = 0;
Premenné a parametre z metódy v lokálnych a anonymných triedach
- Lokálna alebo anonymná trieda môže používať aj lokálne premenné a parametre metódy, v ktorej sa nachádza
- Ale pozor, iba ak sú definované ako final alebo sa do nich po inicializácii nič nepriradzuje
- Ak však ide o referenciu na objekt alebo pole, obsah poľa resp. premenných objektu je možné meniť, nemení sa iba referencia
- V nasledujúcej ukážke pridáme ďalší iterátor, ktorý sa neposúva o 1, ale o zadanú hodnotu jump
public class MyArray implements Iterable<Integer> { // sem prídu premenné a metódy z príkladu vyššie (konštruktor, iterator) public Iterator<Integer> jumpingIterator(Integer jump) { // jump = 3; // s týmto príkazom by to neskompilovalo return new Iterator<Integer>() { // zaciatok anonymnej triedy private int index = 0; // poloha v poli, priamo inicializovana public Integer next() { index += jump; return array[index-jump]; } public boolean hasNext() { return index < array.length; } }; // bodkociarka za prikazom return } public static void main(String[] args) { MyArray a = new MyArray(5); Iterator<Integer> it = a.jumpingIterator(2); while(it.hasNext()) { System.out.println(it.next()); } }
Metóda forEach v Iterable, lambda výrazy
Rozhranie Iterable definuje aj metódu forEach, ktorá ako argument dostane objekt implementujúci generické rozhranie Consumer. Toto rozhranie má jedinú metódu accept vracajúcu void.
Pripomeňme si vypísanie našeho slovníka s frekvenciami výskytu slov pomocou for-each cyklu
for (Map.Entry<String, Integer> e : map.entrySet()) { System.out.println("Slovo " + e.getKey() + " sa vyskytuje " + e.getValue() + " krat"); }
Pomocou metódy forEach a anonymnej triedy ho prepíšeme takto:
Consumer<Map.Entry<String, Integer>> printAll = new Consumer<Map.Entry<String, Integer>>() { public void accept(Map.Entry<String, Integer> e) { System.out.println("Slovo " + e.getKey() + " sa vyskytuje " + e.getValue() + " krat"); } }; map.entrySet().forEach(printAll);
- Tento kód však nie je príliš prehľadný.
- Namiesto anonymnej triedy s iba jednou metódou môžeme použiť lambda výraz (lambda expression)
- Telo metódy bez zvyšku triedy napíšeme priamo kde treba, t.j. ho môžeme priradiť do premennej printAll alebo priamo ako argument metódy forEach.
map.entrySet() .forEach(e -> System.out.println("Slovo " + e.getKey() + " sa vyskytuje " + e.getValue() + " krat"));
Map má tiež forEach, čo je ešte jednoduchšie:
map.forEach((key, value) -> System.out.println("Slovo " + key + " sa vyskytuje " + value + " krat"));
Vo všeobecnosti má lambda výraz tvar
(param1,param2) -> { doSomething(param1, param2); return somethingElse(param1, param2); } (param1,param2) -> someExpression(param1, param2); // vynecháme return param -> someExpression(param)
V Jave je tiež možnosť vykonať viacero operácií špecifikovaných lambda výrazmi na postupnostiach nazývaných Stream.
Cvičenia
- V našom príklade s počítaním frekvencií slov máme v štruktúre Map ako kľúče slová a hodnoty ich počty výskytov. Čo vypíšu nasledujúce dva kúsky kódu?
map.forEach((key, value) -> { if (key.length() > 2) { System.out.println("Slovo " + key + " sa vyskytuje " + value + " krat"); } }); ArrayList<String> words = new ArrayList<>(); map.forEach((key, value) -> { if (value > 1) { words.add(key); } }); System.out.println(words);
- V tejto prednáške sme videli príklad, ktorý udržiaval množinu utriedenú podľa absolútnej hodnoty čísla tak, že implementoval pomocnú statickú vnorenú triedu AbsoluteValueComparator. Prepíšte tento príklad tak, aby ste namiesto tejto triedy použili anonymnú triedu alebo lambda výraz.
Riešenie pre Comparator
Riadok s vytváraním premennej set zmeníme na:
TreeSet<Integer> set = new TreeSet<>((o1,o2)-> { Integer x1 = Math.abs(o1); Integer x2 = Math.abs(o2); return x1.compareTo(x2); });
alebo ešte kratšie
TreeSet<Integer> set = new TreeSet<>((o1,o2)-> ((Integer)Math.abs(o1)).compareTo(Math.abs(o2)) );
(potrebujeme pretypovať, lebo Math.abs vracia int, nie Integer.
Trieda AbsoluteValueComparator a riadok s vytváraním premennej comp sa vynechá. Ak aj na výpis použijeme lambda výraz a na pridanie prvkov do množiny metódu asList z triedy Arrays, celý program sa skráti takto:
import java.util.*; public class MySet { public static void main(String[] args) { // vytvoríme SortedSet utriedený podľa absolútnej hodnoty SortedSet<Integer> set = new TreeSet<>((o1, o2)-> ((Integer)Math.abs(o1)).compareTo(Math.abs(o2)) ); // pridáme doňho nejaké prvky set.addAll(Arrays.asList(-3, 0, 7, -10)); // vypíšeme usporiadané podľa absolútnej hodnoty set.forEach(x -> System.out.print(" " + x)); System.out.println(); } }
Ešte poznámky pre zvedavých: ak by sme chceli vynechať medzeru pred prvým číslom, môžeme skúsiť použiť premennú first, ktorá určuje, či ide o prvé číslo:
boolean first = true; set.forEach(x -> { if(!first) System.out.print(" "); System.out.print(x); first = false; });
To však neskompiluje, lebo premennú first nemôžeme meniť (chyba local variables referenced from a lambda expression must be final or effectively final). Ako jednoduchý trik môžeme použiť pole booleanov dĺžky 1:
boolean[] first = {true}; set.forEach(x -> { if (!first[0]) System.out.print(" "); System.out.print(x); first[0] = false; });
Kompilátor je spokojný, lebo first je referencia na pole a tá sa nemení, mení sa len obsah poľa.
Cvičenia 17
Na začiatku cvičenia riešte individuálne rozcvičku zadanú na testovači. Potom riešte ďalšie príklady z tohto cvičenia, ktoré nie sú bodované, takže ich môžete riešiť aj v skupinkách. Cieľom cvičenia je precvičiť si Collections.
Index riadkov
Na prednáške sme mali program, ktorý pomocou HashMap<String,Integer> spočítal počty výskytov slov na vstupe. Teraz chceme zmenenú verziu tohto programu, ktorá vypíše zoznam slov a pre každé slovo zoznam čísel riadkov, na ktorých sa toto slovo nachádza. Slová budú usporiadané podľa abecedy, riadky vzostupne. Ak je slovo na riadku viackrát, uvedie sa toto číslo iba raz. Odporúčame použiť TreeMap<String,TreeSet<Integer>>. Kostra nižšie už rozkladá vstup na riadky a riadky na slová.
Príklad vstupu a výstupu (vstup sú prvé tri riadky):
jeden dva tri jeden jeden styri tri Slovo dva sa vyskytuje na riadkoch: 1 Slovo jeden sa vyskytuje na riadkoch: 1 2 Slovo styri sa vyskytuje na riadkoch: 2 Slovo tri sa vyskytuje na riadkoch: 1 2
Kostra programu:
import java.util.*; import java.io.*; public class Prog { public static void main(String[] args) throws IOException { //TODO: VAS KOD NA INICIALIZACIU MAPY TU BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); int lineNumber = 0; // pocitadlo cisel riadkov while (true) { // nacitame riadok do retazca String line = in.readLine(); // skoncime, ked uzivatel zada prazdny riadok // alebo ked prideme na koniec vstupu (null) if (line == null || line.equals("")) { break; } lineNumber ++; // inicializujeme scanner, ktory rozlozi riadok na slova Scanner scanner = new Scanner(line); while(scanner.hasNext()) { String word = scanner.next(); // TODO VAS KOD NA SPRACOVANIE SLOVA TU } } // TODO: VAS KOD NA VYPIS VYSLEDKU TU } }
Metódy equals, hashCode, compareTo
Napíšte triedu Zlomok, ktorá bude implementovať zlomok s celočíselným čitateľom a menovateľom.
- Triede spravte konštruktor a preťažte equals tak, aby správne testovala rovnosť zlomkov a hashCode tak, aby bol konzistentný s equals.
- Vaša trieda by tiež mala implementovať interface Comparable s bežným porovnaním zlomkov podľa veľkosti.
- Prekryte aj metódu toString aby vrátila reťazec typu "a/b"
- Skúšajte zlomky vkladať do TreeSet a HashSet a skontrolujte, že dostávate správne výsledky.
- Pre jednoduchosť môžete predpokladať, že čitateľ aj menovateľ sú kladné a že nedôjde k pretečeniu rozsahu čísla typu Integer pri aritmetických operáciách.
- Môže sa vám zísť Euklidov algoritmus na nájdenie najväčšieho spoločného deliteľa:
// ratame nsd(a,b) while(b != 0) { int x = a % b; a = b; b = x; } // vysledok je v premennej a
Generické statické metódy
- Napíšte generickú statickú metódu prienik, ktorá dostane dve SortedSet (s tým istým typom prvkov E) a vráti SortedSet obsahujúcu ich prienik, t.j. prvky, ktoré sa nachádzajú v oboch.
- Najskôr funkciu naprogramujte pomocou iterácie cez jednu vstupnú množinu a volaním contains na druhú.
- Komplikovanejší ale efektívnejší program dostanete použitím iterátorov na obe množiny, ktoré budete postupne posúvať algoritmom podobným na merge (iterátory pre SortedSet vracajú prvky v utriedenom poradí).
- Potrebujete si tiež pamätať aktuálny prvok z každej množiny, lebo po volaní next sa už späť k tomu istému prvku nedostanete.
Metóda remove
Trieda ArrayList (ale aj LinkedList a iné triedy implementujúce List) má dve metódy s menom remove, ale s iným typom parametra
- remove(int index) zmaže prvok na pozícii index
- remove(Object o) zmaže prvý výskyt prvku o
Úloha:
- Zistite experimentovaním, ktorá z metód remove sa vykoná v nasledujúcom kóde.
- Zmeňte kód tak, aby sa zavolala opačná forma metódy remove.
Pozor, podobná zámena metód môže byť zdrojom zákernej chyby v programe.
import java.util.ArrayList; public class Prog { public static void main(String[] args) { Integer[] tmp = {3,2,1}; ArrayList<Integer> a = new ArrayList<Integer>(Arrays.asList(tmp)); System.out.println("Pred remove:" + a); a.remove(1); System.out.println("Po remove: " + a); } }
Prednáška 29
Oznamy
- Domácu úlohu č. 7 je potrebné odovzdať do stredy 27. marca, 22:00.
- Cvičenie v stredu 27. marca nebude (rektorské voľno).
- V stredu 3. apríla bude o 18:10 v posluchárni B prvá písomka.
- Najbližšia rozcvička bude na cvičení v stredu 17. apríla.
Testovanie programov
- Cieľom testovania je nájsť chyby v programe, teda preukázať, že program nefunguje podľa špecifikácie (aby sme potom vedeli chybu nájsť a opraviť).
- Test pozostáva zo vstupu, správneho výstupu a popisu jeho významu.
- Program sa spustí na vstupe a jeho výsledok sa porovná so správnou odpoveďou.
- Tradičný prístup: najprv sa napíše kód, potom sa vytvárajú testy.
- Test-driven development: najprv sa napíšu testy, potom sa programuje kód, ktorý ich dokáže splniť.
Black-box testovanie
Pod black-box testovaním sa rozumie prístup, pri ktorom sa sada testov vytvorí len na základe špecifikácie programu. V testoch sa pritom snažíme zachytiť okrajové aj typické prípady.
Uvažujme napríklad nasledujúcu neformálnu špecifikáciu metódy remove:
/** Z pola a vyhodi prvy vyskyt objektu rovneho x * pricom rovnost sa testuje metodou equals. * Vsetky dalsie prvky posunie o jedno dolava a na koniec * pola da null. * Vrati true, ak bolo pole modifikovane, inak false. * Ak a je null alebo x je null, vyhodi java.lang.NullPointerException. */ public static boolean remove(Object[] a, Object x);
K nej môžeme zhotoviť napríklad nasledujúcu sadu testovacích vstupov:
- Prázdne pole a.
- Pole obsahujúce iba x.
- Pole obsahujúce x na začiatku.
- Pole obsahujúce x na konci.
- Pole obsahujúce x niekde v strede.
- Pole obsahujúce viacero kópií objektu x.
- Pole obsahujúce prvky null.
- Pole obsahujúce objekty rôznych typov.
- Veľmi dlhé pole.
- Prípad, keď a je rovné null.
- Prípad, keď x je rovné null.
Podrobnejšie rozpísanie jedného z testov:
- Vstup: a = {1,2,3}, x = 1.
- Výstup: a = {2,3,null}, návratová hodnota true.
- Význam testu: testovanie prípadu, keď pole a obsahuje x na začiatku.
White-box testovanie
Pod white-box testovaním sa naopak rozumie prístup, pri ktorom testy vytvárame na základe kódu; snažíme sa pritom preveriť všetky vetvy výpočtu.
- V cykle vyskúšame 0 iterácií, 1 iteráciu, maximálny počet iterácií.
- V podmienke vyskúšame vetvu true aj false.
- ...
Nevýhodou tohto prístupu však je, že sústredením sa na kód môžeme pozabudnúť na prípady, na ktoré sa v kóde nemyslelo. Napríklad nasledujúci kód nespĺňa úplne špecifikáciu:
/** Z pola a vyhodi prvy vyskyt objektu rovneho x * pricom rovnost sa testuje metodou equals. * Vsetky dalsie prvky posunie o jedno dolava a na koniec * pola da null. * Vrati true, ak bolo pole modifikovane, inak false. * Ak a je null alebo x je null, hodi java.lang.NullPointerException. */ public static boolean remove(Object[] a, Object x) { int i; for (i = 0; i <= a.length - 1; i++) { if (a[i].equals(x)) { break; } } if (i == a.length) { return false; } while (i <= a.length - 2) { a[i] = a[i + 1]; i++; } a[i] = null; return true; }
JUnit
- Systém JUnit umožňuje vytvárať špeciálne triedy obsahujúce testy iných tried.
- Sadu testov môžeme ľahko automaticky spustiť a vyhodnotiť, vidíme všetky výsledky.
- Dobrá podpora v Netbeans.
- Krátky návod: [5].
Príklad niekoľkých testov pre funkciu remove vyššie:
package prog; import org.junit.Test; import static org.junit.Assert.*; import java.util.*; public class ProgTest { @Test public void testEmpty() { // hladame x v poli dlzky nula Object[] working = new Object[0]; // vstupne pole Object x = new Object(); Object[] correct = new Object[0]; // spravna odpoved boolean result = Prog.remove(working, x); // spustime testovanu metodu assertEquals(result, false); // testujeme navratovu hodnotu assertTrue(Arrays.equals(working,correct)); // testujeme obsah pola po vykonani metody remove } @Test public void testXOnly() { // hladame x v poli obsahujucom iba x Object[] working = {7}; Object x = 7; Object[] correct = {null}; boolean result = Prog.remove(working, x); assertEquals(result, true); assertTrue(Arrays.equals(working,correct)); } @Test(expected = NullPointerException.class) public void testANull() { // Testujeme, ci hodi vynimku ked je pole null Object[] working = null; Object x = 7; boolean result = Prog.remove(working, x); } }
Tento príklad je možné rôzne vylepšovať:
- Opakujúce sa časti kódu môžeme dať do pomocných metód.
- Môžeme pridať výpisy výsledkov, aby sme v prípade chyby videli, čo sa stalo.
- Môžeme triede ProgTest pridať premenné, konštruktor, ako aj špeciálne metódy, ktoré sa vykonajú pred každým testom, prípadne po každom teste.
Úvod do JavaFX
JavaFX je (vo verzii Java SE 8) sada knižníc, ktorú možno využiť ako nástroj na tvorbu aplikácií s grafickým používateľským rozhraním (GUI). Namiesto konzolových aplikácií teda budeme v nasledujúcich niekoľkých prednáškach vytvárať aplikácie grafické (typicky pozostávajúce z jedného alebo niekoľkých okien s ovládacími prvkami, akými sú napríklad tlačidlá, textové polia, a podobne).
Vytvorenie aplikácie s jedným grafickým oknom
Minimalistickú JavaFX aplikáciu zobrazujúcu jedno prázdne okno o 300 krát 250 pixeloch s titulkom „Hello, World!” vytvoríme nasledovne:
package aplikacia; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.layout.*; public class Aplikacia extends Application { @Override public void start(Stage primaryStage) { Pane pane = new Pane(); Scene scene = new Scene(pane, 300, 250); primaryStage.setTitle("Hello, World!"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
Uvedený kód si teraz rozoberme:
- Hlavná trieda JavaFX aplikácie (tzn. trieda obsahujúca metódu main) sa vyznačuje tým, že dedí od abstraktnej triedy Application definovanej v balíku javafx.application, ktorý je potrebné importovať.
- Každá trieda dediaca od triedy Application musí implementovať jej abstraktnú metódu start, ktorej argumentom je objekt primaryStage typu Stage reprezentujúci hlavné grafické okno aplikácie (trieda Stage je definovaná v balíku javafx.stage, ktorý je potrebné importovať). Metóda start sa vykoná hneď po spustení aplikácie. V rámci metódy start sa typicky vytvárajú jednotlivé ovládacie prvky aplikácie a špecifikujú sa ich vlastnosti.
- V našom prípade je kľúčovým riadkom metódy start volanie primaryStage.show(), ktorým zobrazíme hlavné okno aplikácie. Bez tohto volania by aplikácia bežala „na pozadí”.
- Volaním primaryStage.setTitle("Hello, World!") nastavíme titulok hlavného okna na text „Hello, World!”.
- Uvedené dva riadky často stačia na zobrazenie grafického okna s titulkom „Hello, World!” a „náhodne” zvolenou veľkosťou. V závislosti od systému sa však môže stať aj to, že sa nezobrazí nič – grafické okno totiž zatiaľ nič neobsahuje a systém nemá ako „rozumne” vypočítať jeho veľkosť; môže teda túto situáciu vyhodnotiť aj tak, že ešte nie je čo zobraziť.
- Zvyšnými riadkami už len hovoríme, že „obsahom” hlavného okna má byť prázdna oblasť o veľkosti 300 krát 250 pixelov:
- Kontajnerom pre obsah okna je trieda Scene. Ide tu o analógiu s divadelnou terminológiou: okno zodpovedá javisku; na javisku následne možno umiestniť scénu pozostávajúcu z jednotlivých rekvizít. Scénu scene možno oknu primaryStage priradiť volaním primaryStage.setScene(scene). Trieda Scene je definovaná v balíku javafx.scene, ktorý je potrebné importovať.
- Scéna je interpretovaná ako hierarchický strom uzlov (detaily neskôr), pričom uzlami môžu byť napríklad oblasti, ale aj ovládacie prvky ako napríklad tlačidlá, či textové polia. Volaním konštruktora Scene scene = Scene(pane, 300, 250) vytvoríme scénu o rozmeroch 300 krát 250 pixelov, ktorej koreňovým uzlom je objekt pane; ten bude v našom prípade reprezentovať prázdnu oblasť.
- Volaním konštruktora Pane pane = new Pane() vytvoríme novú oblasť pane. Tá môže neskôr slúžiť ako kontajner pre pridávanie rôznych ovládacích prvkov a podobne. Trieda Pane je definovaná v balíku javafx.scene.layout, ktorý je potrebné importovať.
- Metóda main JavaFX aplikácie typicky pozostáva z jediného riadku, v ktorom sa volá statická metóda launch triedy Application. Tá sa postará o vytvorenie inštancie našej triedy Aplikacia, o vytvorenie hlavného grafického okna aplikácie, ako aj o následné zavolanie metódy start, ktorá dostane vytvorené okno ako argument.
Poznámka: V NetBeans je možné pri vytváraní projektu zvoliť ako typ projektu JavaFX -> JavaFX Application. V takom prípade sa automaticky vygeneruje krátky kód aplikácie s jedným tlačidlom vypisujúcim na konzolu text Hello World!. Po zmazaní nepotrebných častí tohto vygenerovaného kódu možno pokračovať v písaní vlastnej JavaFX aplikácie. Alternatívne možno cez Tools -> Templates -> JavaFX -> JavaFX Main Class prestaviť obsah generovanej kostry podľa vlastných preferencií.
Okno s niekoľkými jednoduchými ovládacími prvkami
Podbne ako v príklade vyššie vytvorme aplikáciu pozostávajúcu s jediného grafického okna:
package aplikacia; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.layout.*; public class Aplikacia extends Application { @Override public void start(Stage primaryStage) { Pane pane = new Pane(); Scene scene = new Scene(pane, 340, 100); primaryStage.setTitle("Zadávanie textu"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
Pridáme teraz do hlavného okna niekoľko ovládacích prvkov tak, ako na obrázku vpravo. Naším cieľom bude vytvorenie aplikácie umožňujúcej zadať text, ktorý sa pri kliknutí na tlačidlo OK zjaví v textovom popisku červenej farby. Ovládacie prvky ako textové pole alebo tlačidlo sú definované v balíku javafx.scene.control, ktorý je tak nutné importovať. Podobne na prácu s fontmi budeme potrebovať balík javafx.scene.text a na prácu s farbami balík javafx.scene.paint.
Začnime s pridaním textového popisku „Zadaj text”. Takéto textové popisky sú v JavaFX reprezentované triedou Label, pričom popisok label1 obsahujúci nami požadovaný text vytvoríme nasledovne:
Label label1 = new Label("Zadaj text:");
Rovnako dobre by sme mohli použiť aj konštruktor bez argumentov, ktorý je ekvivalentný volaniu konštruktora s argumentom "" – text popisku label1 možno upraviť aj neskôr volaním label1.setText("Nový text").
Po jeho vytvorení ešte musíme popisok label1 pridať do našej scény – presnejšie do oblasti pane, ktorá je jej koreňovým uzlom (čo znamená, že všetky ostatné uzly budú umiestnené v tejto oblasti). Vytvorený popisok label1 teda pridáme do zoznamu synov oblasti pane nasledujúcim volaním:
pane.getChildren().add(label1);
Následne môžeme upraviť niektoré vlastnosti vytvoreného popisku, ako napríklad jeho pozíciu a font:
label1.setFont(Font.font("Tahoma", FontWeight.BOLD, 12)); label1.setLayoutX(20); label1.setLayoutY(10);
Analogicky vytvoríme aj ostatné komponenty:
TextField textField = new TextField(); pane.getChildren().add(textField); textField.setFont(Font.font("Tahoma", FontWeight.BOLD, 12)); textField.setLayoutX(20); textField.setLayoutY(30); textField.setPrefWidth(300); Label label2 = new Label("(Zatiaľ nebolo zadané nič)"); pane.getChildren().add(label2); label2.setFont(Font.font("Tahoma", 12)); label2.setTextFill(Color.RED); label2.setLayoutX(20); label2.setLayoutY(70); Button button = new Button("OK"); pane.getChildren().add(button); button.setFont(Font.font("Tahoma", FontWeight.BOLD, 12)); button.setLayoutX(280); button.setLayoutY(60); button.setPrefWidth(40); button.setPrefHeight(30);
Uvedený spôsob grafického návrhu scény má však hneď dva zásadné nedostatky:
- Môžeme si všimnúť, že takéto pevné rozloženie ovládacích prvkov na scéne nevyzerá dobre, keď zmeníme veľkosť okna. Provizórne môžeme tento problém vyriešiť tým, že menenie rozmerov okna jednoducho zakážeme: primaryStage.setResizable(false). Takéto riešenie má však ďaleko od ideálneho. Odporúčaným prístupom je využiť namiesto triedy Pane niektorú z jej „inteligentnejších” podtried umožňujúcich (polo)automatické škálovanie scény v závislosti od veľkosti okna. V takom prípade sa absolútne súradnice ovládacích prvkov zvyčajne vôbec nenastavujú.
- Formát jednotlivých ovládacích prvkov (ako napríklad font alebo farba) by sa po správnosti nemal nastavovať priamo v zdrojovom kóde. Namiesto toho je odporúčaným prístupom využitie štýlov definovaných v pomocných súboroch JavaFX CSS. Takto je možné meniť formátovanie bez väčších zásahov do zdrojového kódu.
Obidvoma týmito problematikami sa budeme zaoberať v rámci nasledujúcej prednášky.
Oživenie ovládacích prvkov (spracovanie udalostí)
Dokončime našu jednoduchú aplikáciu so zadávaním textu pridaním jej kľúčovej funkcionality: po stlačení tlačidla OK (t.j. button) sa má do „červeného” popisku prekopírovať text zadaný používateľom do textového poľa.
Po stlačení tlačidla button je systémom (Java Virtual Machine) vygenerovaná tzv. udalosť, ktorá je v tomto prípade typu ActionEvent. Udalosť je teda akýsi objekt nesúci informáciu o tom, že bolo stlačené dané tlačidlo. Každé tlačidlo – objekt typu Button – má navyše k dispozícii (zdedenú) metódu
public final void setOnAction(EventHandler<ActionEvent> value)
umožňujúcu „zaregistrovať” pre dané tlačidlo jeho spracovávateľa udalostí typu ActionEvent. Ním môže byť ľubovoľná trieda implementujúca rozhranie EventHandler<ActionEvent>, ktoré vyžaduje implementáciu jedinej metódy
void handle(ActionEvent event)
Po zaregistrovaní objektu eventHandler ako spracovávateľa udalostí ActionEvent pre tlačidlo button volaním
button.setOnAction(eventHandler);
sa po každom stlačení tlačidla button vykoná metóda eventHandler.handle.
Nami požadovanú funkcionalitu tlačidla button tak vieme vyjadriť napríklad pomocou lokálnej triedy ButtonActionEventHandler:
public void start(Stage primaryStage) { ... class ButtonActionEventHandler implements EventHandler<ActionEvent> { @Override public void handle(ActionEvent event) { label2.setText(textField.getText()); } } EventHandler<ActionEvent> eventHandler = new ButtonActionEventHandler(); button.setOnAction(eventHandler); ... }
Skrátene môžeme to isté napísať s použitím anonymnej triedy:
public void start(Stage primaryStage) { ... button.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { label2.setText(textField.getText()); } }); ... }
Ak si navyše uvedomíme, že rozhranie EventHandler pozostáva z jedinej metódy, môžeme tento zápis ešte ďalej zjednodušiť použitím lambda výrazu:
public void start(Stage primaryStage) { ... button.setOnAction((ActionEvent e) -> { label2.setText(textField.getText()); }); ... }
Podrobnejšie sa spracúvaním udalostí v JavaFX budeme zaoberať na nasledujúcej prednáške.
Geometrické útvary
Špeciálnym typom uzlov, ktoré možno umiestňovať do scén, sú geometrické útvary ako napríklad Circle, Rectangle, Arc, Ellipse, Line, Polygon, atď. Všetky útvary dedia od spoločnej abstraktnej nadtriedy Shape. Sú definované v balíku javafx.scene.shape, ktorý je nutné na prácu s nimi importovať.
Aj keď útvary nevedia vyvolať udalosť typu ActionEvent, môžu vyvolávať udalosti iných typov. Napríklad kliknutie na útvar myšou vyústi v udalosť typu MouseEvent (definovanú v balíku javafx.scene.input) a spracovávateľa takejto udalosti možno pre útvar shape zaregistrovať pomocou metódy shape.setOnMouseClicked.
Nasledujúci kód vykreslí „tabuľku” o 10 krát 10 útvaroch, pričom pre každý sa náhodne určí, či pôjde o štvorec, alebo o kruh. Farba každého z útvarov sa taktiež určí náhodne. Navyše po kliknutí myšou na ktorýkoľvek z útvarov sa jeho farba náhodne zmení.
package aplikacia; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.layout.*; import javafx.scene.shape.*; import javafx.scene.paint.*; import javafx.scene.input.*; import java.util.Random; public class Aplikacia extends Application { public Shape createSquare(double ulX, double ulY, double sideLength, Color color) { Rectangle square = new Rectangle(ulX, ulY, sideLength, sideLength); square.setFill(color); return square; } public Shape createCircle(double centerX, double centerY, double radius, Color color) { Circle circle = new Circle(centerX, centerY, radius); circle.setFill(color); return circle; } @Override public void start(Stage primaryStage) { Pane pane = new Pane(); Random random = new Random(); for (int i = 0; i <= 9; i++) { for (int j = 0; j <= 9; j++) { Shape shape; boolean isSquare = random.nextBoolean(); Color color = Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble()); if (isSquare) { shape = createSquare(i * 60 + 5, j * 60 + 5, 50, color); } else { shape = createCircle(i * 60 + 30, j * 60 + 30, 25, color); } shape.setOnMouseClicked((MouseEvent e) -> { shape.setFill(Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble())); }); pane.getChildren().add(shape); } } Scene scene = new Scene(pane, 600, 600); primaryStage.setTitle("Geometrické útvary"); primaryStage.setResizable(false); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
Prednáška 30
Oznamy
- V stredu 3. apríla o 18:10 bude v posluchárni B prvá písomka (obsahovo bude pokrývať zhruba prvé štyri prednášky letného semestra; môže sa vyskytnúť aj úloha na prácu so stromami).
- Ďalšiu domácu úlohu treba odovzdať do stredy 10. apríla, 22:00.
Grafický návrh scény: jednoduchá kalkulačka
Prístup ku grafickému návrhu aplikácií z minulej prednášky, v ktorom sme každému ovládaciemu prvku na scéne manuálne nastavovali jeho polohu a štýl, sa už pri o čo i len málo rozsiahlejších aplikáciách javí byť príliš prácnym a nemotorným. V nasledujúcom sa preto zameriame na alternatívny prístup založený predovšetkým na dvoch základných technikách:
- Namiesto koreňovej oblasti typu Pane budeme používať jej „inteligentnejšie” podtriedy, ktoré presnú polohu ovládacích prvkov určujú automaticky na základe preferencií daných programátorom.
- Formátovanie ovládacích prvkov (napríklad font textu, farba výplne, atď.) obvykle nebudeme nastavovať priamo zo zdrojového kódu, ale pomocou štýlov definovaných v externých JavaFX CSS súboroch. (Tie sa podobajú na klasické CSS používané pri návrhu webových stránok. Na zvládnutie tejto prednášky však nie je potrebná žiadna predošlá znalosť CSS; obmedzíme sa navyše len na naznačenie niektorých základných možností JavaFX CSS štýlov). Výhodou použitia externých CSS štýlov je aj možnosť meniť vzhľad aplikácie bez zásahov do jej zdrojového kódu.
Uvedené techniky demonštrujeme na ukážkovej aplikácii: (azda až priveľmi) jednoduchej kalkulačke. Výsledný vzhľad tejto aplikácie je na obrázku vpravo. Jej základná funkcionalita bude pozostávať z možnosti zadať dve reálne čísla a zvoliť jednu zo štyroch operácií – sčítanie, odčítanie, násobenie, prípadne delenie. Po stlačení tlačidla Počítaj! sa zobrazí výsledok vybranej operácie na zadanej dvojici čísel. Okrem toho aplikácia obsahuje tlačidlo na zmazanie všetkých vstupných údajov a zobrazeného výsledku a tlačidlo na ukončenie aplikácie.
Základom pre túto aplikáciu bude rovnaká kostra programu ako na minulej prednáške:
package calculator; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.layout.*; import javafx.scene.control.*; import javafx.event.*; public class Calculator extends Application { @Override public void start(Stage primaryStage) { Pane pane = new Pane(); Scene scene = new Scene(pane); primaryStage.setScene(scene); primaryStage.setTitle("Kalkulačka"); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
Formátovanie pomocou JavaFX CSS štýlov (1. časť)
Predpokladajme, že potrebujeme vytvoriť aplikáciu s dostatočnou veľkosťou písma všetkých jej textových prvkov (napríklad 11 typografických bodov). Mohli by sme túto vlastnosť nastavovať manuálne pre všetky jednotlivé ovládacie prvky, podobne ako na minulej prednáške – takýto prístup však po čase omrzí. Podobne každá zmena požadovanej veľkosti písma (napríklad na 12 bodov) by v budúcnosti vyžadovala vynaloženie rovnakého úsilia. Vhodnejším prístupom je použitie JavaFX CSS štýlov definovaných v externom súbore.
Vytvorme textový súbor styles.css s nasledujúcim obsahom:
.root { -fx-font-size: 11pt; }
Pri práci s NetBeans ho uložme napríklad do umistnenia <Koreňový adresár projektu>/src/resources, kde resources je novovytvorený adresár. V prípade práce z príkazového riadku bez použitia balíka je ekvivalentným umiestnením <Adresár hlavného zdrojového súboru>/resources.
JavaFX CSS súbor s uvedeným obsahom hovorí, že východzia veľkosť písma má byť 11 bodov. Zostáva tak súbor styles.css „aplikovať” na našu scénu:
@Override public void start(Stage primaryStage) { ... scene.getStylesheets().add("resources/styles.css"); ... }
Scéna a strom uzlov
Obsah JavaFX scény sa reprezentuje v podobe tzv. stromu uzlov (alebo grafu uzlov).
- Prvky umiestňované na scénu sa nazývajú uzly – triedy reprezentujúce tieto prvky majú ako spoločného predka triedu Node.
- Niektoré z uzlov môžu byť rodičmi iných uzlov na scéne – typickým príkladom sú napríklad oblasti typu Pane, s ktorými sme sa stretli už minule a ktoré sme využívali ako kontajnery pre ovládacie prvky umiestňované na scénu (tie sa tak stali deťmi danej oblasti). Všetky triedy reprezentujúce uzly, ktoré môžu byť rodičmi iných uzlov, sú potomkami triedy Parent; tá je priamou podtriedou triedy Node. Medzi takéto triedy okrem Pane patria aj triedy pre ovládacie prvky ako napríklad Button alebo Label. Nepatria medzi ne napríklad triedy pre geometrické útvary (trieda Shape a jej potomkovia).
- Pri vytváraní scény je ako argument konštruktora potrebné zadať koreňový uzol stromu – tým môže byť inštancia ľubovoľnej triedy, ktorá je potomkom triedy Parent. Ďalšie „poschodia” stromu uzlov sa typicky pridávajú podobne ako na minulej prednáške, napríklad s využitím metód getChildren().add pre jednotlivé rodičovské uzly.
Rozloženie uzlov na scéne
Vráťme sa teraz k nášmu projektu jednoduchej kalkulačky. Naším najbližším cieľom bude umiestnenie jednotlivých ovládacích prvkov na scénu. Chceli by sme sa pritom vyhnúť manuálnemu nastavovaniu ich polôh; namiesto oblasti typu Pane preto ako koreňový uzol použijeme oblasť, ktorá sa bude o rozloženie ovládacích prvkov starať do veľkej miery samostatne.
GridPane
Odmyslime si na chvíľu tlačidlá „Zmaž” a „Skonči” a umiestnime na scénu zvyšné ovládacie prvky. Ako koreňový uzol použijeme namiesto oblasti typu Pane oblasť typu GridPane. Trieda GridPane je jednou z podtried triedy Pane umožňujúcich „inteligentné” spravovanie rozloženia uzlov.
@Override public void start(Stage primaryStage) { ... // Pane pane = new Pane(); GridPane grid = new GridPane(); // Scene scene = new Scene(pane); Scene scene = new Scene(grid); ... }
Oblasť typu GridPane umožňuje pridávanie ovládacích prvkov do obdĺžnikovej mriežky. Pridajme teda prvý ovládací prvok – popisok obsahujúci text „Zadajte vstupné hodnoty:”. Riadky aj stĺpce mriežky sa pri GridPane indexujú počínajúc nulou; maximálny index je (takmer) neobmedzený. Vytvorený textový popisok teda vložíme do políčka v nultom stĺpci a v nultom riadku. Okrem toho povieme, že obsah vytvoreného textového popisku môže prípadne zabrať až dva stĺpce mriežky, ale iba jeden riadok.
Label lblHeader = new Label("Zadajte vstupné hodnoty:"); // Vytvorenie textoveho popisku grid.getChildren().add(lblHeader); // Pridanie do stromu uzlov za syna oblasti grid GridPane.setColumnIndex(lblHeader, 0); // Vytvoreny popisok bude v 0-tom stlpci GridPane.setRowIndex(lblHeader, 0); // Vytvoreny popisok bude v 0-tom riadku GridPane.setColumnSpan(lblHeader, 2); // Moze zabrat az 2 stlpce... GridPane.setRowSpan(lblHeader, 1); // ... ale iba 1 riadok
Všimnime si, že pozíciu lblHeader v mriežke nastavujeme pomocou statických metód triedy GridPane. Ak sa riadok resp. stĺpec nenastavia ručne, použije sa východzia hodnota 0. Podobne sa pri nenastavení zvyšných dvoch hodnôt použije východzia hodnota 1 (na čo sa budeme často spoliehať).
Uvedený spôsob pridania ovládacieho prvku do mriežky je však pomerne prácny – vyžaduje si až päť príkazov. Existuje preto skratka: všetkých päť príkazov možno vykonať v rámci jediného volania metódy grid.add:
Label lblHeader = new Label("Zadajte vstupné hodnoty:"); grid.add(lblHeader, 0, 0, 2, 1); // grid.getChildren().add(lblHeader); // GridPane.setColumnIndex(lblHeader, 0); // GridPane.setRowIndex(lblHeader, 0); // GridPane.setColumnSpan(lblHeader, 2); // GridPane.setRowSpan(lblHeader, 1);
(Bez explicitného uvedenia posledných dvoch parametrov metódy add by sa použili ich východzie hodnoty 1, 1.)
Podobne môžeme do mriežky umiestniť aj ďalšie ovládacie prvky:
Label lblNum1 = new Label("Prvý argument:"); grid.add(lblNum1, 0, 1); Label lblNum2 = new Label("Druhý argument:"); grid.add(lblNum2, 0, 2); Label lblOp = new Label("Operácia:"); grid.add(lblOp, 0, 3); Label lblResultText = new Label("Výsledok:"); grid.add(lblResultText, 0, 5); Label lblResult = new Label("0"); grid.add(lblResult, 1, 5); TextField tfNum1 = new TextField(); grid.add(tfNum1, 1, 1); TextField tfNum2 = new TextField(); grid.add(tfNum2, 1, 2); ComboBox cbOp = new ComboBox(); grid.add(cbOp, 1, 3); cbOp.getItems().addAll("+", "-", "*", "/"); cbOp.setValue("+"); Button btnOK = new Button("Počítaj!"); grid.add(btnOK, 1, 4);
Novým prvkom je tu „vyskakovací zoznam” ComboBox. Jeho metóda getItems vráti zoznam všetkých možností na výber (ten je na začiatku prázdny), do ktorého následne vkladáme možnosti zodpovedajúce jednotlivým operáciám. Metóda setValue nastaví aktuálne zvolenú možnosť. (V takomto východzom stave nemožno do ComboBox-u zadávať text manuálne; v prípade potreby je ale možné túto možnosť aktivovať metódou setEditable s parametrom true.)
Pre účely ladenia ešte môžeme zviditeľniť deliace čiary mriežky nasledujúcim spôsobom:
grid.setGridLinesVisible(true);
Môžeme ďalej napríklad nastaviť preferované rozmery niektorých ovládacích prvkov (neskôr ale uvidíme lepší spôsob, ako to robiť):
tfNum1.setPrefWidth(300); tfNum2.setPrefWidth(300); cbOp.setPrefWidth(300);
Tiež si môžeme všimnúť, že medzi jednotlivými políčkami mriežky nie sú žiadne medzery. To vyriešime napríklad nasledovne:
grid.setHgap(10); // Horizontalna medzera medzi dvoma polickami mriezky bude 10 pixelov grid.setVgap(10); // To iste pre vertikalnu medzeru
Podobne nie je žiadna medzera medzi mriežkou a okrajmi okna. To možno vyriešiť pomocou nastavenia „okrajov”:
import javafx.geometry.*; ... grid.setPadding(new Insets(10,20,10,20)); // horny okraj 10 pixelov, pravy 20, dolny 10, lavy 20
Trieda Insets (skratka od angl. Inside Offsets) reprezentuje iba súbor štyroch hodnôt pre „veľkosti okrajov” a je definovaná v balíku javafx.geometry. Vzhľad aplikácie v tomto momente je na obrázku vpravo.
Ďalej si môžeme všimnúť, že obsah mriežky zostáva aj pri zväčšovaní veľkosti okna v jeho ľavom hornom rohu. Zarovnanie obsahu mriežky na stred dostaneme volaním
grid.setAlignment(Pos.CENTER);
Pre oblasť grid je vyhradené prakticky celé okno; uvedeným volaním hovoríme, že jej reálny obsah sa má zarovnať na stred tejto vyhradenej oblasti.
Pomerne žiadúcim správaním aplikácie pri zväčšovaní šírky okna je súčasné rozširovanie textových polí a „vyskakovacieho zoznamu”. Zrušme najprv manuálne nastavenú preferovanú šírku uvedených ovládacích prvkov:
// tfNum1.setPrefWidth(300); // tfNum2.setPrefWidth(300); // cbOp.setPrefWidth(300);
Cielený efekt dosiahneme naplnením zoznamu „obmedezní pre jednotlivé stĺpce”, ktorý si každá oblasť typu GridPane udržiava. „Obmedzenia” na nultý stĺpec nebudú žiadne; v „obmedzeniach” nasledujúceho stĺpca nastavíme jeho preferovanú šírku na 300 pixelov a povieme tiež, aby sa pri rozširovaní oblasti rozširoval aj daný stĺpec. „Obmedzenia” pre jednotlivé stĺpce sú reprezentované triedou ColumnConstraints. (Analogicky je možné nastavovať „obmedzenia” aj pre jednotlivé riadky.)
ColumnConstraints cc = new ColumnConstraints(); cc.setPrefWidth(300); cc.setHgrow(Priority.ALWAYS); grid.getColumnConstraints().addAll(new ColumnConstraints(), cc);
Uvedený kód funguje až na jeden detail: pri rozširovaní okna sa nemení veľkosť „vyskakovacieho zoznamu”. To je dané tým, že jeho východzia maximálna veľkosť je totožná s jeho preferovanou veľkosťou; na dosiahnutie kýženého efektu je teda potrebné prestaviť túto maximálnu veľkosť tak, aby viac „neprekážala”:
cbOp.setMaxWidth(Double.MAX_VALUE);
Nastavme ešte zarovnanie niektorých ovládacích prvkov na pravý okraj ich políčka mriežky. Využijeme pritom statickú metódu setHalignment triedy GridPane:
GridPane.setHalignment(lblNum1, HPos.RIGHT); GridPane.setHalignment(lblNum2, HPos.RIGHT); GridPane.setHalignment(lblOp, HPos.RIGHT); GridPane.setHalignment(lblResultText, HPos.RIGHT); GridPane.setHalignment(lblResult, HPos.RIGHT); GridPane.setHalignment(btnOK, HPos.RIGHT);
S grafickým návrhom rozloženia prvkov mriežky sme teraz hotoví a môžeme teda aj zrušiť zobrazovanie deliacich čiar:
// grid.setGridLinesVisible(true);
Momentálny vzhľad aplikácie je na obrázku vpravo.
BorderPane a VBox
Pridáme teraz tlačidlá „Zmaž” a „Skonči”. Mohli by sme ich samozrejme umiestniť napríklad do ďalšieho stĺpca mriežky grid. Tu si však ukážeme odlišný prístup – namiesto oblasti typu GridPane použijeme ako koreňový uzol scény oblasť typu BorderPane. Tá sa ako koreňový uzol scény používa asi najčastejšie, pretože umožňuje nastaviť päť základných častí scény: hornú, pravú, dolnú, ľavú a stredovú časť.
Typicky každá z týchto častí (ak je definovaná) pozostáva z ďalšej oblasti nejakého iného typu – v našom prípade za centrálnu časť zvolíme už vytvorenú mriežku grid:
BorderPane border = new BorderPane(); border.setCenter(grid); // Scene scene = new Scene(grid); Scene scene = new Scene(border);
Zostáva si teraz vytvoriť „kontajnerovú” oblasť pre pravú časť a umiestniť do nej spomínané dve tlačidlá. Keďže majú byť tieto tlačidlá umiestnené nad sebou, pravdepodobne najlepšou voľbou ich „kontajnerovej” oblasti je oblasť typu VBox, do ktorej sa jednotlivé uzly vkladajú vertikálne jeden pod druhý:
VBox right = new VBox(); // Vytvorenie oblasti typu VBox right.setPadding(new Insets(10, 20, 10, 60)); // Nastavenie okrajov (v poradi horny, pravy, lavy, dolny) right.setSpacing(10); // Vertikalne medzery medzi vkladanymi uzlami right.setAlignment(Pos.BOTTOM_LEFT); // Zarovnanie obsahu oblasti vertikalne nadol a horizontalne dolava border.setRight(right); // Nastavenie oblasti right ako pravej casti oblasti border
Vložíme teraz do oblasti right obidve tlačidlá:
Button btnClear = new Button("Zmaž"); right.getChildren().add(btnClear); Button btnExit = new Button("Skonči"); right.getChildren().add(btnExit);
Vidíme ale, že tlačidlá majú rôznu šírku, čo nevyzerá veľmi dobre. Rovnakú šírku by sme samozrejme vedeli dosiahnuť manuálnym nastavením veľkosti tlačidiel na nejakú fixnú hodnotu; to však nie je najideálnejší prístup. Na dosiahnutie rovnakého efektu využijeme skutočnosť, že šírka oblasti right sa automaticky nastaví na preferovanú šírku širšieho z oboch tlačidiel. Užšie z tlačidiel ostáva menšie preto, lebo jeho východzia maximálna šírka je rovná jeho preferovanej šírke. Po prestavení maximálnej šírky na dostatočne veľkú hodnotu sa toto tlačidlo taktiež roztiahne na celú šírku oblasti right:
btnClear.setMaxWidth(Double.MAX_VALUE); btnExit.setMaxWidth(Double.MAX_VALUE);
Momentálny vzhľad aplikácie je na obrázku vpravo.
Ďalšie rozloženia
Okrem GridPane, BorderPane a VBox existuje v JavaFX aj niekoľko ďalších oblastí umožňujúcich (polo)automaticky spravovať rozloženie jednotlivých uzlov:
- HBox: ide o horizontálnu obdobu VBox-u.
- StackPane: umiestňuje prvky na seba (dá sa použiť napríklad pri tvorbe grafických komponentov; môžeme dajme tomu jednoducho vytvoriť obdĺžnik obsahujúci nejaký text, atď.).
- FlowPane: umiestňuje prvky za seba po riadkoch, prípadne po stĺpcoch. Pri zmene rozmerov okna môže dôjsť k zmene pozície jednotlivých prvkov.
- TilePane: udržiava „dlaždice” rovnakej veľkosti.
- AnchorPane: umožňuje ukotvenie prvkov na danú pozíciu.
Kalkulačka: oživenie aplikácie
Pridajme teraz jednotlivým ovládacím prvkom aplikácie ich funkcionalitu (vystačíme si pritom s metódami z minulej prednášky). Kľúčovou je pritom funkcionalita tlačidla btnOK:
... class InvalidOperatorException extends RuntimeException { } ... public class Calculator extends Application { ... /** * Aplikuje na argumenty arg1, arg2 operaciu reprezentovanu retazcom op. */ public double calculate(String op, double arg1, double arg2) { switch (op) { case "+": return arg1 + arg2; case "-": return arg1 - arg2; case "*": return arg1 * arg2; case "/": return arg1 / arg2; default: throw new InvalidOperatorException(); } } ... @Override public void start(Stage primaryStage) { ... btnOK.setOnAction((ActionEvent event) -> { try { if (cbOp.getValue() instanceof String) { lblResult.setText(Double.toString(calculate((String) cbOp.getValue(), Double.parseDouble(tfNum1.getText()), Double.parseDouble(tfNum2.getText())))); } else { throw new InvalidOperatorException(); } } catch (NumberFormatException exception) { lblResult.setText("Výnimka!"); } }); ... } }
Podobne môžeme pridať aj funkcionalitu zostávajúcich dvoch tlačidiel:
@Override public void start(Stage primaryStage) { ... btnClear.setOnAction((ActionEvent e) -> { tfNum1.setText(""); tfNum2.setText(""); cbOp.setValue("+"); lblResult.setText("0"); }); btnExit.setOnAction((ActionEvent e) -> { Platform.exit(); // Specialna metoda, ktora ukonci beh aplikacie }); ... }
Formátovanie pomocou JavaFX CSS štýlov (2. časť)
Finálny vzhľad aplikácie získame doplnením súboru styles.css. Môžeme začať tým, že okrem východzej veľkosti fontu nastavíme aj východziu skupinu fontov a textúru na pozadí aplikácie:
.root { -fx-font-size: 11pt; -fx-font-family: 'Tahoma'; -fx-background-image: url("texture.jpg"); -fx-background-size: cover; }
Na internete je množstvo textúr dostupných pod licenciou Public Domain (CC0) – to je aj prípad textúry z ukážky finálneho vzhľadu aplikácie z úvodu tejto prednášky. Súbor s textúrou je potrebné uložiť do rovnakého adresára ako súbor styles.css.
Možno tiež nastavovať formát jednotlivých skupín ovládacích prvkov. Nasledovne napríklad docielime, aby sa pri všetkých tlačidlách a textových popiskoch použilo tučné písmo; textové popisky navyše ofarbíme bielou farbou:
.label { -fx-font-weight: bold; -fx-text-fill: white; } .button { -fx-font-weight: bold; }
Formát ovládacích prvkov je možné nastavovať aj individuálne – v takom prípade ale musíme dotknutým prvkom v zdrojovom kóde aplikácie nastaviť ich identifikátor:
@Override public void start(Stage primaryStage) { ... lblHeader.setId("header"); lblResult.setId("result"); ... }
V JavaFX CSS súbore následne vieme prispôsobiť formát pomenovaných ovládacích prvkov:
#header { -fx-font-size: 18pt; } #result { -fx-font-size: 16pt; -fx-text-fill: black; }
Kalkulačka: kompletný kód aplikácie
Zdrojový kód aplikácie:
package calculator; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.layout.*; import javafx.scene.control.*; import javafx.event.*; import javafx.geometry.*; class InvalidOperatorException extends RuntimeException { } public class Calculator extends Application { public double calculate(String op, double arg1, double arg2) { switch (op) { case "+": return arg1 + arg2; case "-": return arg1 - arg2; case "*": return arg1 * arg2; case "/": return arg1 / arg2; default: throw new InvalidOperatorException(); } } @Override public void start(Stage primaryStage) { GridPane grid = new GridPane(); grid.setHgap(10); grid.setVgap(10); grid.setPadding(new Insets(10,20,10,20)); grid.setAlignment(Pos.CENTER); ColumnConstraints cc = new ColumnConstraints(); cc.setPrefWidth(300); cc.setHgrow(Priority.ALWAYS); grid.getColumnConstraints().addAll(new ColumnConstraints(), cc); Label lblHeader = new Label("Zadajte vstupné hodnoty:"); grid.add(lblHeader, 0, 0, 2, 1); lblHeader.setId("header"); Label lblNum1 = new Label("Prvý argument:"); grid.add(lblNum1, 0, 1); GridPane.setHalignment(lblNum1, HPos.RIGHT); Label lblNum2 = new Label("Druhý argument:"); grid.add(lblNum2, 0, 2); GridPane.setHalignment(lblNum2, HPos.RIGHT); Label lblOp = new Label("Operácia:"); grid.add(lblOp, 0, 3); GridPane.setHalignment(lblOp, HPos.RIGHT); Label lblResultText = new Label("Výsledok:"); grid.add(lblResultText, 0, 5); GridPane.setHalignment(lblResultText, HPos.RIGHT); Label lblResult = new Label("0"); grid.add(lblResult, 1, 5); GridPane.setHalignment(lblResult, HPos.RIGHT); lblResult.setId("result"); TextField tfNum1 = new TextField(); grid.add(tfNum1, 1, 1); TextField tfNum2 = new TextField(); grid.add(tfNum2, 1, 2); ComboBox cbOp = new ComboBox(); grid.add(cbOp, 1, 3); cbOp.getItems().addAll("+", "-", "*", "/"); cbOp.setValue("+"); cbOp.setMaxWidth(Double.MAX_VALUE); Button btnOK = new Button("Počítaj!"); grid.add(btnOK, 1, 4); GridPane.setHalignment(btnOK, HPos.RIGHT); VBox right = new VBox(); right.setPadding(new Insets(10, 20, 10, 60)); right.setSpacing(10); right.setAlignment(Pos.BOTTOM_LEFT); Button btnClear = new Button("Zmaž"); right.getChildren().add(btnClear); btnClear.setMaxWidth(Double.MAX_VALUE); Button btnExit = new Button("Skonči"); right.getChildren().add(btnExit); btnExit.setMaxWidth(Double.MAX_VALUE); BorderPane border = new BorderPane(); border.setCenter(grid); border.setRight(right); Scene scene = new Scene(border); scene.getStylesheets().add("resources/styles.css"); btnOK.setOnAction((ActionEvent event) -> { try { if (cbOp.getValue() instanceof String) { lblResult.setText(Double.toString(calculate((String) cbOp.getValue(), Double.parseDouble(tfNum1.getText()), Double.parseDouble(tfNum2.getText())))); } else { throw new InvalidOperatorException(); } } catch (NumberFormatException exception) { lblResult.setText("Výnimka!"); } }); btnClear.setOnAction((ActionEvent e) -> { tfNum1.setText(""); tfNum2.setText(""); cbOp.setValue("+"); lblResult.setText("0"); }); btnExit.setOnAction((ActionEvent e) -> { Platform.exit(); }); primaryStage.setScene(scene); primaryStage.setTitle("Kalkulačka"); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
Súbor JavaFX CSS:
.root { -fx-font-size: 11pt; -fx-font-family: 'Tahoma'; -fx-background-image: url("texture.jpg"); -fx-background-size: cover; } .label { -fx-font-weight: bold; -fx-text-fill: white; } .button { -fx-font-weight: bold; } #header { -fx-font-size: 18pt; } #result { -fx-font-size: 16pt; -fx-text-fill: black; }
Programovanie riadené udalosťami
Základné princípy programovania riadeného udalosťami
V súvislosti s JavaFX sme začali používať novú paradigmu: programovanie riadené udalosťami. Namiesto sekvenčného vykonávania jednotlivých príkazov sa tu s vykonávaním kódu čaká na udalosť zvonka, ktorou môže byť napríklad stlačenie tlačidla používateľom. Tento spôsob programovania má svoje špecifiká – s niektorými z nich sme sa už koniec koncov stretli. Na lepšie ozrejmenie princípov programovania riadeného udalosťami teraz na chvíľu odbočíme od programovania aplikácií s grafickým používateľským rozhraním a demonštrujeme esenciu tejto paradigmy na jednoduchej konzolovej aplikácii. Stále však budeme využívať triedy pre udalosti definované v balíku javafx.event.
Začíname teda s nasledujúcou kostrou:
package simpleevents; import javafx.event.*; import java.util.*; public class SimpleEvents { public static void main(String[] args) { } }
Pre zmysluplnú prácu s udalosťami potrebujeme minimálne tri triedy: aspoň jednu triedu pre samotnú udalosť, aspoň jednu triedu pre spracovávateľa udalostí a aspoň jednu triedu schopnú udalosti spúšťať (o spúšťanie udalostí v JavaFX sa zvyčajne stará prostredie).
Definujme teda najprv triedu MyEvent reprezentujúcu jednoduchú udalosť obsahujúcu nejakú správu o sebe:
class MyEvent extends Event { // Nasa trieda dedi od Event, ktora je najvyssou triedou pre udalosti v JavaFX private static EventType myEventType = new EventType("MyEvent"); // Typ udalosti zodpovedajuci udalostiam MyEvent (pre nas nepodstatna technikalita) private final String message; public MyEvent(Object source, String message) { // Konstruktor, ktory ma vytvorit udalost s danym odosielatelom source a spravou message super(source, NULL_SOURCE_TARGET, myEventType); // Volanie konstruktora nadtriedy. Druhy a treti parameter su pre nase ucely nepodstatne this.message = message; // Nastavime spravu zodpovedajucu nasej udalosti } public String getMessage() { // Metoda, ktora vrati spravu zodpovedajucu udalosti return message; } }
Spracovávateľ JavaFX udalostí typu T sa vyznačuje tým, že implementuje rozhranie EventHandler<T>. S týmto rozhraním sme sa stretli už minule a vieme, že vyžaduje implementáciu jedinej metódy handle (čo okrem iného umožňuje nahradiť inštancie takýchto tried lambda výrazmi). Vytvorme teda jednoduchú triedu MyEventHandler pre spracovávateľa udalostí MyEvent:
class MyEventHandler implements EventHandler<MyEvent> { @Override public void handle(MyEvent event) { System.out.println("Spracuvam udalost: " + event.getMessage()); } }
Potrebujeme ešte triedu MyEventSender, ktorá bude schopná udalosti typu MyEvent vytvárať. Tá bude zo všetkých najkomplikovanejšia. Musí totiž:
- Uchovávať zoznam actionListeners všetkých spracovávateľov udalostí, ktoré čakajú na ňou generované udalosti (v JavaFX sme zatiaľ pracovali len so situáciou, keď na jednu udalosť čaká najviac jeden spracovávateľ; hoci je to najčastejší prípad, nebýva to vždy tak).
- Poskytovať metódu addActionListener pridávajúcu spracovávateľa udalosti. (Tá sa podobá napríklad na metódu Button.setOnAction s tým rozdielom, že Button.setOnAction nepridáva ďalšieho spracovávateľa, ale pridáva nového jediného spracovávateľa. Aj Button však poskytuje metódu AddEventHandler, ktorá je dokonca o niečo všeobecnejšia, než bude naša metóda addActionListener).
- Poskytovať metódu fireAction, ktorá udalosť spustí. To si vyžaduje zavolať metódu handle všetkých spracovávateľov zo zoznamu actionListeners.
class MyEventSender { private final String name; private final ArrayList<EventHandler<MyEvent>> actionListeners; // Zoznam spracovavatelov udalosti public MyEventSender(String name) { this.name = name; actionListeners = new ArrayList<>(); } public String getName() { return this.name; } public void addActionListener(EventHandler<MyEvent> handler) { // Metoda pridavajuca spracovavatela udalosti actionListeners.add(handler); } public void fireAction(int type) { // Metoda spustajuca udalost MyEvent event = new MyEvent(this, "UDALOST " + type); for (EventHandler<MyEvent> eventHandler : actionListeners) { eventHandler.handle(event); } } }
Môžeme teraz ešte upraviť triedu MyEventHandler tak, aby využívala metódu MyEventSender.getName:
class MyEventHandler implements EventHandler<MyEvent> { @Override public void handle(MyEvent event) { System.out.println("Spracuvam udalost: " + event.getMessage()); Object sender = event.getSource(); if (sender instanceof MyEventSender) { System.out.println("Odosielatel udalosti: " + ((MyEventSender) sender).getName()); } } }
Funkcia main potom môže vyzerať napríklad nasledovne:
public static void main(String[] args) { MyEventSender sender1 = new MyEventSender("prvy"); MyEventSender sender2 = new MyEventSender("druhy"); MyEventHandler handler = new MyEventHandler(); sender1.addActionListener(handler); sender2.addActionListener((MyEvent event) -> { System.out.println("Spracuvavam udalost " + event.getMessage() + " inym sposobom."); }); sender2.addActionListener(handler); sender2.addActionListener((MyEvent event) -> { System.out.println("Spracuvavam udalost " + event.getMessage() + " este inym sposobom."); }); sender1.fireAction(1000); sender2.fireAction(2000); }
Konzumácia udalostí
V triede Event sú okrem iného definované dve špeciálne metódy: consume() a isConsumed(). Ak je udalosť skonzumovaná, znamená to zhruba toľko, že už je spracovaná a nemusí sa predávať prípadným ďalším spracovávateľom. V našom jednoduchom programe vyššie napríklad môžeme upraviť triedu MyEventHandler tak, aby pri spracovaní udalosti túto udalosť aj rovno skonzumovala; triedu MyEventSender naopak upravíme tak, aby metódy handle jednotlivých spracovávateľov volala len kým ešte udalosť nie je skonzumovaná.
class MyEventHandler implements EventHandler<MyEvent> { @Override public void handle(MyEvent event) { System.out.println("Spracuvam udalost: " + event.getMessage()); Object sender = event.getSource(); if (sender instanceof MyEventSender) { System.out.println("Odosielatel udalosti: " + ((MyEventSender) sender).getName()); } event.consume(); } } ... class MyEventSender { ... public void fireAction(int type) { MyEvent event = new MyEvent(this, "UDALOST " + type); for (EventHandler<MyEvent> eventHandler : actionListeners) { eventHandler.handle(event); if (event.isConsumed()) { break; } } } ... }
V JavaFX je mechanizmus konzumovania udalostí o niečo zložitejší.
JavaFX: udalosti myši
- Udalosti nejakým spôsobom súvisiace s myšou (napríklad stlačenie alebo uvoľnenie tlačidla) v JavaFX reprezentuje trieda MouseEvent.
- Obsahuje napríklad metódy getButton(), getSceneX(), getSceneY() umožňujúce získať informácie o danej udalosti.
Vytváranie a spracovanie udalostí myši v JavaFX funguje nasledovne:
- Ako prvá sa udalosť vytvorí na tom uzle, ktorý je v mieste udalosti na scéne viditeľný (zaujímavé najmä v prípade prekrývajúcich sa uzlov).
- Spracovávatelia danej udalosti na danom uzle môžu udalosť spracovať.
- Ak po vykonaní predchádzajúceho kroku ešte nie je udalosť skonzumovaná, môže sa dostať aj k iným uzlom.
- Celkovo je predávanie udalostí k ďalším uzlom relatívne komplikovaný proces (viac detailov tu).
JavaFX: udalosti klávesnice
- Udalosti súvisiace s klávesnicou v JavaFX reprezentuje trieda KeyEvent.
- Kľúčovou metódou tejto triedy je getCode, ktorá vracia kód stlačeného tlačidla klávesnice.
- Udalosť sa vytvorí na uzle, ktorý má tzv. fokus – každý uzol oň môže požiadať metódou requestFocus().
Časovač: pohybujúci sa kruh
V balíku javafx.animation je definovaná abstraktná trieda AnimationTimer, ktorá umožňuje „periodické” vykonávanie určitej udalosti (zakaždým, keď sa nanovo prekreslí obsah scény). Obsahuje implementované metódy start() a stop() a abstraktnú metódu s hlavičkou
abstract void handle(long now)
Prekrytím tejto metódy v podtriede dediacej od AnimationTimer možno špecifikovať udalosť, ktorá sa bude „periodicky” vykonávať. Jej vstupnou hodnotu je časová pečiatka now reprezentujúca čas v nanosekundách; pomocou nej sa dá ako-tak prispôsobiť interval vykonávania jednotlivých udalostí.
Použitie takéhoto časovača demonštrujeme na jednoduchej aplikácii: v okne sa bude buď vodorovne alebo zvisle pohybovať kruh určitej veľkosti. Pri každom „náraze” na okraj scény sa otočí o 180 stupňov. Pri stlačení niektorej zo šípok klávesnice sa kruh začne pohybovať daným smerom. Navyše sa raz za cca. pol sekundy náhodne zmení farba kruhu.
package movingcircle; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.layout.*; import javafx.scene.control.*; import javafx.event.*; import javafx.scene.shape.*; import javafx.scene.paint.*; import javafx.animation.*; import javafx.scene.input.*; import java.util.*; public class MovingCircle extends Application { enum MoveDirection{ // Vymenovany typ reprezentujuci mozne smery pohybu kruhu UP, RIGHT, DOWN, LEFT }; private MoveDirection moveDirection; // Aktualny smer pohybu kruhu // Metoda, ktora na scene scene posunie kruh circle smerom moveDirection o pocet pixelov delta: private void moveCircle(Scene scene, Circle circle, MoveDirection moveDirection, double delta) { double newX; double newY; switch (moveDirection) { case UP: newY = circle.getCenterY() - delta; if (newY >= circle.getRadius()) { // Ak kruh nevyjde von zo sceny, posun ho circle.setCenterY(newY); } else { // V opacnom pripade zmen smer o 180 stupnov this.moveDirection = MoveDirection.DOWN; } break; case DOWN: newY = circle.getCenterY() + delta; if (newY <= scene.getHeight() - circle.getRadius()) { // Ak kruh nevyjde von zo sceny, posun ho circle.setCenterY(newY); } else { // V opacnom pripade zmen smer o 180 stupnov this.moveDirection = MoveDirection.UP; } break; case LEFT: newX = circle.getCenterX() - delta; if (newX >= circle.getRadius()) { // Ak kruh nevyjde von zo sceny, posun ho circle.setCenterX(newX); } else { // V opacnom pripade zmen smer o 180 stupnov this.moveDirection = MoveDirection.RIGHT; } break; case RIGHT: newX = circle.getCenterX() + delta; if (newX <= scene.getWidth() - circle.getRadius()) { // Ak kruh nevyjde von zo sceny, posun ho circle.setCenterX(newX); } else { // V opacnom pripade zmen smer o 180 stupnov this.moveDirection = MoveDirection.LEFT; } break; } } @Override public void start(Stage primaryStage) { Pane pane = new Pane(); Scene scene = new Scene(pane, 400, 400); Random random = new Random(); double radius = 20; // Fixny polomer kruhu double x = radius + (random.nextDouble() * (scene.getWidth() - 2 * radius)); // Nahodna pociatocna x-ova suradnica kruhu double y = radius + (random.nextDouble() * (scene.getHeight() - 2 * radius)); // Nahodna pociatocna y-ova suradnica kruhu Circle circle = new Circle(x , y, radius); // Vytvorenie kruhu s danymi parametrami pane.getChildren().add(circle); circle.setFill(Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble())); moveDirection = MoveDirection.values()[random.nextInt(4)]; // Nahodne zvoleny pociatocny smer pohybu circle.requestFocus(); // Kruh dostane fokus, aby mohol reagovat na klavesnicu circle.setOnKeyPressed((KeyEvent e) -> { // Nastavime reakciu kruhu na stlacenie klavesy switch (e.getCode()) { case UP: // Ak bola stlacena niektora zo sipok, zmenime podla nej smer moveDirection = MoveDirection.UP; break; case RIGHT: moveDirection = MoveDirection.RIGHT; break; case DOWN: moveDirection = MoveDirection.DOWN; break; case LEFT: moveDirection = MoveDirection.LEFT; break; } }); AnimationTimer animationTimer = new AnimationTimer() { // Vytvorenie casovaca private long lastMoveTime = 0; // Casova peciatka posledneho pohybu kruhu private long lastColorChangeTime = 0; // Casova peciatka poslednej zmeny farby kruhu @Override public void handle(long now) { // Ak bol kruh naposledy posunuty pred viac ako 20 milisekundami, posun ho o 5 pixelov if (now - lastMoveTime >= 20000000) { moveCircle(scene, circle, moveDirection, 5); lastMoveTime = now; } // Ak sa farba kruhu naposledy zmenila pred viac ako 500 milisekundami, zmen ju nahodne if (now - lastColorChangeTime >= 500000000) { circle.setFill(Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble())); lastColorChangeTime = now; } } }; animationTimer.start(); // Spusti casovac primaryStage.setScene(scene); primaryStage.setTitle("Pohyblivý kruh"); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
Cvičenia 18
- Vytvorte JavaFX aplikáciu realizujúcu prevod uhla v stupňoch na radiány a naopak.
- Vytvorte JavaFX aplikáciu, ktorá:
- Načíta (z konzoly alebo zo súboru) prirodzené číslo N.
- Vytvorí scénu pozostávajúcu z N tlačidiel s nápismi zodpovedajúcimi číslam od 1 po N. Ako koreňovú oblasť scény môžete použiť napríklad FlowPane.
- Oživte jednotlivé tlačidlá v predchádzajúcej aplikácii tak, aby každé z nich do konzoly (alebo do textového popisku na scéne) vypisovalo svoje číslo. Implementujte túto funkcionalitu dvoma spôsobmi:
- So samostatným EventHandler-om pre každé z tlačidiel.
- S jediným EventHandler-om spracúvajúcim udalosti každého z tlačidiel (k zdroju udalosti event možno pristupovať metódou event.getSource()).
- Vytvorte aplikáciu s tlačidlom Pridaj, ktoré bude po stlačení na scéne vytvárať ďalšie tlačidlá s postupne rastúcimi číslami (každé z nich navyše môže svoje číslo vypisovať na konzolu). Skúste na rozloženie tlačidiel použiť rôzne podtriedy triedy Pane.
- Upravte predchádzajúcu aplikáciu tak, aby sa namiesto tlačidiel vytvárali štvorčeky nejakej farby, v ktorých strede bude text s daným číslom (na vytváranie takýchto štvorčekov použite StackPane).
- Vytvorte aplikáciu s jediným tlačidlom štvorcového tvaru umiestneným v oblasti základného typu Pane. Po stlačení tlačidla sa jeho veľkosť zväčší o nejaký konštantný faktor. Po stlačení niektorej zo šípok na klávesnici sa zmení poloha tlačidla v rámci scény. Na menenie veľkosti tlačidla použite metódy setPrefWidth resp. setPrefHeight. Jeho polohu môžete upravovať metódami setLayoutX a setLayoutY.
Prednáška 31
Oznamy
- Termín opravnej písomky bol stanovený na pondelok 15. apríla o 16:30. Písať sa bude v miestnosti M-II.
- Domácu úlohu č. 8 je potrebné odovzdať do stredy 10. apríla, 22:00.
- Bola zverejnená domáca úloha č. 9 (za 20 bodov), ktorú treba odovzdať do pondelka 29. apríla, 22:00.
- V prípade záujmu o nepovinný projekt si do štvrtka 2. mája, 22:00 vyberte tému. Do 15. apríla môžete navrhovať aj vlastné témy, ktoré v prípade schválenia budú k dispozícii pre všetkých.
Zložitejšie ovládacie prvky a aplikácie s viacerými oknami: jednoduchý textový editor
Cieľom tejto prednášky je demonštrovať použitie niektorých zložitejších ovládacích prvkov v JavaFX (ako napríklad Menu, RadioButton, či ListView) a štandardných dialógov (Alert resp. FileChooser), ako aj základné techniky návrhu aplikácií pozostávajúcich z viac ako jedného okna.
Urobíme tak na ukážkovej aplikácii (opäť až priveľmi) jednoduchého textového editora, ktorý bude zvládať nasledujúce úkony:
- Vytvorenie prázdneho textového dokumentu a jeho následná modifikácia.
- Vytvorenie textového dokumentu pozostávajúceho z nejakého fixného počtu náhodných cifier (nezmyselná funkcionalita slúžiaca len na ukážku možností triedy Menu).
- Načítanie textu z používateľom zvoleného textového súboru (v našom prípade budeme predpokladať kódovanie UTF-8).
- Uloženie textu do súboru.
- V prípade požiadavky na zatvorenie neuloženého súboru výzva na jeho uloženie.
- Do určitej miery aj zmena fontu, ktorým sa text vypisuje.
Základ aplikácie
Ako koreňovú oblasť hlavného okna aplikácie zvolíme oblasť typu BorderPane, s ktorým sme sa stretli už minule. Vzhľadom na o niečo väčší rozsah našej aplikácie sa navyše zdá rozumné nepracovať výhradne s lokálnymi premennými metódy start, ale dôležitejšie ovládacie prvky uchovávať ako premenné samotnej hlavnej triedy Editor, čo umožňí ich neskoršiu modifikáciu z rôznych pomocných metód. Takto si okrem iného budeme uchovávať aj referenciu primaryStage na hlavné okno aplikácie.
Základ nášho programu tak môže vyzerať napríklad nasledovne:
package editor; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.layout.*; import javafx.scene.control.*; import javafx.event.*; public class Editor extends Application { private Stage primaryStage; @Override public void start(Stage primaryStage) { this.primaryStage = primaryStage; BorderPane border = new BorderPane(); Scene scene = new Scene(border, 800, 600); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
Predpokladajme, že titulok hlavného okna má obsahovať text Textový editor, za ktorým v zátvorke nasleduje názov momentálne otvoreného súboru (alebo informácia o tom, že dokument nie je uložený v žiadnom súbore). Za zátvorkou sa navyše bude zobrazovať znak * v prípade, že sa obsah dokumentu od jeho posledného uloženia zmenil.
Aktuálne otvorený súbor si budeme pamätať v premennej openedFile; v prípade, že nie je otvorený žiaden súbor, bude hodnota tejto premennej null. Premenná openedFileChanged bude rovná true práve vtedy, keď od posledného uloženia dokumentu došlo k jeho zmene. Metóda updateOpenedFileInformation dostane dvojicu premenných s rovnakým významom a nastaví podľa nich premenné openedFile a openedFileChanged; vhodným spôsobom pritom upraví aj titulok hlavného okna. Z metódy start budeme volať updateOpenedFileInformation(null, true), keďže po spustení aplikácie nebude dokument uložený v žiadnom súbore a jeho obsah sa (triviálne) od posledného uloženia zmenil.
... import java.io.*; ... public class Editor extends Application { private File openedFile; private boolean openedFileChanged; ... private void updateOpenedFileInformation(File file, boolean hasChanged) { openedFile = file; openedFileChanged = hasChanged; String changeIndicator; if (hasChanged) { changeIndicator = "*"; } else { changeIndicator = ""; } String paren; if (file == null) { paren = "(neuložené v žiadnom súbore)" + changeIndicator; } else { paren = "(" + file.getName() + ")" + changeIndicator; } primaryStage.setTitle("Textový editor " + paren); } @Override public void start(Stage primaryStage) { ... updateOpenedFileInformation(null, true); ... } ... }
Ovládací prvok TextArea
Môžeme pokračovať pridaním kľúčového ovládacieho prvku našej aplikácie – priestoru na písanie samotného textu. Takýto ovládací prvok má v JavaFX názov TextArea a my inštanciu tejto triedy zvolíme za stredovú časť koreňovej oblasti border.
Podobne ako vyššie budeme referenciu textArea na prvok typu TextArea uchovávať ako premennú triedy Editor. Navyše si v premenných triedy Editor budeme pamätať aj kľúčové atribúty fontu, ktoré vhodne inicializujeme. Na font použitý v priestore textArea tieto atribúty aplikujeme v pomocnej metóde applyFont, ktorú zavoláme hneď po inicializácii premennej textArea.
... import javafx.geometry.*; import javafx.scene.text.*; ... public class Editor extends Application { ... private TextArea textArea; private String fontFamily = "Tahoma"; private FontWeight fontWeight = FontWeight.NORMAL; private FontPosture fontPosture = FontPosture.REGULAR; private double fontSize = 16; ... private void applyFont() { textArea.setFont(Font.font(fontFamily, fontWeight, fontPosture, fontSize)); } ... @Override public void start(Stage primaryStage) { ... textArea = new TextArea(); border.setCenter(textArea); textArea.setPadding(new Insets(5,5,5,5)); applyFont(); ... } }
Vlastnosti a spracovanie ich zmeny
Chceli by sme teraz pomocou metódy updateOpenedFileInformation prestaviť premennú openedFileChanged na true zakaždým, keď sa v textovom poli udeje nejaká zmena (viditeľný efekt to bude mať až po implementácii ukladania do súboru; po vhodných dočasných zmenách v našom programe ale môžeme funkčnosť nasledujúceho kódu testovať už teraz).
To znamená: zakaždým, keď sa zmení obsah priestoru textArea, potrebujeme vykonať nasledujúcu metódu:
private void handleTextAreaChange() { if (!openedFileChanged) { updateOpenedFileInformation(openedFile, true); } }
Aby sme takúto akciu vedeli vykonať po každej zmene textového obsahu priestoru textArea, využijeme mechanizmus takzvaných vlastností. Pod vlastnosťou sa v JavaFX rozumie trieda implementujúca generické rozhranie Property<T> a možno si ju predstaviť ako „značne pokročilý obal pre nejakú hodnotu typu T”.
Podobne ako sme k ovládacím prvkom pridávali spracúvateľov udalostí, možno k vlastnostiam pridávať „spracúvateľov zmien”, ktoré sa vykonajú zakaždým, keď sa zmení hodnota obalená danou vlastnosťou. Týmito spracúvateľmi však teraz nebudú inštancie tried implementujúcich rozhranie EventHandler<E>, ale inštancie tried implementujúcich rozhranie ChangeListener<T>. Takéto rozhranie vyžaduje implementáciu jedinej metódy
void changed(ObservableValue<? extends T> observable, T oldValue, T newValue)
ktorá sa vykoná pri každej zmene vlastnosti observable z oldValue na newValue. Ide pritom o funkcionálne rozhranie, takže na jeho implementáciu možno použiť aj lambda výrazy.
Vráťme sa teraz k nášmu textovému editoru: textový obsah priestoru textArea je reprezentovaný ako vlastnosť, ktorú môžeme získať volaním metódy textArea.textProperty(). Ide tu o inštanciu triedy StringProperty implementujúcej rozhranie Property<String>. Môžeme tak pre ňu zaregistrovať „spracúvateľa zmien” pomocou metódy addListener, ktorej jediným argumentom bude inštancia takéhoto spracúvateľa. Môžeme to urobiť s využitím anonymnej triedy
import javafx.beans.value.*; ... public void start(Stage primaryStage) { ... textArea.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { handleTextAreaChange(); } }); ... }
alebo alternatívne prostredníctvom lambda výrazu
import javafx.beans.value.*; ... public void start(Stage primaryStage) { ... textArea.textProperty().addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) -> { handleTextAreaChange(); }); ... }
Poznámky:
- Textový obsah priestoru typu TextArea je v JavaFX iba jednou z obrovského množstva vlastností, na ktorých zmenu možno reagovať. Ovládacie prvky typicky ponúkajú veľké množstvo vlastností, o ktorých sa možno dočítať v dokumentácii (ako príklady uveďme napríklad text alebo font tlačidla resp. textového popisku, rozmery okna, atď.).
- V prípade, že nejaký ovládací prvok ponúka vlastnosť, ku ktorej sa pristupuje metódou cokolvekProperty, typicky ponúka aj metódu getCokolvek, ktorá vráti hodnotu obalenú danou vlastnosťou. V prípade, že možno meniť hodnotu danej vlastnosti, môže byť k dispozícii aj metóda setCokolvek.
- Vlastnosti navyše možno medzi sebou aj vzájomne previazať (napríklad veľkosť kruhu vykresleného na scéne možno previazať s veľkosťou okna tak, aby bol polomer kruhu rovný tretine menšieho z rozmerov okna...). S príkladom previazania vlastností sa stretneme nižšie.
- Treba upozorniť na to, že v iných jazykoch sa pod vlastnosťami často rozumie niečo úplne odlišné.
Hlavné ponuky (MenuItem, Menu a MenuBar)
Kľúčovou súčasťou mnohých aplikácií býva hlavná ponuka (menu). Hlavnú ponuku možno v JavaFX vytvoriť nasledujúcim spôsobom:
- Do hlavného okna aplikácie sa umiestní ovládací prvok typu MenuBar, ktorý reprezentuje priestor, v ktorom sa budú jednotlivé ponuky zobrazovať. Každý MenuBar si udržiava zoznam ponúk v ňom umiestnených.
- Každá ponuka (ako napríklad Súbor, Formát, ...) je reprezentovaná inštanciou triedy Menu, ktorá si okrem iného pamätá zoznam všetkých položiek danej ponuky.
- Položka ponuky je reprezentovaná inštanciou triedy MenuItem. Každej položke možno napríklad pomocou metódy setOnAction priradiť akciu, ktorá sa má vykonať po jej zvolení používateľom.
- Trieda Menu je podtriedou triedy MenuItem, z čoho okrem iného vyplýva, že položkou ponuky môže byť aj ďalšia podponuka.
- Špeciálne položky ponúk sú reprezentované triedami CheckMenuItem(takúto položku ponuky možno zvolením zaškrtnúť resp. odškrtnúť) a SeparatorMenuItem (reprezentuje vodorovnú čiaru na vizuálne oddelenie častí ponuky).
V našej aplikácii teraz vytvoríme MenuBar s dvojicou ponúk Súbor a Formát s nasledujúcou štruktúrou:
Súbor (Menu) Formát (Menu) | | |- Nový (Menu) --- Prázdny súbor (MenuItem) |- Písmo... (MenuItem) | | | | |- Náhodné cifry (MenuItem) |- Zalamovať riadky (CheckMenuItem) | |- Otvoriť... (MenuItem) | |- Uložiť (MenuItem) | |- Uložiť ako... (MenuItem) | |--------------- (SeparatorMenuItem) | |- Koniec (MenuItem)
Vytvorenie takýchto ponúk realizujeme nasledujúcim kódom (v ktorom ponuky a ich položky reprezentujeme ako premenné triedy Editor, kým MenuBar vytvárame iba lokálne v metóde start):
... public class Editor extends Application { ... private Menu mFile; private Menu mFileNew; private MenuItem miFileNewEmpty; private MenuItem miFileNewRandom; private MenuItem miFileOpen; private MenuItem miFileSave; private MenuItem miFileSaveAs; private MenuItem miFileExit; private Menu mFormat; private MenuItem miFormatFont; private CheckMenuItem miFormatWrap; ... @Override public void start(Stage primaryStage) { ... MenuBar menuBar = new MenuBar(); border.setTop(menuBar); mFile = new Menu("Súbor"); mFileNew = new Menu("Nový"); miFileNewEmpty = new MenuItem("Prázdny súbor"); miFileNewRandom = new MenuItem("Náhodné cifry"); mFileNew.getItems().addAll(miFileNewEmpty, miFileNewRandom); miFileOpen = new MenuItem("Otvoriť..."); miFileSave = new MenuItem("Uložiť"); miFileSaveAs = new MenuItem("Uložiť ako..."); miFileExit = new MenuItem("Koniec"); mFile.getItems().addAll(mFileNew, miFileOpen, miFileSave, miFileSaveAs, new SeparatorMenuItem(), miFileExit); mFormat = new Menu("Formát"); miFormatFont = new MenuItem("Písmo..."); miFormatWrap = new CheckMenuItem("Zalamovať riadky"); miFormatWrap.setSelected(false); // Nie je nutne, kedze false je tu vychodzia hodnota mFormat.getItems().addAll(miFormatFont, miFormatWrap); menuBar.getMenus().add(mFile); menuBar.getMenus().add(mFormat); ... } }
K dôležitejším položkám môžeme priradiť aj klávesové skratky:
@Override public void start(Stage primaryStage) { ... miFileNewEmpty.setAccelerator(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN)); // Ctrl + N miFileOpen.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN)); // Ctrl + O miFileSave.setAccelerator(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN)); // Ctrl + S ... }
V rámci metódy updateOpenedFileInformation ešte môžeme zabezpečiť, aby položka miFileSave bola aktívna práve vtedy, keď má premenná hasChanged hodnotu true (v opačnom prípade nie je čo ukladať):
private void updateOpenedFileInformation(File file, boolean hasChanged) { ... if (hasChanged) { ... miFileSave.setDisable(false); } else { ... miFileSave.setDisable(true); } ... }
Výsledný vzhľad aplikácie je na obrázku vpravo.
Kontextové ponuky (ContextMenu)
Ďalším užitočným typom ponúk sú kontextové (resp. vyskakovacie) ponuky, ktoré sa zobrazia po kliknutí na nejaký ovládací prvok pravou myšou. Všimnime si, že TextArea už prichádza s prednastavenou kontextovou ponukou. Chceli by sme teraz túto ponuku nahradiť vlastnou obsahujúcu rovnaké dve položky ako ponuka mFormat (budeme však musieť tieto položky vytvárať nanovo, pretože každá položka môže patriť iba do jedinej ponuky).
Jediným rozdielom oproti tvorbe hlavnej ponuky bude spočívať v použití inštancie triedy ContextMenu. Tú následne s použitím špeciálnej metódy setContextMenu priradíme ako kontextové ponuku ovládaciemu prvku textArea. Výsledný vzhľad kontextovej ponuky je na obrázku vpravo.
public class Editor extends Application { ... private ContextMenu cm; private MenuItem cmiFormatFont; private CheckMenuItem cmiFormatWrap; ... @Override public void start(Stage primaryStage) { ... cm = new ContextMenu(); cmiFormatFont = new MenuItem("Formát písma..."); cmiFormatWrap = new CheckMenuItem("Zalamovať riadky"); cmiFormatWrap.setSelected(false); cm.getItems().addAll(cmiFormatFont, cmiFormatWrap); textArea.setContextMenu(cm); ... } }
Priradenie udalostí k jednotlivým položkám ponúk
Môžeme teraz k jednotlivým položkám ponúk (okrem položiek typu CheckMenuItem) priradiť ich funkcionalitu, ktorá bude zatiaľ pozostávať z volania metód s prázdnym telom. Všetky tieto metódy budú mať návratový typ boolean, pričom výstupná hodnota bude hovoriť o tom, či sa zamýšľaná akcia podarila alebo nie – táto črta sa nám zíde neskôr.
public class Editor extends Application { ... private boolean newEmptyAction() { return true; // Neskor nahradime zmysluplnym telom metody } private boolean newRandomAction() { return true; } private boolean openAction() { return true; } private boolean saveAction() { return true; } private boolean saveAsAction() { return true; } private boolean exitAction() { return true; } private boolean fontAction() { return true; } ... @Override public void start(Stage primaryStage) { ... cmiFormatFont.setOnAction((ActionEvent event) -> { fontAction(); }); ... miFileNewEmpty.setOnAction((ActionEvent e) -> { newEmptyAction(); }); miFileNewRandom.setOnAction((ActionEvent e) -> { newRandomAction(); }); miFileOpen.setOnAction((ActionEvent event) -> { openAction(); }); miFileSave.setOnAction((ActionEvent event) -> { saveAction(); }); miFileSaveAs.setOnAction((ActionEvent event) -> { saveAsAction(); }); miFileExit.setOnAction((ActionEvent event) -> { exitAction(); }); miFormatFont.setOnAction((ActionEvent event) -> { fontAction(); }); ... } }
Previazanie vlastností
Na implementáciu funkcionality položiek miFormatWrap a cmiFormatWrap použijeme ďalšiu črtu vlastností – možnosť ich (obojstranného) previazania. Pri zmene niektorej z vlastností sa automaticky zmenia aj všetky vlastnosti s ňou previazané. V našom prípade navzájom previažeme vlastnosti hovoriace o zaškrtnutí položiek miFormatWrap a cmiFormatWrap a tiež vlastnosť hovoriacu o zalamovaní riadkov v textovom priestore textArea:
@Override public void start(Stage primaryStage) { ... miFormatWrap.selectedProperty().bindBidirectional(cmiFormatWrap.selectedProperty()); miFormatWrap.selectedProperty().bindBidirectional(textArea.wrapTextProperty()); ... }
- Previazanie vlastností možno využiť aj v rôzličných ďalších situáciách. Užitočným cvičením môže byť napísať aplikáciu, v ktorej hlavnom okne je vykreslený kruh, ktorého polomer ostáva rovný jednej tretine menšieho z rozmerov okna (a to aj v prípade, že sa rozmery okna zmenia). Pri tejto úlohe sa zídu metódy triedy Bindings.
- Viac sa o vlastnostiach a ich previazaní možno dočítať napríklad v oficiálnom tutoriáli.
Jednoduché dialógy (Alert)
Naším najbližším cieľom teraz bude implementácia metód newEmptyAction, newRandomAction a exitAction. Spoločným menovateľom týchto akcií je, že vyžadujú zatvorenie práve otvoreného dokumentu. V takom prípade by sme ale chceli (pokiaľ boli v práve otvorenom dokumente urobené nejaké neuložené zmeny) zobraziť výzvu na uloženie dokumentu (ako na obrázku nižšie).
Na zobrazenie takejto výzvy využijeme jeden z jednoduchých dialógov – inštanciu triedy Alert:
... import java.util.*; // Kvoli triede Optional ... public class Editor extends Application { ... private boolean saveBeforeClosingAlert() { if (openedFileChanged) { // Ak dokument nie je ulozeny Alert alert = new Alert(Alert.AlertType.CONFIRMATION); // Vytvor novy dialog Alert typu AlertType.CONFIRMATION alert.setTitle("Uložiť súbor?"); // Nastav titulok dialogu alert.setHeaderText(null); // Dialog nebude mat ziaden "nadpis" alert.setContentText("Uložiť zmeny v súbore?"); // Nastav text dialogu ButtonType buttonTypeYes = new ButtonType("Áno"); // Nastav typy tlacidiel ButtonType buttonTypeNo = new ButtonType("Nie"); // Typ tlacidla "Zrušiť" dostane aj druhy argument, vdaka ktoremu sa bude rovnako spravat aj "krizik" vpravo hore: ButtonType buttonTypeCancel = new ButtonType("Zrušiť", ButtonBar.ButtonData.CANCEL_CLOSE); alert.getButtonTypes().setAll(buttonTypeYes, buttonTypeNo, buttonTypeCancel); // Prirad typy tlacidiel k dialogu Optional<ButtonType> result = alert.showAndWait(); // Zobraz dialog a cakaj, kym sa zavrie if (result.get() == buttonTypeYes) { // Dalsie akcie vykonavaj podla typu stlaceneho tlacidla return saveAction(); } else if (result.get() == buttonTypeNo) { return true; } else { return false; } } else { // Ak je dokument ulozeny, netreba robit nic return true; } } ... }
Môžeme teraz pristúpiť k implementácii spomínaných troch akcií:
public class Editor extends Application { ... private final int N = 1000; // V newRandomAction() budeme generovat N riadkov o N nahodnych cifrach ... private boolean newEmptyAction() { if (saveBeforeClosingAlert()) { // Pokracuj len ak sa podarilo zavriet dokument updateOpenedFileInformation(null, true); // Nebude teraz otvoreny ziaden subor... textArea.clear(); // Zmazeme obsah textoveho priestoru textArea return true; } else { return false; } } private boolean newRandomAction() { if (newEmptyAction()) { // Skus vytvorit novy subor a pokracuj len, ak sa to podarilo StringBuilder sb = new StringBuilder(); // Vygeneruj retazec o N x N nahodnych cifrach Random random = new Random(); for (int i = 1; i <= N; i++) { for (int j = 1; j <= N; j++) { sb.append(Integer.toString(random.nextInt(10))); } sb.append(System.lineSeparator()); } textArea.setText(sb.toString()); // Vypis vygenerovany retazec do textoveho priestoru textArea return true; } else { return false; } } private boolean exitAction() { if (saveBeforeClosingAlert()) { // Pokracuj len ak sa podarilo zavriet dokument Platform.exit(); // Ukonci aplikaciu return true; } else { return false; } } }
Ďalšie typy jednoduchých dialógov
V JavaFX možno využívať aj ďalšie preddefinované jednoduché dialógy – od Alert-u s odlišným AlertType až po dialógy ako TextInputDialog alebo ChoiceDialog.
- Viac sa o preddefinovaných jednoduchých dialógoch v JavaFX možno dočítať napríklad tu.
Zatvorenie hlavného okna aplikácie „krížikom”
Metódu exitAction(), ktorá sa vykoná zakaždým, keď používateľ zvolí v hlavnej ponuke možnosť Súbor -> Koniec, sme implementovali tak, aby sa najprv zobrazila prípadná výzva na uloženie súboru. Táto výzva pritom v niektorých prípadoch môže aj ukončeniu aplikácie zamedziť (napríklad keď používateľ klikne na tlačidlo Zrušiť).
Ak ale používateľ aplikáciu zavrie kliknutím na „krížik” v pravom hornom rohu okna, aplikácia sa zavrie bez akejkoľvek ďalšej akcie. Chceli by sme pritom, aby sa vykonali rovnaké operácie, ako pri zvolení možnosti Súbor -> Koniec. To môžeme urobiť napríklad takto:
public class Editor extends Application { ... // Metoda, ktora sa bude vykonavat pri pokuse o zatvorenie hlavneho okna: private boolean handleStageCloseRequest(WindowEvent event) { if (saveBeforeClosingAlert()) { // Ak sa podarilo zavriet subor return true; } else { // Ak sa nepodarilo zavriet subor ... event.consume(); // ... nechceme ani zavriet okno, a teda poziadavku na zatvorenie okna skonzumujeme return false; } } ... @Override public void start(Stage primaryStage) { ... // Udalost onCloseRequest vznikne pri pokuse o zavretie okna aplikacie: primaryStage.setOnCloseRequest((WindowEvent event) -> { handleStageCloseRequest(event); }); ... } }
Otvárací a ukladací dialóg (FileChooser)
Implementujeme teraz kľúčovú funkcionalitu textového editora – metódy openAction, saveAction a saveAsAction realizujúce otváranie resp. ukladanie textových súborov.
Na výber súboru môžeme pri oboch typoch akcií využiť preddefinovaný dialóg FileChooser.
Pomocné metódy realizujúce výber súboru na otvorenie resp. uloženie môžu s jeho použitím vyzerať napríklad takto:
public class Editor extends Application { ... // Pomocna metoda inicializujuca niektore parametre FileChooser-a: private FileChooser prepareFileChooser() { FileChooser fileChooser = new FileChooser(); // Vytvorenie dialogu fileChooser.setInitialDirectory(new File(System.getProperty("user.dir"))); // Nastavenie vychodzieho adresara fileChooser.getExtensionFilters().addAll( // Filtre na pripony new FileChooser.ExtensionFilter("Textové súbory (*.txt)", "*.txt"), new FileChooser.ExtensionFilter("Všetky súbory (*.*)", "*.*")); return fileChooser; // Vytvoreny dialog metoda vrati na vystupe } // Pomocna metoda realizujuca vyber suboru na otvorenie: private File chooseFileToOpen() { FileChooser fileChooser = prepareFileChooser(); fileChooser.setTitle("Otvoriť"); // Nastavi titulok dialogu fileChooser File file = fileChooser.showOpenDialog(primaryStage); // Vrati subor vybrany v dialogu (null, ak dialog neskoncil potvrdenim vyberu) return file; } // Pomocna metoda realizujuca vyber suboru na ulozenie: private File chooseFileToSave() { FileChooser fileChooser = prepareFileChooser(); fileChooser.setTitle("Uložiť"); File file = fileChooser.showSaveDialog(primaryStage); return file; } ... }
Samotná implementácia otváracích a ukladacích metód môže potom vyzerať takto:
... import java.nio.file.*; import java.nio.charset.*; ... public class Editor extends Application { ... /* Pomocne metody realizujuce citanie zo suboru a zapis do suboru. Obe predpokladaju kodovanie UTF-8. Samozrejme by sme mohli pouzit aj zname sposoby citania a zapisu (napr. cez BufferedReader a pod.). */ private String loadFromFile(File file) throws IOException { byte[] enc = Files.readAllBytes(Paths.get(file.getPath())); return new String(enc, StandardCharsets.UTF_8); } private void writeToFile(File file, String s) throws IOException { byte[] enc = s.getBytes(StandardCharsets.UTF_8); Files.write(Paths.get(file.getPath()), enc); } // Implementacia samotnych metod openAction, saveAction a saveAsAction: private boolean openAction() { if (saveBeforeClosingAlert()) { // Ak sa podarilo zavriet dokument File file = chooseFileToOpen(); // Vyber subor na otvorenie if (file != null) { // Ak bol nejaky subor vybrany ... try { textArea.setText(loadFromFile(file)); // ... vypis jeho obsah do textArea updateOpenedFileInformation(file, false); // Aktualizuj informacie o otvorenom subore } catch (IOException e) { System.err.println("Nieco sa pokazilo."); } return true; } else { return false; } } else { return false; } } private boolean saveAction() { if (openedFile == null) { // Ak nebol otvoreny ziaden subor ... return saveAsAction(); // ... realizuj to iste ako pri "Ulozit ako" } else { // V opacnom pripade ... try { writeToFile(openedFile, textArea.getText()); // ... prepis aktualne otvoreny subor updateOpenedFileInformation(openedFile, false); // Aktualizuj informacie o otvorenom subore } catch (IOException e) { System.err.println("Nieco sa pokazilo."); } return true; } } private boolean saveAsAction() { File file = chooseFileToSave(); // Vyber subor, do ktoreho sa ma ukladat if (file != null) { // Ak bol nejaky subor vybrany ... try { writeToFile(file, textArea.getText()); // ... zapis don obsah textArea updateOpenedFileInformation(file, false); // Aktualizuj informacie o otvorenom subore } catch (IOException e) { System.err.println("Nieco sa pokazilo."); } return true; } else { return false; } } ... }
Vlastné dialógy (aplikácie s viacerými oknami)
Narozdiel od dialógov na výber súboru JavaFX neobsahuje ako štandardnú súčasť žiaden dialóg na výber fontu (hoci viaceré takéto dialógy možno nájsť v externých knižniciach). Vytvoríme si teda dialóg vlastný (nebude však úplne dokonalý), čo využijeme predovšetkým ako príležitosť na demonštráciu niekoľkých ďalších aspektov práce s JavaFX:
- Dialóg na výber fontu bude realizovaný pomocou ďalšieho okna (Stage) aplikácie; ukážeme si teda základné techniky spravovania aplikácií s viacerými oknami.
- Tento dialóg navyše miestami schválne navrhneme trochu suboptimálne, čo nám umožní demonštrovať použitie ďalších dvoch ovládacích prvkov v JavaFX: ListView a predovšetkým RadioButton.
Dialóg na výber fontu budeme reprezentovať samostatnou triedou
class FontDialog { private final Stage stage; // Dialogove okno public FontDialog() { // ToDo: Dorobit implementaciu konstruktora BorderPane border = new BorderPane(); Scene scene = new Scene(border, 500, 360); stage = new Stage(); stage.initStyle(StageStyle.UTILITY); // Prejavi sa v systemovych ikonach okna stage.initModality(Modality.APPLICATION_MODAL); // Pocas zobrazenia okna sa nebude dat pristupovat k ostatnym oknam stage.setScene(scene); stage.setTitle("Formát písma"); } /* Metoda, ktora zobrazi dialogove okno stage, pricom vychodzie hodnoty ovladacich prvkov budu nastavene podla oldFontAttributes. V pripade, ze dialog skonci potvrdenim, vrati na vystupe vybrane atributy fontu. V pripade, ze dialog skonci inym sposobom, vrati null. */ public FontAttributes showFontDialog(FontAttributes oldFontAttributes) { // ToDo: Prerobit na zmysluplnu implementaciu stage.showAndWait(); // Od metody show() sa showAndWait() lisi tym, ze zvysok kodu sa vykona az po zavreti okna stage return new FontAttributes("Tahoma", FontWeight.NORMAL, FontPosture.REGULAR, 16); } }
kde FontAttributes je naša pomocná trieda slúžiaca ako obal niektorých atribútov fontu (ku ktorým sa pomocou štandardnej triedy Font nedá pristupovať):
class FontAttributes { private final String family; private final FontWeight weight; private final FontPosture posture; private final double size; public String getFamily() { return family; } public FontWeight getWeight() { return weight; } public FontPosture getPosture() { return posture; } public double getSize() { return size; } public FontAttributes(String family, FontWeight weight, FontPosture posture, double size) { this.family = family; this.weight = weight; this.posture = posture; this.size = size; } }
V hlavnej triede Editor potom vytvoríme inštanciu triedy FontDialog a implementujeme metódu fontAction:
public class Editor extends Application { ... private FontDialog fontDialog; ... private boolean fontAction() { FontAttributes fontAttributes = fontDialog.showFontDialog( new FontAttributes(fontFamily, fontWeight, fontPosture, fontSize)); if (fontAttributes != null) { fontFamily = fontAttributes.getFamily(); fontWeight = fontAttributes.getWeight(); fontPosture = fontAttributes.getPosture(); fontSize = fontAttributes.getSize(); applyFont(); } return true; } @Override public void start(Stage primaryStage) { ... fontDialog = new FontDialog(); ... } }
Ovládacie prvky ListView a RadioButton
Pridáme teraz do nášho dialógového okna jednotlivé ovládacie prvky tak, aby dialóg vyzeral ako na obrázku vpravo. Okrem dvojice tlačidiel pozostáva dialóg z ovládacích prvkov, s ktorými sme sa doposiaľ nestretli:
- Z jedného „zoznamu” typu ListView. Kľúčovou vlastnosťou ListView je jeho selectionModel, ktorý hovorí o móde výberu jednotlivých prvkov. My si vystačíme s východzím selectionModel-om, pri ktorom možno zo zoznamu vybrať najviac jeden prvok. Výber prvkov zoznamu je však vždy potrebné realizovať prostredníctvom jeho selectionModel-u.
- Z viacerých tlačidiel typu RadioButton. Toto pomenovanie je zvolené na základe toho, že pri ich typickom použití zvolenie jedného z tlačidiel „na diaľku” vypína doposiaľ zvolené tlačidlo v danej skupine.
Rozmiestnenie jednotlivých ovládacích prvkov na scéne realizujeme podobne ako na minulej prednáške; tentokrát však v konštruktore triedy FontDialog:
class FontDialog { ... private final ListView lboxFamilies; private final ArrayList<RadioButton> rbSizes; private final RadioButton rbRegular; private final RadioButton rbBold; private final RadioButton rbItalic; private final RadioButton rbBoldItalic; public FontDialog() { ... GridPane grid = new GridPane(); border.setCenter(grid); grid.setPadding(new Insets(10,15,10,15)); grid.setAlignment(Pos.CENTER); grid.setHgap(10); grid.setVgap(20); lboxFamilies = new ListView(); grid.add(lboxFamilies, 0, 0); lboxFamilies.getItems().addAll(Font.getFamilies()); // Do zoznamu pridame vsetky skupiny fontov, ktore su k dispozicii lboxFamilies.setPrefHeight(200); rbSizes = new ArrayList<>(); for (int i = 8; i <= 24; i += 2) { RadioButton rb = new RadioButton(Integer.toString(i)); rbSizes.add(rb); } VBox rbBox1 = new VBox(); rbBox1.setSpacing(10); rbBox1.setPadding(new Insets(0, 20, 0, 10)); rbBox1.getChildren().addAll(rbSizes); grid.add(rbBox1, 1, 0); rbRegular = new RadioButton("Obyčajný"); rbBold = new RadioButton("Tučný"); rbItalic = new RadioButton("Kurzíva"); rbBoldItalic = new RadioButton("Tučná kurzíva"); VBox rbBox2 = new VBox(); rbBox2.setSpacing(20); rbBox2.setPadding(new Insets(0, 10, 0, 20)); rbBox2.getChildren().addAll(rbRegular, rbBold, rbItalic, rbBoldItalic); grid.add(rbBox2, 2, 0); TilePane bottom = new TilePane(); border.setBottom(bottom); bottom.setPadding(new Insets(15, 15, 15, 15)); bottom.setAlignment(Pos.BASELINE_RIGHT); bottom.setHgap(10); bottom.setVgap(10); Button btnOK = new Button("Potvrdiť"); btnOK.setMaxWidth(Double.MAX_VALUE); Button btnCancel = new Button("Zrušiť"); btnCancel.setMaxWidth(Double.MAX_VALUE); bottom.getChildren().addAll(btnOK, btnCancel); ... } ... }
Skupiny RadioButton-ov (ToggleGroup)
Po otvorení dialógu vytvoreného vyššie zisťujeme, že je možné „zaškrtnúť” ľubovoľnú podmnožinu RadioButton-ov. Ak totiž nepovieme inak, každý z nich tvorí osobitnú skupinu.
V nasledujúcom zabezpečíme, aby bolo možné „zaškrtnúť” najviac jedno z tlačidiel v zozname rbSizes a najviac jedno zo zvyšných tlačidiel. Urobíme to tak, že každé z tlačidiel pridáme do zodpovedajúcej skupiny, pričom každá zo skupín bude reprezentovaná inštanciou triedy ToggleGroup:
class FontDialog { ... private final ToggleGroup tg1; // novy riadok private final ToggleGroup tg2; // novy riadok ... public FontDialog() { ... tg1 = new ToggleGroup(); // novy riadok rbSizes = new ArrayList<>(); for (int i = 8; i <= 24; i += 2) { RadioButton rb = new RadioButton(Integer.toString(i)); rb.setToggleGroup(tg1); // novy riadok rbSizes.add(rb); } ... tg2 = new ToggleGroup(); // novy riadok rbRegular = new RadioButton("Obyčajný"); rbBold = new RadioButton("Tučný"); rbItalic = new RadioButton("Kurzíva"); rbBoldItalic = new RadioButton("Tučná kurzíva"); rbRegular.setToggleGroup(tg2); // novy riadok rbBold.setToggleGroup(tg2); // novy riadok rbItalic.setToggleGroup(tg2); // novy riadok rbBoldItalic.setToggleGroup(tg2); // novy riadok ... } ... }
Dokončenie dialógu na výber fontu
Pridajme najprv funkcionalitu jednotlivých tlačidiel dialógu na výber fontu:
class FontDialog { ... private boolean confirmed; private void okAction() { confirmed = true; stage.close(); } private void cancelAction() { stage.close(); } public FontDialog() { ... btnOK.setOnAction((ActionEvent event) -> { okAction(); }); btnCancel.setOnAction((ActionEvent event) -> { cancelAction(); }); ... } ... }
Teraz už len ostáva implementovať metódu showFontDialog, ktorá:
- Podľa vstupných parametrov – atribútov doposiaľ zvoleného fontu – nastaví predvolené hodnoty v dialógu.
- Otvorí dialóg metódou stage.showAndWait (vykonávanie programu sa teda zablokuje, až kým sa dialóg nezavrie; tým sa táto metóda líši od metódy show).
- V prípade, že dialóg skončil potvrdením (confirmed == true), vráti na výstupe atribúty fontu na základe tých zvolených v dialógu.
Jej implementácia môže byť napríklad nasledovná:
class FontDialog { ... public FontAttributes showFontDialog(FontAttributes oldFontAttributes) { /* NASTAV PREDVOLENY FONT PODLA oldFontAttributes */ lboxFamilies.getSelectionModel().select(oldFontAttributes.getFamily()); rbSizes.get((int)((oldFontAttributes.getSize() - 8) / 2)).setSelected(true); if (oldFontAttributes.getWeight() == FontWeight.NORMAL) { if (oldFontAttributes.getPosture() == FontPosture.REGULAR) { rbRegular.setSelected(true); } else if (oldFontAttributes.getPosture() == FontPosture.ITALIC) { rbItalic.setSelected(true); } } else if (oldFontAttributes.getWeight() == FontWeight.BOLD) { if (oldFontAttributes.getPosture() == FontPosture.REGULAR) { rbBold.setSelected(true); } else if (oldFontAttributes.getPosture() == FontPosture.ITALIC) { rbBoldItalic.setSelected(true); } } /* OTVOR DIALOGOVE OKNO */ confirmed = false; stage.showAndWait(); /* AK DIALOG SKONCIL POTVRDENIM, VRAT ZVOLENE HODNOTY NA VYSTUPE */ if (confirmed) { String newFamily = ""; FontWeight newWeight; FontPosture newPosture; double newSize = 0; if (lboxFamilies.getSelectionModel().getSelectedItem() instanceof String) { newFamily = (String) lboxFamilies.getSelectionModel().getSelectedItem(); } if (rbRegular.isSelected() || rbItalic.isSelected()) { newWeight = FontWeight.NORMAL; } else { newWeight = FontWeight.BOLD; } if (rbRegular.isSelected() || rbBold.isSelected()) { newPosture = FontPosture.REGULAR; } else { newPosture = FontPosture.ITALIC; } int i = 8; for (RadioButton rb : rbSizes) { if (rb.isSelected()) { newSize = i; } i += 2; } return new FontAttributes(newFamily, newWeight, newPosture, newSize); } else { return null; } } }
Cvičenia
- Rozšírte textový editor o možnosť nastavovania farby fontu a farby výplne textového priestoru. Zísť sa tu môže ovládací prvok ColorPicker.
- Ukončenie aplikácie sme implementovali tak, že pokiaľ bol dokument od posledného uloženia zmenený, zobrazí sa výzva na jeho uloženie; pokiaľ zmenený nebol, aplikácia sa priamo ukončí. Upravte aplikáciu tak, aby sa aj v druhom prípade zobrazila výzva na potvrdenie ukončenia programu (avšak bez toho, aby sa aplikácia pýtala na uloženie dokumentu).
Textový editor: kompletný zdrojový kód
Cvičenia 19
- Ak ste na minulom cvičení nestihli všetky príklady, môžete začať s nimi.
- Pozrite si v prednáške alebo v dokumentácii základné informácie o triedach RadioButton a ToggleGroup.
- Vytvorte aplikáciu s dvoma tlačidlami, kde každé bude postupne pridávať RadioButton-y do svojej skupiny (ToggleGroup). Každý RadioButton môže byť označený napríklad poradovým číslom.
- Pridajte do aplikácie pre každú skupinu RadioButton-ov jeden textový popisok (Label), ktorý bude počas behu aplikácie obsahovať text práve zvoleného RadioButton-u v danej skupine. Implementujte túto funkcionalitu dvoma spôsobmi: pridaním EventHandler-ov jednotlivým RadioButton-om a pridaním ChangeListener-a na vlastnosť selectedToggleProperty() danej inštancie triedy ToggleGroup.
- Vytvorte aplikáciu, ktorá po stlačení tlačidla btn1 zobrazí nové okno obsahujúce textové pole a tlačidlo btn2. Po kliknutí na tlačidlo btn2 sa nové okno zavrie a nápis tlačidla btn1 sa zmení na text zadaný používateľom do textového poľa.
- S využitím zväzovania vlastností napíšte aplikáciu zobrazujúcu v strede scény kruh, ktorého polomer bude rovný tretine menšieho z rozmerov okna (a to aj po prípadnom menení týchto rozmerov používateľom). Môže sa tu zísť trieda Bindings.
Prednáška 32
Oznamy
- Dnes o 16:30 bude v miestnosti M-II opravná písomka.
- V stredu 17. apríla bude na cvičení rozcvička so zameraním na JavaFX.
- Cvičenie v stredu 24. apríla bude zamerané na grafy. Bude tiež zverejnená bonusová úloha na grafy, ktorá sa bude dať riešiť iba počas týchto cvičení.
- Posledné cvičenie bude v stredu 15. mája a bude na ňom rozcvička so zameraním na grafy.
Viacoknové aplikácie v JavaFX: jednoduchý príklad
Ukončime teraz tému tvorby aplikácií v JavaFX jednoduchým príkladom aplikácie s dvoma oknami (o niečo zložitejší príklad možno nájsť v minulotýždňovej prednáške, kde sa tiež hovorí o preddefinovaných dialógových oknách v JavaFX). Hlavné okno aplikácie bude pozostávať z jedného tlačidla a jedného textového popisku. Po stlačení tlačidla sa zobrazí druhé okno s dvoma tlačidlami (Áno resp. Nie). Každé z týchto tlačidiel toto druhé okno zavrie. Ak sa tak stane tlačidlom Áno, v textovom popisku hlavného okna sa objaví text Áno; ak je druhé okno zavreté tlačidlom Nie alebo „krížikom”, v textovom popisku sa objaví text Nie.
Základom aplikácie môže byť nasledujúci kód pre rozmiestnenie ovládacích prvkov v hlavnom okne:
package nejakaaplikacia; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.layout.*; import javafx.scene.control.*; import javafx.event.*; import javafx.geometry.*; public class NejakaAplikacia extends Application { Stage primaryStage; Button btnOpenDialog; Label lblResult; @Override public void start(Stage primaryStage) { this.primaryStage = primaryStage; GridPane grid = new GridPane(); grid.setAlignment(Pos.CENTER); grid.setPadding(new Insets(10,10,10,10)); grid.setHgap(10); grid.setVgap(10); btnOpenDialog = new Button("Otvor dialóg"); grid.add(btnOpenDialog, 0, 0); lblResult = new Label(""); grid.add(lblResult, 0, 1); Scene scene = new Scene(grid, 300, 200); primaryStage.setTitle("Hlavné okno"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } }
Najpriamočiarejší spôsob vytvorenia druhého okna (a jeho zobrazenia v prípade stlačenia tlačidla btnOpenDialog) potom môže vyzerať napríklad nasledovne:
public class NejakaAplikacia extends Application { ... Stage dialogStage; // Dialogove okno Button btnYes; Button btnNo; boolean result; // Pomocna premenna, ktora bude true prave vtedy, ked sa okno dialogStage zavrie tlacidlom Ano @Override public void start(Stage primaryStage) { ... /* Po stlaceni tlacidla btnOpenDialog zobraz dialogove okno a po jeho zavreti nastav text popisku lblResult podla premennej result: */ btnOpenDialog.setOnAction((ActionEvent e) -> { result = false; dialogStage.showAndWait(); if (result) { lblResult.setText("Áno"); } else { lblResult.setText("Nie"); } }); ... dialogStage = new Stage(); // Vytvor dialogove okno HBox hb = new HBox(); // Korenova oblast sceny dialogoveho okna hb.setSpacing(10); hb.setPadding(new Insets(10,10,10,10)); btnYes = new Button("Áno"); // Tlacidlo "Ano" ... btnYes.setOnAction((ActionEvent e) -> { // ... po jeho stlaceni sa premenna result nastavi na true a okno sa zavrie result = true; dialogStage.close(); }); btnNo = new Button("Nie"); // Tlacidlo "Nie" ... btnNo.setOnAction((ActionEvent e) -> { // ... po jeho stlaceni sa okno zavrie result = false; // (nepodstatny riadok) dialogStage.close(); }); hb.getChildren().addAll(btnYes, btnNo); Scene dialogScene = new Scene(hb, 120, 50); dialogStage.setScene(dialogScene); // Nastavenie sceny dialogoveho okna dialogStage.setTitle("Dialóg"); // Nastavenie titulku dialogoveho okna dialogStage.initModality(Modality.APPLICATION_MODAL); // Pocas zobrazenia dialogu nebude mozne pristupovat k hlavnemu oknu dialogStage.initStyle(StageStyle.UTILITY); // Jedinou systemovou ikonou okna bude "krizik" na jeho zavretie a pod. ... } }
O niečo elegantnejším prístupom je však vytvorenie samostatnej triedy (napr. CustomDialog) pre dialógové okno. Do konštruktora tejto triedy môžeme presunúť všetok kód rozmiestňujúci ovládacie prvky dialógového okna:
class CustomDialog { Stage dialogStage; Button btnYes; Button btnNo; boolean result; public CustomDialog() { dialogStage = new Stage(); HBox hb = new HBox(); hb.setSpacing(10); hb.setPadding(new Insets(10,10,10,10)); btnYes = new Button("Áno"); btnYes.setOnAction((ActionEvent e) -> { result = true; dialogStage.close(); }); btnNo = new Button("Nie"); btnNo.setOnAction((ActionEvent e) -> { result = false; dialogStage.close(); }); hb.getChildren().addAll(btnYes, btnNo); Scene dialogScene = new Scene(hb, 120, 50); dialogStage.setScene(dialogScene); dialogStage.setTitle("Dialóg"); dialogStage.initModality(Modality.APPLICATION_MODAL); dialogStage.initStyle(StageStyle.UTILITY); } public String showCustomDialog() { result = false; dialogStage.showAndWait(); if (result) { return "Áno"; } else { return "Nie"; } } } public class NejakaAplikacia extends Application { ... CustomDialog dialog; @Override public void start(Stage primaryStage) { ... dialog = new CustomDialog(); ... btnOpenDialog.setOnAction((ActionEvent e) -> { lblResult.setText(dialog.showCustomDialog()); }); } }
Grafy: úvod
Po zvyšok semestra sa budeme venovať práci s grafmi a implementácii jednoduchých grafových algoritmov.
- Na tomto predmete nebudeme grafy definovať matematicky – to je náplň predmetu „Úvod do kombinatoriky a teórie grafov”.
- Ďalšie predmety, na ktorých sa robí s grafmi:
- „Tvorba efektívnych algoritmov” (pokročilejšie grafové algoritmy).
- „Teória grafov” (teoretické aspekty grafov, niektoré grafové algoritmy).
- „Neštruktúrované rozpravy o štruktúrach: kapitoly z matematiky pre informatikov (1)” (prevažne súvis grafov s maticami).
Orientované a neorientované grafy
- Pod orientovaným grafom budeme rozumieť konečnú množinu vrcholov (zvyčajne {0,1,...,n-1} pre nejaké kladné prirodzené číslo n), kde medzi každou dvojicou vrcholov môže viesť najviac jedna orientovaná hrana. Vrcholy (angl. vertices) znázorňujeme bodmi resp. krúžkami, orientované hrany (angl. edges) šípkami. Nezaujímajú nás pritom geometrické vlastnosti diagramu grafu, ale iba to, či dané vrcholy sú alebo nie sú spojené hranou. Špeciálnym prípadom hrany je tzv. slučka – hrana s rovnakým počiatočným a koncovým vrcholom.
- V neorientovanom grafe nerozlišujeme orientáciu hrán; hrany tak namiesto šípok kreslíme „obyčajnými čiarami”. Neorientovaný graf budeme stotožňovať s orientovaným grafom, v ktorom existencia hrany z vrchola u do vrchola v (rôzneho od u) implikuje existenciu hrany z v do u.
Vybrané aplikácie grafov
- Grafy cestnej (resp. železničnej, leteckej, elektrickej, potrubnej, počítačovej...) siete.
- Modely zložitých sietí (napr. internet, interakcie proteínov, ľudský mozog...).
- Grafy molekúl (vrcholmi sú atómy a hranami väzby medzi nimi).
- Časové závislosti medzi činnosťami (ak činnosť u treba vykonať pred činnosťou v, vedie z u do v orientovaná hrana).
- Preferencie (napríklad pri tvorbe rozvrhov môžu byť hranami pospájané predmety s časmi, v ktorých sa musia vyučovať).
- Všeobecnejšie možno grafom zadať akúkoľvek konečnú binárnu reláciu.
- Niektoré modely výpočtov (booleovské obvody, konečné automaty...).
- Každý strom je súčasne aj grafom...
- ...
Reprezentácia grafov
Na dnešnej prednáške sa budeme zaoberať orientovanými a neorientovanými grafmi na množine vrcholov {0,1,...,n-1} pre kladné prirodzené n. Najužitočnejšími spôsobmi reprezentácie grafu v pamäti počítača sú nasledujúce dva:
Matica susednosti (angl. adjacency matrix)
- Hrany grafu reprezentujeme pomocou štvorcovej booleovskej matice A typu n x n. Pritom A[i][j] == true práve vtedy, keď z vedie hrana z vrchola i do vrchola j.
- Napríklad pre graf s vrcholmi V = {0,1,2,3,4} a hranami E = {(0,1),(1,2),(1,3),(2,3),(3,0),(3,3)}:
0 1 2 3 4 0 F T F F F 1 F F T T F 2 F F F T F 3 T F F T F 4 F F F F F
- Matica susednosti neorientovaného grafu je vždy symetrická.
Zoznamy susedov (angl. adjacency lists)
- Pre každý vrchol u si pamätáme zoznam vrcholov, do ktorých vedie z vrchola u hrana (pole, ArrayList, LinkedList, ...).
- Napríklad pre graf s vrcholmi V = {0,1,2,3} a hranami E = {(0,1),(1,2),(1,3),(2,3),(3,0),(3,3)}:
0: 1 1: 2, 3 2: 3 3: 0, 3 4:
- Pre neorientované hrany obsahuje každý zo zoznamov práve všetkých susedov daného vrcholu.
Graf ako abstraktný dátový typ: rozhranie Graph
- Skôr, než si ukážeme konkrétne implementácie grafu pomocou matíc susednosti aj zoznamov susedov, potrebujeme vedieť, aké operácie by mal graf poskytovať.
- Napíšeme preto jednoduché rozhranie pre graf definujúce metódy na pridávanie hrán, testovanie existencie hrán a prechádzanie cez všetkých susedov určitého vrcholu:
/* Rozhranie pre reprezentaciu grafu o vrcholoch 0,1,...,n-1 pre nejake prirodzene cislo n: */ interface Graph { int getNumberOfVertices(); // Vrati pocet vrcholov grafu. int getNumberOfEdges(); // Vrati pocet hran grafu. /* Prida hranu z vrchola from do vrchola to a vrati true, ak sa ju podarilo pridat: */ boolean addEdge(int from, int to); /* Vrati true, ak existuje hrana z vrchola from do vrchola to: */ boolean existsEdge(int from, int to); /* Vrati iterovatelnu skupinu pozostavajucu z prave vsetkych vrcholov, do ktorych vedie hrana z vrchola vertex. Pre neorientovane grafy ide o prave vsetkych susedov vrchola vertex: */ Iterable<Integer> adjVertices(int vertex); }
Výstupom metódy adjVertices je inštancia triedy implementujúcej rozhranie Iterable<Integer>. V ňom je predpísaná jediná metóda iterator(), ktorá vráti iterátor (v našom prípade cez prvky typu Integer). Inštancie inst tried implementujúcich Iterable<Integer> sa dajú použiť v cykle typu for (int x : inst). Napríklad:
/* Vypise orientovany graf g do vystupneho prudu out. */ static void printGraph(Graph g, PrintStream out) { int n = g.getNumberOfVertices(); out.println(n + " " + g.getNumberOfEdges()); for (int u = 0; u <= n - 1; u++) { for (int v : g.adjVertices(u)) { out.println(u + " " + v); } } }
Orientované grafy pomocou zoznamov susedov: trieda AdjListsGraph
- Pre každý vrchol u budeme udržiavať ArrayList vrcholov, do ktorých vedie z vrchola u hrana.
- V metóde adjVertices jednoducho vrátime tento ArrayList; „obalíme” ho však tak, aby sa nedal meniť.
/* Trieda reprezentujuca orientovany graf pomocou zoznamov (ArrayList-ov) susedov: */ class AdjListsGraph implements Graph { /* Pre kazdy vrchol zoznam vrcholov, do ktorych z daneho vrchola vedie hrana: */ private ArrayList<ArrayList<Integer>> adjLists; /* Pocet hran v grafe: */ private int numEdges; /* Konstruktor, ktory ako parameter dostane prirodzene cislo numVertices a vytvori graf o numVertices vrcholoch a bez jedinej hrany: */ public AdjListsGraph(int numVertices) { adjLists = new ArrayList<>(); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<>()); } numEdges = 0; } @Override public int getNumberOfVertices() { return adjLists.size(); } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { if (existsEdge(from, to)) { return false; } else { adjLists.get(from).add(to); numEdges++; return true; } } @Override public boolean existsEdge(int from, int to) { return adjLists.get(from).contains(to); } @Override public Iterable<Integer> adjVertices(int from) { // Zoznam adjLists.get(from) "obalime" tak, aby sa nedal menit: return Collections.unmodifiableList(adjLists.get(from)); } }
Orientované grafy pomocou matice susednosti: trieda AdjMatrixGraph
/* Trieda reprezentujuca orientovany graf pomocou matice susednosti: */ class AdjMatrixGraph implements Graph { /* Matica susednosti: */ private boolean[][] adjMatrix; /* Pocet hran v grafe: */ private int numEdges; /* Konstruktor, ktory ako parameter dostane prirodzene cislo numVertices a vytvori graf o numVertices vrcholoch a bez jedinej hrany: */ public AdjMatrixGraph(int numVertices) { adjMatrix = new boolean[numVertices][numVertices]; for (int i = 0; i <= numVertices - 1; i++) { for (int j = 0; j <= numVertices - 1; j++) { adjMatrix[i][j] = false; } } numEdges = 0; } @Override public int getNumberOfVertices() { return adjMatrix.length; } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { if (existsEdge(from, to)) { return false; } else { adjMatrix[from][to] = true; numEdges++; return true; } } @Override public boolean existsEdge(int from, int to) { return adjMatrix[from][to]; } @Override public Iterable<Integer> adjVertices(int vertex) { // Vytvorime pomocny zoznam susedov a vratime ho "obaleny" tak, aby sa nedal menit: ArrayList<Integer> a = new ArrayList<>(); for (int i = 0; i <= adjMatrix[vertex].length - 1; i++) { if (adjMatrix[vertex][i]) { a.add(i); } } return Collections.unmodifiableList(a); } }
Neorientované grafy: triedy AdjListsUndirectedGraph a AdjMatrixUndirectedGraph
Pri implementácii neorientovaných grafov môžeme využiť dedenie od prislúchajúcich tried reprezentujúcich orientované grafy. Narážame tu len na dva rozdiely: pridanie neorientovanej hrany metódou addEdge v skutočnosti zodpovedá pridaniu dvojice protichodných orientovaných hrán (s výnimkou slučiek, kde sa pridáva jediná orientovaná hrana) a metóda getNumberOfEdges by nemala vracať počet orientovaných hrán, ale počet neorientovaných hrán.
interface UndirectedGraph extends Graph { } /* Trieda reprezentujuca neorientovany graf pomocou zoznamov susedov reprezentovanych v podobe ArrayList-ov. */ class AdjListsUndirectedGraph extends AdjListsGraph implements UndirectedGraph { /* Pocet neorientovanych hran v grafe: */ private int numUndirectedEdges; public AdjListsUndirectedGraph(int numVertices) { super(numVertices); numUndirectedEdges = 0; } @Override public int getNumberOfEdges() { return numUndirectedEdges; } @Override public boolean addEdge(int from, int to) { boolean r = super.addEdge(from, to); super.addEdge(to, from); if (r) { numUndirectedEdges++; } return r; } } /* Trieda reprezentujuca neorientovany graf pomocou matice susednosti: */ class AdjMatrixUndirectedGraph extends AdjMatrixGraph implements UndirectedGraph { /* Pocet neorientovanych hran v grafe: */ private int numUndirectedEdges; public AdjMatrixUndirectedGraph(int numVertices) { super(numVertices); numUndirectedEdges = 0; } @Override public int getNumberOfEdges() { return numUndirectedEdges; } @Override public boolean addEdge(int from, int to) { boolean r = super.addEdge(from, to); super.addEdge(to, from); if (r) { numUndirectedEdges++; } return r; } }
Vytvorenie grafu
- Vytvoríme prázdny graf s určitým počtom vrcholov, následne po jednom pridávame hrany.
/* Metoda, ktora zo scannera s nacita graf vo formate: pocet vrcholov, pocet hran a zoznam hran zadanych dvojicami koncovych vrcholov. Ak undirected == true, vytvori neorientovany graf, inak orientovany. Ak matrix == true, vytvori graf reprezentovany maticou susednosti, v opacnom pripade vytvori graf reprezentovany zoznamami susedov. */ static Graph readGraph(Scanner s, boolean undirected, boolean matrix) { int n = s.nextInt(); int m = s.nextInt(); Graph g; if (undirected) { if (matrix) { g = new AdjMatrixUndirectedGraph(n); } else { g = new AdjListsUndirectedGraph(n); } } else { if (matrix) { g = new AdjMatrixGraph(n); } else { g = new AdjListsGraph(n); } } for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); g.addEdge(u, v); } return g; }
Porovnanie reprezentácií grafov
- Majme orientovaný graf s n vrcholmi a m hranami – počet hrán m teda môže byť od 0 po n2.
- Vyjadríme čas rôznych operácií v O-notácii:
- O(1): robíme len konštantný počet operácií bez ohľadu na veľkosť grafu.
- O(n): čas operácie rastie v najhoršom prípade lineárne s počtom vrcholov grafu.
- O(m): čas operácie rastie v najhoršom prípade lineárne s počtom hrán grafu.
- O(n2): čas operácie rastie v najhoršom prípade kvadraticky s počtom vrcholov grafu.
- Väčšinou máme viac hrán ako vrcholov, takže O(1) je lepšie ako O(n), to je lepšie ako O(m) a to je lepšie ako O(n2).
Matica susednosti | Zoznamy susedov | |
Pamäť | O(n2) | O(n+m) |
Vytvoriť graf bez hrán | O(n2) | O(n) |
addEdge | O(1) | O(n) |
existsEdge | O(1) | O(n) |
Prejdenie susedov vrchola | O(n) | O(stupeň vrchola) |
Výpis grafu pomocou adjVertices | O(n2) | O(n+m) |
- Matica susednosti:
- Rýchla operácia existsEdge.
- Ak je graf riedky (málo hrán), zaberá zbytočne veľa pamäte a dlho trvá prejdenie susedov vrchola.
- Maticová reprezentácia sa však dá využiť pri niektorých algoritmoch (viac vo vyšších ročníkoch).
- Zoznamy susednosti
- Vhodná reprezentácia aj pre riedke grafy.
- Dlho trvá nájdenie konkrétnej hrany, ale všetkých susedov vrchola vieme prejsť relatívne rýchlo.
- Najvhodnejšia reprezentácia pre väčšinu algoritmov, ktoré uvidíme.
Ďalšie varianty grafov
Grafy na tejto prednáške chápeme v relatívne obmedzenom slova zmysle. V praxi sa často zídu aj rôzne rozšírenia definície grafu:
- Grafy s násobnými hranami (niekde tiež multigrafy) umožňujú viesť medzi danou dvojicou vrcholov viacero paralelných hrán. To možno v pamäti počítača realizovať napríklad nahradením booleovskej matice maticou prirodzených čísel udávajúcich násobnosti jendotlivých hrán, prípadne nahradením zoznamov susedov zobrazeniami z množiny vrcholov do množiny prirodzených čísel.
- Ohodnotené grafy obsahujú na hranách nejakú ďalšiu prídavnú informáciu (napríklad pri cestnej sieti si môžeme pamätať dĺžku jednotlivých úsekov). Možno ich reprezentovať nahradením booleovskej matice maticou ohodnotení, prípadne nahradením zoznamov susedov zobrazeniami z množiny vrcholov do množiny ohodnotení.
- Dynamické grafy podporujú aj mazanie hrán a pridávanie a mazanie vrcholov...
Prehľadávanie neorientovaného grafu do hĺbky
Existencia cesty medzi dvojicou vrcholov
Zamerajme sa teraz na neorientované grafy a riešme nasledujúci problém: pre danú dvojicu vrcholov u a v zistiť, či sú spojené cestou (tzn. či sú tieto dva vrcholy v rovnakom komponente súvislosti grafu). Použijeme pritom prehľadávanie do hĺbky – podobné, ako sme už používali minulý semester pri vyfarbovaní súvislých oblastí v obdĺžnikovej mriežke. Procedúra na grafoch však bude všeobecnejšia:
- Mriežku môžeme reprezentovať grafom, v ktorom vrcholy zodpovedajú políčkam mriežky. Dvojica vrcholov je navyše spojená hranou práve vtedy, keď zodpovedajúce políčka spolu susedia a súčasne majú rovnakú farbu.
- Ostrovy rovnakej farby v mriežke potom zodpovedajú komponentom súvislosti výsledného grafu.
Na riešenie uvedeného problému na neorientovaných grafoch napíšeme rekurzívnu metódu search, ktorá bude prehľadávať všetkých ešte nenavštívených susedov daného vrchola. Informáciu o navštívení jednotlivých vrcholov si budeme uchovávať v zozname visited. Metóda existsPath bude metódu search využívať na riešenie horeuvedeného problému.
/* Pomocna metoda pre metodu existsPath. Dostane graf g, vrchol vertex a zoznam visited s informaciou o navstiveni jednotlivych vrcholov. Pri volani by malo platit visited.get(vertex) == false. Metoda rekurzivne prehlada vsetky doposial nenavstivene vrcholy dosiahnutelne z vrchola vertex. */ static void search(UndirectedGraph g, int vertex, List<Boolean> visited) { visited.set(vertex, true); for (int neighbour : g.adjVertices(vertex)) { if (!visited.get(neighbour)) { search(g, neighbour, visited); } } } /* Metoda, ktora zisti, ci su vrcholy from a to v grafe g spojene cestou. */ static boolean existsPath(UndirectedGraph g, int from, int to) { ArrayList<Boolean> visited = new ArrayList<>(); for (int i = 0; i <= g.getNumberOfVertices() - 1; i++) { visited.add(false); } search(g, from, visited); return visited.get(to); }
Hľadanie komponentov súvislosti
V prípade, že by sme existenciu cesty medzi dvojicami vrcholov chceli testovať veľakrát, oplatí sa nájsť všetky komponenty súvislosti v danom grafe. Komponenty môžeme očíslovať od nuly až po nejaké k - 1, pričom pre každý vrchol si môžeme pamätať číslo jeho komponentu. Túto úlohu realizuje nasledujúca trieda:
/* Trieda reprezentujuca rozdelenie neorientovaneho grafu na komponenty suvislosti: */ class Components { private UndirectedGraph g; // Neorientovany graf private ArrayList<Integer> componentId; // Pre kazdy vrchol si budeme v tomto zozname pamatat cislo jeho komponentu private int numComponents; // Celkovy pocet komponentov /* Konstruktor, ktory dostane graf a prehladavanim do hlbky najde jeho komponenty suvislosti: */ public Components(UndirectedGraph g) { this.g = g; numComponents = 0; // Pocet komponentov inicializujeme na 0 int n = g.getNumberOfVertices(); // Pocet vrcholov grafu componentId = new ArrayList<>(); for (int i = 0; i <= n - 1; i++) { // Komponenty jednotlivych vrcholov inicializujeme na -1 (nezmyselna hodnota) componentId.add(-1); } for (int i = 0; i <= n - 1; i++) { // Prejdeme vsetky vrcholy ... if (componentId.get(i) == -1) { // ... a ak najdeme nespracovany ... search(i, numComponents); // ... vyrobime novy komponent suvislosti numComponents++; } } } /* Pomocna rekurzivna metoda pouzivana v konstruktore na oznacenie jedneho komponentu suvislosti cislom id: */ private void search(int vertex, int id) { componentId.set(vertex, id); for (int neighbour : g.adjVertices(vertex)) { if (componentId.get(neighbour) == -1) { search(neighbour, id); } } } /* Metoda, ktora vrati true prave vtedy, ked su vrcholy from a to v rovnakom komponente: */ public boolean existsPath(int from, int to) { return componentId.get(from).equals(componentId.get(to)); } /* Metoda, ktora vrati pocet komponentov grafu: */ public int getNumberOfComponents() { return numComponents; } }
Cvičenia 20
- Riešte rozcvičku na testovači.
- Vytvorte aplikáciu, ktorej hlavné okno obsahuje tlačidlo s textom Zobraz dialóg. Po jeho stlačení sa zobrazí dialóg umožňujúci používateľovi v nejakej forme zadať prirodzené číslo a potvrdiť svoju voľbu tlačidlom OK. Po úspešnom ukončení dialógu po zadaní čísla n sa v hlavnom okne vytvorí presne n tlačidiel s nápismi 1,2,...,n, pričom každé z týchto tlačidiel bude na konzolu vypisovať svoje číslo. Po prípadnom opätovnom úspešnom ukončení dialógu sa pôvodné tlačidlá z hlavného okna vymažú a vytvorí sa nová sada tlačidiel.
Cvičenia 21
- Riešte bonus na testovači.
- Máme danú šachovnicu n x m, na ktorej sú niektoré políčka obsadené figúrkami. Na políčku (i,j) stojí kôň. Na ktoré políčka šachovnice vie preskákať, pričom môže použiť aj viacero ťahov, ale môže chodiť iba po prázdnych políčkach?
- Použite prehľadávanie do hĺbky na grafe, v ktorom sú vrcholy jednotlivé políčka a hrany spájajú dvojice voľných políčok, medzi ktorými môže kôň skočiť. Môžete vytvoriť graf v niektorej reprezentácii, alebo prehľadávať priamo v matici visited rozmerov n x m.
- Šachový kôň môže z pozície (i,j) skočiť na jednu z pozícií (i+2,j+1), (i+2,j-1), (i-2,j+1), (i-2,j-1), (i+1,j+2), (i+1,j-2), (i-1,j+2), (i-1,j-2)
Prednáška 33
Oznamy
- Deviatu domácu úlohu treba odovzdať najneskôr dnes do 22:00.
- Čoskoro bude zverejnená posledná domáca úloha (zameraná na grafy).
- Do štvrtka 2. mája, 22:00 je možné prihlásiť sa na tému nepovinného projektu.
- Najpravdepodobnejší termín druhej písomky: pondelok, 20. mája. Definitívny termín bude dohodnutý na nasledujúcej prednáške.
Triedy pre grafy z minulej prednášky
Na dnešnej prednáške budeme pokračovať v implementácii vybraných jednoduchých grafových algoritmov. Zopakujme si teda najprv kód pre jednotlivé triedy reprezentujúce grafy:
import java.util.*; /* Rozhranie pre reprezentaciu (vo vseobecnosti orientovaneho) grafu o vrcholoch 0,1,...,n-1 pre nejake prirodzene cislo n: */ interface Graph { int getNumberOfVertices(); // Vrati pocet vrcholov grafu. int getNumberOfEdges(); // Vrati pocet hran grafu. /* Prida hranu z vrcholu from do vrcholu to a vrati true, ak sa ju podarilo pridat: */ boolean addEdge(int from, int to); /* Vrati true, ak existuje hrana z vrcholu from do vrcholu to: */ boolean existsEdge(int from, int to); /* Vrati iterovatelnu skupinu pozostavajucu z prave vsetkych vrcholov, do ktorych vedie hrana z vrcholu vertex. Pre neorientovane grafy ide o prave vsetkych susedov vrcholu vertex: */ Iterable<Integer> adjVertices(int vertex); } /* Rozhranie pre reprezentaciu neorientovaneho grafu: */ interface UndirectedGraph extends Graph { } /* Trieda reprezentujuca orientovany graf pomocou zoznamov (ArrayList-ov) susedov: */ class AdjListsGraph implements Graph { /* Pre kazdy vrchol zoznam vrcholov, do ktorych z daneho vrcholu vedie hrana: */ private ArrayList<ArrayList<Integer>> adjLists; /* Pocet hran v grafe: */ private int numEdges; /* Konstruktor, ktory ako parameter dostane prirodzene cislo numVertices a vytvori graf o numVertices vrcholoch a bez jedinej hrany: */ public AdjListsGraph(int numVertices) { adjLists = new ArrayList<>(); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<>()); } numEdges = 0; } @Override public int getNumberOfVertices() { return adjLists.size(); } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { if (existsEdge(from, to)) { return false; } else { adjLists.get(from).add(to); numEdges++; return true; } } @Override public boolean existsEdge(int from, int to) { return adjLists.get(from).contains(to); } @Override public Iterable<Integer> adjVertices(int from) { // Zoznam adjLists.get(from) "obalime" tak, aby sa nedal menit: return Collections.unmodifiableList(adjLists.get(from)); } } /* Trieda reprezentujuca orientovany graf pomocou matice susednosti: */ class AdjMatrixGraph implements Graph { /* Matica susednosti: */ private boolean[][] adjMatrix; /* Pocet hran v grafe: */ private int numEdges; /* Konstruktor, ktory ako parameter dostane prirodzene cislo numVertices a vytvori graf o numVertices vrcholoch a bez jedinej hrany: */ public AdjMatrixGraph(int numVertices) { adjMatrix = new boolean[numVertices][numVertices]; for (int i = 0; i <= numVertices - 1; i++) { for (int j = 0; j <= numVertices - 1; j++) { adjMatrix[i][j] = false; } } numEdges = 0; } @Override public int getNumberOfVertices() { return adjMatrix.length; } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { if (existsEdge(from, to)) { return false; } else { adjMatrix[from][to] = true; numEdges++; return true; } } @Override public boolean existsEdge(int from, int to) { return adjMatrix[from][to]; } @Override public Iterable<Integer> adjVertices(int vertex) { // Vytvorime pomocny zoznam susedov a vratime ho "obaleny" tak, aby sa nedal menit: ArrayList<Integer> a = new ArrayList<>(); for (int i = 0; i <= adjMatrix[vertex].length - 1; i++) { if (adjMatrix[vertex][i]) { a.add(i); } } return Collections.unmodifiableList(a); } } /* Trieda reprezentujuca neorientovany graf pomocou zoznamov susedov reprezentovanych v podobe ArrayList-ov. */ class AdjListsUndirectedGraph extends AdjListsGraph implements UndirectedGraph { /* Pocet neorientovanych hran v grafe: */ private int numUndirectedEdges; public AdjListsUndirectedGraph(int numVertices) { super(numVertices); numUndirectedEdges = 0; } @Override public int getNumberOfEdges() { return numUndirectedEdges; } @Override public boolean addEdge(int from, int to) { boolean r = super.addEdge(from, to); super.addEdge(to, from); if (r) { numUndirectedEdges++; } return r; } } /* Trieda reprezentujuca neorientovany graf pomocou matice susednosti: */ class AdjMatrixUndirectedGraph extends AdjMatrixGraph implements UndirectedGraph { /* Pocet neorientovanych hran v grafe: */ private int numUndirectedEdges; public AdjMatrixUndirectedGraph(int numVertices) { super(numVertices); numUndirectedEdges = 0; } @Override public int getNumberOfEdges() { return numUndirectedEdges; } @Override public boolean addEdge(int from, int to) { boolean r = super.addEdge(from, to); super.addEdge(to, from); if (r) { numUndirectedEdges++; } return r; } } public class Prog { /* Metoda, ktora zo scannera s nacita graf vo formate: pocet vrcholov, pocet hran a zoznam hran zadanych dvojicami koncovych vrcholov. Ak undirected == true, vytvori neorientovany graf, inak orientovany. Ak matrix == true, vytvori graf reprezentovany maticou susednosti, v opacnom pripade vytvori graf reprezentovany zoznamami susedov. */ static Graph readGraph(Scanner s, boolean undirected, boolean matrix) { int n = s.nextInt(); int m = s.nextInt(); Graph g; if (undirected) { if (matrix) { g = new AdjMatrixUndirectedGraph(n); } else { g = new AdjListsUndirectedGraph(n); } } else { if (matrix) { g = new AdjMatrixGraph(n); } else { g = new AdjListsGraph(n); } } for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); g.addEdge(u, v); } return g; } // ... }
Prehľadávanie (orientovaného alebo neorientovaného) grafu do šírky
- Jednou z tém minulej prednášky bolo prehľadávanie grafu do hĺbky (angl. depth-first search) použité ako nástroj na hľadanie komponentov súvislosti neorientovaného grafu.
- Teraz si ukážeme prehľadávanie grafu do šírky (angl. breadth-first search) – to sa bude dať použiť na hľadanie najkratších ciest medzi dvojicami vrcholov orientovaného (a teda aj neorientovaného) grafu.
- Opäť pôjde o zovšeobecnenie algoritmu, ktorý sme videli už minulý semester v súvislosti s vyfarbovaním súvislých oblastí mriežky.
Hľadanie najkratšej cesty
Cestou v grafe rozumieme postupnosť vrcholov v0, v1, ..., vn takú, že žiaden z vrcholov sa v nej nevyskytuje viac ako raz a pre i = 1,...,n existuje v grafe hrana z vi-1 do vi.
- Dĺžkou cesty rozumieme počet hrán na nej – t.j. číslo n.
Hľadanie najkratších ciest v danom (vo všeobecnosti aj orientovanom) grafe realizuje trieda ShortestPathsFromVertex:
- Jej konštruktor dostane ako parameter graf g a nejaký jeho význačný „štartovací” vrchol start. Následne spustí na grafe g prehľadávanie do šírky z vrcholu start.
- Takto sa postupne prehľadajú vrcholy vo vzdialenosti 1 od start, potom vrcholy vo vzdialenosti 2 od start, atď. Na zabezpečenie takéhoto poradia sa použije rad, podobne ako pri algoritme na mriežke minulý semester. V každom momente vykonávania algoritmu môže tento rad obsahovať vrcholy najviac dvoch rôznych vzdialeností od start.
- Pre každý vrchol v sa počas prehľadávania do ArrayList-u dist uloží jeho vzdialenosť od start a do ArrayList-u prev sa uloží vrchol u, z ktorého bol vrchol v objavený (to je nutne predposledný vrchol na najkratšej ceste zo start do v).
- Metóda distanceFromStart bude pre daný vrchol vertex vracať jeho vzdialenosť od vrcholu start. Tu sa jednoducho využije hodnota uložená v ArrayList-e dist.
- Metóda shortestPathFromStart bude pre daný vrchol vertex vracať najkratšiu cestu z vrcholu start do vrcholu vertex reprezentovanú zoznamom vrcholov. Tú bude konštruovať od konca: začne vo vrchole vertex a postupne bude hľadať predchodcov pomocou hodnôt uložených v ArrayList-e prev.
/* Trieda, ktora reprezentuje najkratsie cesty a vzdialenosti z jedneho vrcholu orientovaneho grafu do vsetkych ostatnych vrcholov. */ class ShortestPathsFromVertex { private final Graph g; // Orientovany (alebo neorientovany) graf, v ktorom budeme cesty hladat. private final int start; // Vrchol grafu g, v ktorom maju cesty zacinat. private final ArrayList<Integer> dist; // i-ty prvok zoznamu bude vzdialenost zo start do i private final ArrayList<Integer> prev; // i-ty prvok zoznamu bude predchodca i na najkratsej ceste zo start do i /* Konstruktor dostane graf g a startovaci vrchol start a prehladavanim do sirky vypocita najkratsie cesty z vrcholu start do ostatnych vrcholov. */ public ShortestPathsFromVertex(Graph g, int start) { this.g = g; this.start = start; /* Incializacia zoznamov dist a prev: */ dist = new ArrayList<>(); prev = new ArrayList<>(); for (int i = 0; i <= g.getNumberOfVertices() - 1; i++) { dist.add(-1); // i-ty prvok zoznamu dist bude -1, ak sa zo start neda dostat do i prev.add(-1); // i-ty prvok zoznamu prev bude -1, ak sa zo start neda dostat do i alebo ak i == start } /* Prehladavanim do sirky vypocitame vzdialenosti a najkratsie cesty zo start: */ LinkedList<Integer> queue = new LinkedList<>(); dist.set(start, 0); queue.addLast(start); // Vzdialenost zo start do start je 0 while (!queue.isEmpty()) { int vertex = queue.removeFirst(); // Vyberieme vrchol z radu /* Prejdeme vsetkych susedov vrcholu vertex; ak este neboli navstiveni, nastavime im vzdialenost a predchodcu a vlozime ich do radu:*/ for (int neighbour : g.adjVertices(vertex)) { if (dist.get(neighbour) == -1) { dist.set(neighbour, dist.get(vertex) + 1); prev.set(neighbour, vertex); queue.addLast(neighbour); } } } } /* Metoda, ktora vrati vzdialenost vrcholu vertex od vrcholu start: */ public int distanceFromStart(int vertex) { return dist.get(vertex); } /* Metoda, ktora vrati najkratsiu cestu z vrcholu start do vrcholu vertex. Reprezentaciou cesty je zoznam vrcholov na nej. V pripade, ze neexistuje ziadna cesta zo start do vertex, vrati null. */ public List<Integer> shortestPathFromStart(int vertex) { if (dist.get(vertex) == -1) { // Ak neexistuje cesta, vrat null return null; } LinkedList<Integer> path = new LinkedList<>(); int v = vertex; while (v != start) { // Postupne pridavaj vrcholy od konca cesty path.addFirst(v); v = prev.get(v); } path.addFirst(start); return path; } }
Nasledujúci kód načíta graf a dvojicu jeho vrcholov; vypíše najkratšiu cestu medzi danými vrcholmi.
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Zadaj graf:"); Graph g = readGraph(scanner, false, false); // PRE NEORIENTOVANY GRAF: Graph g = readGraph(scanner, true, false); System.out.println("Zadaj pociatocny a koncovy vrchol:"); int from = scanner.nextInt(); int to = scanner.nextInt(); ShortestPathsFromVertex spfv = new ShortestPathsFromVertex(g, from); System.out.println("Najkratsia cesta ma dlzku " + spfv.distanceFromStart(to)); List<Integer> shortest = spfv.shortestPathFromStart(to); if (shortest != null) { System.out.println(shortest); } }
Špeciálny prípad neorientovaných grafov
Inštanciu triedy ShortestPathsFromVertex možno vytvoriť ako pre orientované, tak aj pre neorientované grafy (neorientované grafy sme implementovali ako špeciálny prípad orientovaných a všetky grafy implementujú spoločné rozhranie Graph).
Pre neorientované grafy si v súvislosti s prehľadávaním do šírky možno všimnúť nasledujúce:
- Hrany medzi vrcholmi a ich predchodcami tvoria strom (ak je graf súvislý, je tento strom jeho kostrou) – to je znázornené na obrázku vpravo.
- Najkratšia cesta zo start do v je potom (ak existuje) jediná cesta zo start do v v tomto strome.
(V orientovaných grafoch je situácia podobná; stromy najkratších ciest však navyše majú hrany orientované smerom od start.)
Prehľadávanie s návratom na grafoch
Pre veľa úloh na grafoch nie sú známe (a v prípade platnosti niektorých hypotéz z teoretickej informatiky často ani neexistujú) efektívne algoritmy. Backtrackingom však vieme spočítať odpoveď aspoň pre malé vstupy.
Hľadanie ciest dĺžky k
Nasledujúca trieda FixedLengthPaths pre daný graf g, danú dvojicu vrcholov from, to a dané prirodzené číslo length vypisuje všetky cesty dĺžky presne length vedúce v g z vrcholu from do vrcholu to. Tento proces sa spustí hneď v konštruktore (nebude teda mať veľký význam vytvárať inštancie triedy FixedLengthPaths).
Prehľadávaním s návratom budeme v LinkedList-e path postupne generovať všetky takéto cesty. Pre každý vrchol budeme mať navyše v ArrayList-e visited poznačené, či sme ho už v generovanej ceste použili. Akonáhle nájdeme cestu požadovanej dĺžky končiacu vo vrchole to, vypíšeme ju na výstup.
/* Trieda, pomocou ktorej mozno najst vsetky cesty danej dlzky medzi danou dvojicou vrcholov. */ class FixedLengthPaths { private Graph g; // Orientovany (alebo neorientovany) graf private int from, to; // Pociatocny a koncovy vrchol private int length; // Pozadovana dlzka cesty private LinkedList<Integer> path; // Zoznam, v ktorom budeme postupne generovat cesty private ArrayList<Boolean> visited; // i-ty prvok zoznamu visited bude true, ak sa vrchol i nachadza v path /* Konstruktor dostane graf, pociatocny a koncovy vrchol a pozadovanu dlzku cesty. Spusti prehladavanie s navratom, ktore hlada vsetky cesty danej dlzky medzi danymi vrcholmi a rovno aj vypisuje vysledky. */ public FixedLengthPaths(Graph g, int from, int to, int length) { this.g = g; this.from = from; this.to = to; this.length = length; visited = new ArrayList<>(); for (int i = 0; i <= g.getNumberOfVertices() - 1; i++) { visited.add(false); } path = new LinkedList<>(); path.add(from); // Kazda cesta z from bude zacinat vo from visited.set(from, true); search(); // Spusti generovanie ciest } /* Hlavna rekurzivna metoda prehladavania s navratom. Ak je vygenerovana cesta kratsia ako length, postupne vyskusa vsetky moznosti jej predlzenia. Ak sa vygeneruje cesta dlzky length, overi sa, ci tato cesta vedie do vrcholu to; ak ano, vypise sa. */ private void search() { if (path.size() == length + 1) { // Ak uz mame cestu pozadovanej dlzky ... if (path.getLast() == to) { // ... a konci v pozadovanom stave ... System.out.println(path); // ... vypis ju } } else { /* Ak este nemame cestu pozadovanej dlzky, vyskusaj vsetky moznosti jej predlzenia: */ for (int neighbour : g.adjVertices(path.getLast())) { if (!visited.get(neighbour)) { visited.set(neighbour, true); path.addLast(neighbour); search(); path.removeLast(); visited.set(neighbour, false); } } } } }
Nasledujúci kód načíta graf, dvojicu vrcholov from, to a prirodzené číslo length a vypíše všetky cesty dĺžky length z vrcholu from do vrcholu to.
public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Zadaj graf:"); Graph g = readGraph(scanner, true, false); // PRE ORIENTOVANY GRAF: Graph g = readGraph(scanner, false, false); System.out.println("Zadaj pociatocny a koncovy vrchol:"); int from = scanner.nextInt(); int to = scanner.nextInt(); System.out.println("Zadaj dlzku cesty:"); int length = scanner.nextInt(); System.out.println("Cesty dlzky " + length + ":"); new FixedLengthPaths(g, from, to, length);
Príklad: pre neorientovaný graf s vrcholmi {0,...,4} a hranami {{0,1},{0,2},{0,3},{1,2},{2,3},{2,4},{3,4}}, počiatočný vrchol 0 a koncový vrchol 3 dostávame nasledujúce výstupy
Cesty dlzky 1: 0 3 Cesty dlzky 2: 0 2 3 Cesty dlzky 3: 0 1 2 3 0 2 4 3 Cesty dlzky 4: 0 1 2 4 3
Cvičenia:
- Upravte triedu FixedLengthPaths tak, aby namiesto vypisovania ciest iba počítala, koľko ich je.
- Upravte triedu FixedLengthPaths tak, aby iba zisťovala, či existuje cesta danej dĺžky (po prvej nájdenej ceste je teda možné prehľadávanie ukončiť).
- Navrhnite spôsoby, ako v niektorých prípadoch zistiť, že aktuálne rozrobenú cestu už nie je možné požadovaným spôsobom rozšíriť.
Hľadanie najdlhšej cesty
Uvažujme teraz problém nájdenia nejakej z najdlhších ciest z u do v (ak existuje aspoň jedna). Túto úlohu bude realizovať trieda LongestPath; oproti predchádzajúcemu programu sa ten nasledujúci bude líšiť iba málo:
- Budeme si pamätať najdlhšiu nájdenú cestu.
- Vždy, keď prídeme do cieľového vrcholu, porovnáme dĺžku aktuálnej cesty s najdlhšou cestou nájdenou doteraz.
/* Trieda, pomocou ktorej mozno najst jednu z najdlhsich ciest medzi danou dvojicou vrcholov. */ class LongestPath { private Graph g; // Orientovany (alebo neorientovany) graf private int from, to; // Pociatocny a koncovy vrchol private int maxLength; // Dlzka doposial najdlhsej najdenej cesty z from do to private LinkedList<Integer> path; // Zoznam, v ktorom budeme postupne generovat cesty private LinkedList<Integer> longestPath; // Okrem toho si budeme pamatat najdlhsiu doposial vygenerovanu cestu private ArrayList<Boolean> visited; // i-ty prvok zoznamu visited bude true, ak sa vrchol i nachadza v path /* Konstruktor dostane graf, pociatocny a koncovy vrchol. Spusti prehladavanie s navratom, ktore hlada najdlhsiu cestu medzi danymi vrcholmi. */ public LongestPath(Graph g, int from, int to) { this.g = g; this.from = from; this.to = to; visited = new ArrayList<>(); for (int i = 0; i <= g.getNumberOfVertices() - 1; i++) { visited.add(false); } maxLength = -1; // Doposial nemame ziadnu cestu path = new LinkedList<>(); path.add(from); // Kazda cesta z from bude zacinat vo from visited.set(from, true); search(); // Spusti generovanie ciest } /* Hlavna rekurzivna metoda prehladavania s navratom. Ak cesta dorazila do vrchola to, jej dlzka sa porovna s najdlhsou doposial najdenou cestou a ak je dlhsia, ulozi sa ako nova doposial najdlhsia cesta. Ak este nedorazila do vrchola to, vyskusaju sa vsetky moznosti na jej predlzenie. */ private void search() { if (path.getLast() == to) { // Ak sme dorazili do cieloveho vrchola, ukonci prehladavanie if (path.size() - 1 > maxLength) { maxLength = path.size() - 1; longestPath = new LinkedList<>(path); } } else { // Inak vyskusaj vsetky moznosti predlzenia cesty for (int neighbour : g.adjVertices(path.getLast())) { if (!visited.get(neighbour)) { visited.set(neighbour, true); path.addLast(neighbour); search(); path.removeLast(); visited.set(neighbour, false); } } } } /* Metoda, ktora vrati najdenu najdlhsiu cestu: */ public List<Integer> longestPath() { if (longestPath != null) { return Collections.unmodifiableList(longestPath); } else { return null; } } }
Použitie triedy:
LongestPath lp = new LongestPath(g, from, to); List<Integer> longest = lp.longestPath(); if (longest != null) { System.out.println("Najdlhsia cesta: " + longest); }
Príklad výstupu na rovnakom grafe ako vyššie:
Najdlhsia cesta: [0, 1, 2, 4, 3]
Ohodnotené grafy
Po zvyšok tejto prednášky sa budeme zaoberať ohodnotenými grafmi nad množinou reálnych čísel. Ide o rozšírenie grafov, pri ktorom má každá hrana priradené nejaké reálne ohodnotenie. Pre ohodnotené grafy napíšeme rozhranie WeightedGraph a triedu WeightedAdjListsGraph reprezentujúcu orientované ohodnotené grafy pomocou zoznamov susedov (podobným spôsobom ako na minulej prednáške by sme však mohli napísať aj triedy ako WeightedAdjMatrixGraph, WeightedAdjListsUndirectedGraph a podobne).
Rozhranie pre ohodnotené grafy (WeightedGraph)
/* Pomocna trieda reprezentujuca dvojicu pozostavajucu zo suseda a ohodnotenia don veducej hrany. */ class WeightedNeighbour { private int vertex; private double weight; public WeightedNeighbour(int vertex, double weight) { this.vertex = vertex; this.weight = weight; } public int vertex() { return vertex; } public double weight() { return weight; } } /* Rozhranie pre ohodnoteny graf: */ interface WeightedGraph extends Graph { // Ohodnoteny graf by mal poskytovat vsetky metody neohodnoteneho boolean addEdge(int from, int to, double weight); // Metoda na pridanie ohodnotenej hrany Iterable<WeightedNeighbour> weightedAdjVertices(int vertex); // Metoda vracajuca iterovatelnu skupinu ohodnotenych susedov }
Orientované ohodnotené grafy pomocou zoznamov susedov (WeightedAdjListsGraph)
Triedu WeightedAdjListsGraph reprezentujúcu orientovaný ohodnotený graf pomocou zoznamov susedov napíšeme jednoduchým rozšírením analogickej triedy AdjListsGraph pre neohodnotené grafy. Navyše si budeme pamätať len ohodnotenia jednotlivých hrán (t.j. pre každý vrchol zoznam jeho ohodnotených susedov).
/* Trieda reprezentujuca orientovany ohodnoteny graf pomocou zoznamov susedov. Ide o rozsirenie triedy AdjListsGraph, v ktorom si navyse pamatame aj realne ohodnotenia jednotlivych hran. */ class WeightedAdjListsGraph extends AdjListsGraph implements WeightedGraph { private ArrayList<ArrayList<WeightedNeighbour>> weightedAdjLists; // Zoznam ohodnotenych susedov pre kazdy vrchol public WeightedAdjListsGraph(int numVertices) { super(numVertices); weightedAdjLists = new ArrayList<>(); for (int i = 0; i <= numVertices - 1; i++) { weightedAdjLists.add(new ArrayList<>()); } } @Override public boolean addEdge(int from, int to) { return addEdge(from, to, 0); // Pridanie hrany bez udania ohodnotenia budeme chapat ako pridanie hrany s nulovym ohodnotenim } @Override public boolean addEdge(int from, int to, double weight) { boolean result = super.addEdge(from, to); // Pridame neohodnotenu hranu if (result) { // V pripade uspechu si zapamatame jej ohodnotenie weightedAdjLists.get(from).add(new WeightedNeighbour(to, weight)); } return result; } @Override public Iterable<WeightedNeighbour> weightedAdjVertices(int from) { return Collections.unmodifiableList(weightedAdjLists.get(from)); } }
Hľadanie najdlhšej cesty v ohodnotenom grafe
Pod dĺžkou cesty v ohodnotenom grafe rozumieme súčet ohodnotení jej hrán. Podobne ako pre neohodnotené grafy možno potom najdlhšiu cestu medzi dvoma vrcholmi nájsť pomocou prehľadávania s návratom (správne bude pracovať za predpokladu, že sú všetky ohodnotenia hrán nezáporné):
/* Trieda, pomocou ktorej mozno najst aspon jednu z najdlhsich ciest medzi danou dvojicou vrcholov ohodnoteneho grafu. */ class LongestWeightedPath { private WeightedGraph g; // Ohodnoteny graf private int from, to; // Pociatocny a koncovy vrchol cesty private double length; // Dlzka momentalne vygenerovanej casti cesty private double maxLength; // Dlzka doposial najdlhsej cesty z from do to private LinkedList<Integer> path; // Zoznam, v ktorom budeme postupne generovat cesty private LinkedList<Integer> longestWeightedPath; // Doposial najdlhsia najdena cesta z from do to private ArrayList<Boolean> visited; // Pre kazdy vrchol informacia o tom, ci sa nachadza v path /* Konstruktor triedy, ktory spusti hladanie najdlhsej cesty v g z from do to: */ public LongestWeightedPath(WeightedGraph g, int from, int to) { this.g = g; this.from = from; this.to = to; visited = new ArrayList<>(); for (int i = 0; i <= g.getNumberOfVertices() - 1; i++) { visited.add(false); } maxLength = -1; length = 0; path = new LinkedList<>(); path.add(from); visited.set(from, true); search(); } /* Hlavna rekurzivna metoda prehladavania s navratom: */ private void search() { if (path.getLast() == to) { if (length > maxLength) { maxLength = length; longestWeightedPath = new LinkedList<>(path); } } else { for (WeightedNeighbour wn : g.weightedAdjVertices(path.getLast())) { int neighbour = wn.vertex(); double weight = wn.weight(); if (!visited.get(neighbour)) { visited.set(neighbour, true); path.addLast(neighbour); length += weight; search(); length -= weight; path.removeLast(); visited.set(neighbour, false); } } } } /* Metoda, ktora vrati najdenu najdlhsiu cestu: */ public List<Integer> longestWeightedPath() { if (longestWeightedPath != null) { return Collections.unmodifiableList(longestWeightedPath); } else { return null; } } }
Použitie tejto triedy:
public class Prog { // ... static WeightedGraph readWeightedGraph(Scanner s) { int n = s.nextInt(); int m = s.nextInt(); WeightedGraph g; g = new WeightedAdjListsGraph(n); for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); double w = s.nextDouble(); g.addEdge(u, v, w); } return g; } // ... public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Zadaj graf:"); WeightedGraph g = readWeightedGraph(scanner); System.out.println("Zadaj pociatocny a koncovy vrchol:"); int from = scanner.nextInt(); int to = scanner.nextInt(); LongestWeightedPath lwp = new LongestWeightedPath(g, from, to); List<Integer> longest = lwp.longestWeightedPath(); if (longest != null) { System.out.println("Najdlhsia cesta: " + lwp.longestWeightedPath()); } } }
Najkratšia cesta v ohodnotenom grafe
Poznamenajme, že najkratšia cesta v ohodnotenom grafe sa vo všeobecnosti nedá nájsť prehľadávaním do šírky:
- Ak sú ohodnoteniami prirodzené čísla, možno hranu ohodnotenú číslom k reprezentovať postupnosťou k nadväzujúcich hrán a aplikovať algoritmus pre neohodnotené grafy. Pokiaľ však nemáme zaručené, že sú ohodnotenia hrán malé, ide o extrémne neefektívny prístup.
- Najkratšiu cestu samozrejme možno hľadať aj prehľadávaním s návratom, podobne ako cestu najdlhšiu. To je však tiež veľmi neefektívne (pri najdlhšej ceste to až tak nevadí, keďže efektívny algoritmus s najväčšou pravdepodobnosťou neexistuje).
- „Rozumné” algoritmy na hľadanie najkratšej cesty v ohodnotenom grafe sa preberajú napríklad v rámci predmetu Tvorba efektívnych algoritmov.
Prednáška 34
Oznamy
- Budúci týždeň prednáška aj cvičenia s rozcvičkou (grafy)
- Posledná DÚ zverejnená, odovzdávajte do stredy 15.5. do 22:00
- Nepovinné projekty bude treba odovzdať do utorka 21.5. 22:00
- Predvádzanie štvrtok 23.5. po skúške (cca o 12:00)
- Druhý test pondelok 20.5. o 16:30 v posluchárni A.
- Termíny skúšok
- Štvrtok 23.5. 9:00 v H6 (riadny)
- Utorok 4.6. 9:00 v H6 (riadny alebo 1. opravný)
- V strede júna 1. alebo 2. opravný, dátum upresníme neskôr
- Koncom júna 2. opravný termín, dátum upresníme neskôr
- Zapisovanie na termíny od dnes 19:00
- Prípadné konflikty s dátumami písomky alebo skúšok nám dajte vedieť čím skôr
- Ak by ste chceli nejakú skupinovú konzultáciu pred skúškou alebo testom, dajte nám vedieť
Informácie ku skúške
- Potrebné informácie a cvičné príklady nájdete na stránke Letný semester, test a skúška
- Na skúške budete dopisovať do hotového programu GraphGUI
Orientované grafy
Opakovanie: V orientovanom grafe má každá hrana smer (zobrazujeme ako šípku z jedného vrcholu do druhého)
- napríklad jednosmerné ulice, závislosti medzi úlohami
- v cestách a cykloch zväčša vyžadujeme, aby išli iba v smere šípky
- v našej implementácii iterátor adjVertices poskytuje iba vychádzajúce hrany
Prehľadávanie do hĺbky
V takto implementovanom grafe môžeme použiť prehľadávanie do hĺbky z minulých prednášok na zistenie existencie orientovanej cesty medzi dvoma vrcholmi.
- Treba si však uvedomiť, že podobne, ako existencia hrany (0,1) nehovorí nič o hrane (1,0) ani existencia cesty medzi dvomi vrcholmi nie je obojsmerná
- Keďže však máme k dispozícii iterátor cez výstupné hrany, vytvárame práve správne orientované cesty
static void search(UndirectedGraph g, int vertex, List<Boolean> visited) { visited.set(vertex, true); for (int neighbour : g.adjVertices(vertex)) { if (!visited.get(neighbour)) { search(g, neighbour, visited); } } }
- Čo ak by sme chceli vypísať cestu z vrcholu u do vrcholu v?
- Viacnásobným použitím funkcie vieme získať zoznamy vrcholov, ktoré sú dosiahnuteľné z jednotlivých vrcholov.
Cvičenie:
- Zamyslite sa, čo by robil algoritmus na hľadanie komponentov z prednášky 32 na orientovanom grafe - funguje? prečo?
Poznámka: ako sme videli na minulej prednáške, prehľadávanie do šírky je na orientovanom (neohodnotenom) grafe rovnako použiteľné.
- Získame najkratšie orientované cesty z vrchola do všetkých ostatných vrcholov
Topologické triedenie, existencia cyklu
Motivačná úloha:
- Na úrade potrebujeme vybaviť niekoľko potvrdení. Ale číha tam na nás byrokracia: o niektorých dvojiciach potvrdení vieme, že na získanie potvrdenia B potrebujeme predložiť potvrdenie A.
- Úlohou je nájsť poradie (a zistiť, či také vôbec existuje) ako potvrdenia na úrade vybavovať.
- Úlohu reprezentujeme ako orientovaný graf, kde vrcholy sú potvrdenia a hrany závislosti medzi nimi.
Topologické usporiadanie orientovaného grafu je permutácia jeho vrcholov Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle v_1,v_2,\dots v_n} taká, že pre každú hranu Syntaktická analýza (parsing) neúspešná (MathML (experimentálne): Neplatná odpověď („Math extension cannot connect to Restbase.“) od serveru „https://wikimedia.org/api/rest_v1/“:): {\displaystyle (v_i,v_j)} platí, že i<j
- t.j. všetky hrany idú v permutácii zľava doprava
- orientovaný graf môže mať aj viac ako jedno topologické usporiadanie
- Koľko najviac topologických usporiadaní môže mať orientovaný graf s n vrcholmi? Pre aký graf sa to stane?
- Môže sa však stať, že graf nemá žiadne topologické usporiadanie
- To sa stane práve vtedy, ak je v grafe orientovaný cyklus
- Zjavne ak je v grafe orientovaný cyklus, topologické usporiadanie neexistuje, lebo v topologickom usporiadaní idú hrany iba zľava doprava a cyklus sa nemá ako vrátiť späť
- Skúste si dokázať aj opačnú implikáciu
- Graf bez cyklu voláme acyklický
Samotné topologické triedenie bude pracovať nasledovne:
- ak máme vrchol, do ktorého nevchádza žiadna hrana, môžeme ho vypísať (potvrdenie, ktoré nemá žiadne závislosti)
- z tohto vrcholu vychádzajú hrany, môžeme ich odteraz ignorovať (splnené závislosti)
- pre každý vrchol si pamätáme počet zatiaľ nesplnených závislostí
- na začiatku to bude počet hrán vchádzajúcich do vrchola
- keď vypíšeme vrchol v, prejdeme všetky hrany z neho vychádzajúce a vrcholom na druhom konci znížime počet nesplnených závislostí
- udržujeme si tiež množinu vrcholov, ktoré už nemajú nesplnené závislosti a v každom kroku jeden vrchol z nej vyberieme a vypíšeme
/* Statická metóda, ktorá dostane orientovaný acyklický graf * a vráti zoznam jeho vrcholov * v topologickom usporiadaní */ public static ArrayList<Integer> TopologicalSort(Graph g) { int n = g.getNumberOfVertices(); // pocet vrcholov grafu // inicializuj pocet nesplnenych zavislosti, potom // prejdi vsetky hrany a zvysuj int[] numberOfPrerequisites = new int[n]; for (int vertex = 0; vertex < n; vertex++) { numberOfPrerequisites[vertex] = 0; } for (int vertex = 0; vertex < n; vertex++) { for (int neighbor : g.adjVertices(vertex)) { numberOfPrerequisites[neighbor]++; } } // vsetky vrcholy bez zavislosti pridaj do mnoziny ready LinkedList<Integer> ready = new LinkedList<Integer>(); for (int vertex = 0; vertex < n; vertex++) { if (numberOfPrerequisites[vertex] == 0) { ready.add(vertex); } } // inicializuj vysledok ArrayList<Integer> order = new ArrayList<Integer>(); // hlavny cyklus - vyberaj vrchol z mnoziny ready a pridavaj do order while (!ready.isEmpty()) { int vertex = ready.remove(); order.add(vertex); // pre susedov vypisaneho vrchola zniz pocet zavislosti for (int neighbor : g.adjVertices(vertex)) { numberOfPrerequisites[neighbor]--; // ak to bola posledna zavislost, vrchol je uz vypisatelny if (numberOfPrerequisites[neighbor] == 0) { ready.add(neighbor); } } } return order; }
Čo spraví tento program, ak mu dáme vstupný graf, v ktorom je orientovaný cyklus?
Ak sa nám do poľa order podarilo dať všetky vrcholy, máme topologické usporiadanie, graf je teda acyklický
- ak máme v poli order menej ako n vrcholov, každý vrchol má aspoň jednu nesplnenú závislosť
- topologické triedenie teda v tomto prípade nemôže existovať (žiaden vrchol nemôže ísť prvý)
- graf má teda cyklus
Cvičenie:
- Upravte program tak, aby v prípade, že graf má orientovaný cyklus, funkcia TopologicalSort vrátila null
- Napíšte program, ktorý v prípade, že graf nie je acyklický, v ňom nájde orientovaný cyklus.
Existencia cyklu a topologické triedenie pomocou prehľadávania do hĺbky
Trochu iný prístup založený na malej modifikácii prehľadávania do hĺbky, ale ťažšie vidieť, že funguje (dobré precvičenie)
Do prehľadávania do hĺbky pridáme pole finished
- keď začneme rekurziu pre vrchol v, nastavíme visited[v]=true (ako predtým)
- keď končíme rekurziu pre vrchol v, nastavíme finished[v]=true
V danom bode behu algoritmu máme vrcholy troch typov:
- ešte nenavštívené, visited[v] aj finished[v] je false
- už ukončené, visited[v] aj finished[v] je true
- rozrobené, visited[v] je true, ale finished[v] je false
Rozrobené vrcholy sú všetky na zásobníku a sú spojené orientovanou cestou
Topologické triedenie:
- Vždy keď nastavíme finished[v]=true, pridáme v do poľa order
- Po prehľadaní celého grafu otočíme poradie poľa order
Predstavme si, že kontrolujeme hrany vychádzajúce z vrchola v, ktorý je teste pred dokončením, t.j. po tom, ako sme pozreli jeho susedov, ale predtým ako sme ho dali do poľa order. Kam môžu ísť?
- Do ešte nenavštíveného vrchola u. To sa nestane, lebo by sme predtým zavolali rekurziu pre u.
- Do ukončeného vrchola u. Ten už je v poli order, ale v tam ešte nie je. Hrana (u,v) teda pôjde vo výsledku zľava doprava.
- Do rozrobeného vrchola u. Toto ale znamená existenciu cyklu v grafe, takže topologické triedenie neexistuje.
class TopologicalSort { /** Samotny graf */ private Graph g; /** Zoznam vrcholov v topologickom usporiadani */ private ArrayList<Integer> order; /** Indikator, ci je graf acyklicky */ private boolean acyclic; /** Pole indikujuce, ci sme uz vrchol v prehladavani navstivili */ private boolean[] visited; /** Pole indikujuce, ci sme uz ukoncili prehladavanie vrchola * a vsetkych jeho nasledovnikov */ private boolean[] finished; /** Konstruktor, ktory dostane graf a prehladavanim do hlbky * testuje acyklickost grafu a hlada topologicke usporiadanie */ public TopologicalSort(Graph g) { this.g = g; // uloz graf int n = g.getNumberOfVertices(); // pocet vrcholov grafu order = new ArrayList<Integer>(); // inicializuj vysledok acyclic = true; // zatial sme nevideli cyklus visited = new boolean[n]; finished = new boolean[n]; for (int i = 0; i < n; i++) { visited[i] = false; finished[i] = false; } // prechadzaj cez vrchol a ak najdes nevyfarbeny, // spusti prehladavanie for (int i = 0; i < n; i++) { if (!visited[i]) { search(i); } } Collections.reverse(order); //prevratime poradie vrcholov } /** Pomocna rekurzivna metoda pouzivana v konstruktore * na vyfarbenie vsetkych nasledovnikov vrchola vertex */ private void search(int vertex) { visited[vertex] = true; // uz sme ho navstivili //prejdi cez vychadzajuce hrany for (int neighbor: g.adjVertices(vertex)){ // ak uz sme suseda navstivili, ale este nie je // ukonceny, mame cyklus if (visited[neighbor] && !finished[neighbor]) { acyclic = false; } // ak este sused nebol vobec navstiveny, navstiv do rekurzivne if (!visited[neighbor]) { search(neighbor); // navstivime ho rekurzivne } } // ukoncili sme prehladavanie aktualneho vrcholu // poznac ako ukonceny a pridaj ho do zoznamu finished[vertex] = true; order.add(vertex); } ... }
Zdrojový kód programu, topologické triedenie
Nasledujúci program počíta topologické triedenie oboma metódami.
Implementácia orientovaného grafu Graph.java
import java.util.*; /* Rozhranie pre reprezentaciu (vo vseobecnosti orientovaneho) grafu o vrcholoch 0,1,...,n-1 pre nejake prirodzene cislo n: */ interface Graph { int getNumberOfVertices(); // Vrati pocet vrcholov grafu. int getNumberOfEdges(); // Vrati pocet hran grafu. /* Prida hranu z vrcholu from do vrcholu to a vrati true, ak sa ju podarilo pridat: */ boolean addEdge(int from, int to); /* Vrati true, ak existuje hrana z vrcholu from do vrcholu to: */ boolean existsEdge(int from, int to); /* Vrati iterovatelnu skupinu pozostavajucu z prave vsetkych vrcholov, do ktorych vedie hrana z vrcholu vertex. Pre neorientovane grafy ide o prave vsetkych susedov vrcholu vertex: */ Iterable<Integer> adjVertices(int vertex); } /* Trieda reprezentujuca orientovany graf pomocou zoznamov (ArrayList-ov) susedov: */ class AdjListsGraph implements Graph { /* Pre kazdy vrchol zoznam vrcholov, do ktorych z daneho vrcholu vedie hrana: */ private ArrayList<ArrayList<Integer>> adjLists; /* Pocet hran v grafe: */ private int numEdges; /* Konstruktor, ktory ako parameter dostane prirodzene cislo numVertices a vytvori graf o numVertices vrcholoch a bez jedinej hrany: */ public AdjListsGraph(int numVertices) { adjLists = new ArrayList<>(); for (int i = 0; i < numVertices; i++) { adjLists.add(new ArrayList<Integer>()); } numEdges = 0; } @Override public int getNumberOfVertices() { return adjLists.size(); } @Override public int getNumberOfEdges() { return numEdges; } @Override public boolean addEdge(int from, int to) { if (existsEdge(from, to)) { return false; } else { adjLists.get(from).add(to); numEdges++; return true; } } @Override public boolean existsEdge(int from, int to) { return adjLists.get(from).contains(to); } @Override public Iterable<Integer> adjVertices(int from) { // Zoznam adjLists.get(from) "obalime" tak, aby sa nedal menit: return Collections.unmodifiableList(adjLists.get(from)); } }
Topologické usporiadanie a hlavný program Prog.java
import java.util.*; /** Trieda obsahujuca topologicke usporiadanie vrcholov orientovaneho grafu * vypoctane prehladavanim do hlbky */ class TopologicalSort { /** Samotny graf */ private Graph g; /** Zoznam vrcholov v topologickom usporiadani */ private ArrayList<Integer> order; /** Indikator, ci je graf acyklicky */ private boolean acyclic; /** Pole indikujuce, ci sme uz vrchol v prehladavani navstivili */ private boolean[] visited; /** Pole indikujuce, ci sme uz ukoncili prehladavanie vrchola * a vsetkych jeho nasledovnikov */ private boolean[] finished; /** Konstruktor, ktory dostane graf a prehladavanim do hlbky * testuje acyklickost grafu a hlada topologicke usporiadanie */ public TopologicalSort(Graph g) { this.g = g; // uloz graf int n = g.getNumberOfVertices(); // pocet vrcholov grafu order = new ArrayList<Integer>(); // inicializuj vysledok acyclic = true; // zatial sme nevideli cyklus visited = new boolean[n]; finished = new boolean[n]; for (int i = 0; i < n; i++) { visited[i] = false; finished[i] = false; } // prechadzaj cez vrchol a ak najdes nevyfarbeny, // spusti prehladavanie for (int i = 0; i < n; i++) { if (!visited[i]) { search(i); } } Collections.reverse(order); //prevratime poradie vrcholov } /** Pomocna rekurzivna metoda pouzivana v konstruktore * na vyfarbenie vsetkych nasledovnikov vrchola vertex */ private void search(int vertex) { visited[vertex] = true; // uz sme ho navstivili //prejdi cez vychadzajuce hrany for (int neighbor : g.adjVertices(vertex)) { // ak uz sme suseda navstivili, ale este nie je // ukonceny, mame cyklus if (visited[neighbor] && !finished[neighbor]) { acyclic = false; } // ak este sused nebol vobec navstiveny, navstiv do rekurzivne if (!visited[neighbor]) { search(neighbor); // navstivime ho rekurzivne } } // ukoncili sme prehladavanie aktualneho vrcholu // poznac ako ukonceny a pridaj ho do zoznamu finished[vertex] = true; order.add(vertex); } /** vratil, ci je vstupny graf acyklicky */ public boolean isAcyclic() { return acyclic; } /** ak je graf acyklicky, vrati topologicke usporiadanie vrcholov */ public ArrayList<Integer> order() { if (!acyclic) { return null; } return new ArrayList<Integer>(order); } } public class Prog { static Graph readGraph(Scanner s) { int n = s.nextInt(); int m = s.nextInt(); Graph g; g = new AdjListsGraph(n); for (int i = 0; i < m; i++) { int u = s.nextInt(); int v = s.nextInt(); g.addEdge(u, v); } return g; } /** Statická metóda, ktorá dostane orientovaný acyklický graf a * vráti zoznam jeho vrcholov v topologickom usporiadaní alebo * null, ak graf ma cyklus*/ public static ArrayList<Integer> topologicalSort(Graph g) { int n = g.getNumberOfVertices(); // pocet vrcholov grafu // inicializuj pocet nesplnenych zavislosti, potom // prejdi vsetky hrany a zvysuj int[] numberOfPrerequisites = new int[n]; for (int vertex = 0; vertex < n; vertex++) { numberOfPrerequisites[vertex] = 0; } for (int vertex = 0; vertex < n; vertex++) { for (int neighbor : g.adjVertices(vertex)) { numberOfPrerequisites[neighbor]++; } } // vsetky vrcholy bez zavislosti pridaj do mnoziny ready LinkedList<Integer> ready = new LinkedList<Integer>(); for (int vertex = 0; vertex < n; vertex++) { if (numberOfPrerequisites[vertex] == 0) { ready.add(vertex); } } // inicializuj vysledok ArrayList<Integer> order = new ArrayList<Integer>(); // hlavny cyklus - vyberaj vrchol z mnoziny ready a pridavaj do order while (!ready.isEmpty()) { int vertex = ready.remove(); order.add(vertex); // pre susedov vypisaneho vrchola zniz pocet zavislosti for (int neighbor : g.adjVertices(vertex)) { numberOfPrerequisites[neighbor]--; // ak to bola posledna zavislost, vrchol je uz vypisatelny if (numberOfPrerequisites[neighbor] == 0) { ready.add(neighbor); } } } if (order.size() < n) { // ak sa nepodarilo usporiadat vsetky vrcholy, vrat null return null; } else { return order; } } public static void main(String[] args) { Scanner s = new Scanner(System.in); Graph g = readGraph(s); s.close(); System.out.println("Metoda s pocitanim zavislosti:"); ArrayList<Integer> order = topologicalSort(g); if (order != null) { System.out.println("Topologicke usporiadanie: " + order); } else { System.out.println("Graf ma cyklus"); } System.out.println("Metoda s prehladavanim do hlbky:"); TopologicalSort sort = new TopologicalSort(g); if (sort.isAcyclic()) { System.out.println("Topologicke usporiadanie: " + sort.order()); } else { System.out.println("Graf ma cyklus"); } } } /** Priklad vstupu bez cyklu: 4 4 1 0 2 0 2 1 3 1 * s cyklom: 4 4 1 0 0 2 2 1 3 1 */
Prednáška 35
Oznamy
- Dnešná prednáška: hľadanie maximálnej kliky, vaše otázky, ukážka OOP v C++
- V stredu cvičenia s rozcvičkou (grafy)
- Poslednú DÚ odovzdávajte do stredy 22:00
- Druhý test pondelok 20.5. o 16:30 v posluchárni A.
- Prvý termín skúšky v štvrtok 23.5., prihláste sa cez AIS
- Nepovinné projekty treba odovzdať do utorka 21.5. 22:00
- Predvádzanie štvrtok 23.5. po skúške (cca o 12:00)
Zhrnutie
Čo by ste mali po dvoch semestroch vedieť
- Základy jazykov C/C++, Java: cykly, podmienky, premenné, funkcie, primitívne typy, polia, alokovanie pamäte, reťazce, súbory
- Základy OOP, triedy, dedenie, polymorfizmus, výnimky, generické programovanie
- Základy tvorby GUI v JavaFX
- Dátové štruktúry: spájaný zoznam, zásobník a rad, binárne stromy a ich využitie (vyhľadávacie, lexikografické, aritmetické, ...), hešovacie tabuľky
- Základné algoritmy: triedenia, binárne vyhľadávanie, prehľadávanie grafov a stromov, prehľadávanie s návratom
- Vymyslieť jednoduchý algoritmus, vedieť ho napísať a odladiť, porozumieť hotovým programom
Nadväzujúce predmety
- Algoritmy a dátové štruktúry (2/Z) a Tvorba efektívnych algoritmov (2/L): viac algoritmov a dátových štruktúr, časová zložitosť
- Programovanie (3) (2/Z): viac programovania v Jave, návrhové vzory, vlákna
- Ročníkový projekt (1) a (2): píšete väčší program na tému podľa vlastného výberu
- Neskôr na túto tému môžete (ale nemusíte) nadviazať bakalárskou prácou
- Rýchlostné programovanie: riešenie úloh z programátorských súťaží, precvičenie programovania, algoritmov, hľadania chýb
- Medzi ďalšie školské aktivity súvisiace s programovaním patrí aj študentský vývojový tím
Prehľadávanie do hĺbky / do šírky / s návratom
- Prehľadávanie grafu do hĺbky a do šírky sú rýchle algoritmy, ktoré navštívia každý vrchol iba raz
- Prehľadávanie do hĺbky je rekurzívne, zistí, či sú dva vrcholy spojené cestou
- Prehľadávanie do šírky používa rad (frontu), nájde najkratšiu cestu
- Prehľadávanie s návratom (backtracking) je všeobecná technika na generovanie všetkých postupností určitého typu
- Riešili sme ňou napr. problém 8 dám, sudoku a pod.
- Dá sa použiť aj na grafoch, keď potrebujeme pozrieť všetky cesty, všetky podmnožiny vrcholov a pod.
- Čas výpočtu je exponenciálny, použiť sa dá iba na veľmi malých vstupoch
- Používame iba vtedy, ak nevieme nájsť rýchlejší algoritmus
Pozor, na DÚ10 máte použiť prehľadávanie do šírky, na skúške prehľadávanie s návratom
Opakovanie backtracky na grafe
- Na predminulej prednáške boli príklady na prehľadávanie grafu s návratom
- Hľadanie ciest dĺžky k
- Hľadanie najdlhšej cesty v neohodnotenom grafe
- Hľadanie najdlhšej cesty v ohodnotenom grafe
- Pri riešení sme postupne vytvárali cestu pridávaním potenciálnych vrcholov do nej a následným kontrolovaním situácie (hotové riešenie, slepá vetva)
Hľadanie maximálnej kliky
- Pozrime sa teraz na iný typ problému, nehľadáme cestu, ale množinu vrcholov
- Klika je taká množina vrcholov, v ktorej sú každé dva vrcholy spojené hranou
- Maximálna klika je klika s najväčším počtom vrcholov v danom grafe
Graf G=(V,E), kde V={0,...,4} a E={{0,1},{0,2},{0,3},{1,2},{2,3},{2,4},{3,4}} obsahuje niekoľko klík veľkosti 3, ale žiadnu kliku veľkosti 4
Maximalna klika: [0, 1, 2]
Po pridaní hrany {0,4} dostávame kliku veľkosti 4:
Maximalna klika: [0, 2, 3, 4]
Jednoduchšia verzia
Hľadanie maximálnej kliky:
- Prehľadávame všetky podmnožiny vrcholov
- Rekurzívne skúšame každý vrchol najprv pridať do podmnožiny, potom vynechať
- Keď prejdeme cez všetky vrcholy, skontrolujeme, že aktuálna podmnožina je klika
- Aktuálnu podmnožinu aj najväčšiu nájdenú kliku ukladáme do LinkedList-u.
/** Trieda, ktora umoznuje najst maximalnu kliku v danom grafe. */ class MaximumClique { /** samotny graf */ private UndirectedGraph g; /** zoznam vrcholov v najvacsej doteraz najdenej klike */ private LinkedList<Integer> maxClique; /** zoznam vrcholov v aktualnej podmnozine */ private LinkedList<Integer> vertexSet; /** Konstruktor, ktory dostane graf a spusti rekurzivne vyhladavanie */ public MaximumClique(UndirectedGraph g) { this.g = g; // uloz vstupny graf // vytvor dve prazdne mnoziny vrcholov vertexSet = new LinkedList<Integer>(); maxClique = new LinkedList<Integer>(); search(0); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda skusi pouzit aj vynechat vrchol vertex * potom skusa vsetky moznosti pre vrcholy vertex+1...n-1. */ private void search(int vertex) { // ak uz sme vycerpali vsetky vrcholy, nie je co skusat dalej if (vertex == g.getNumberOfVertices()) { // skontroluj, ci mame kliku a ak ano, porovnaj s najlepsou doteraz if(isClique(g, vertexSet) && vertexSet.size() > maxClique.size()) { // konstruktor LinkedListu moze vytvorit kopiu inej Collection maxClique = new LinkedList<Integer>(vertexSet); } return; } // pridaj vrchol vertex do mnoziny a zavolaj rekurziu vertexSet.addLast(vertex); search(vertex + 1); // odober vertex z mnoziny a zavolaj rekurziu na ostatne vrcholy vertexSet.removeLast(); search(vertex + 1); } /** pomocna metoda, ktora overi, ci je vertexSet klika. */ private static boolean isClique(Graph g, Collection<Integer> vertexSet) { // iterujeme cez vsetky dvojice v mnozine for(int u : vertexSet) { for(int v : vertexSet) { if (u != v && !g.existsEdge(u, v)) { // over hranu return false; // hrana nie je } } } return true; // vsetky hrany najdene } /** vrati maximalnu kliku najdenu v grafe g */ public List<Integer> maxClique() { // vrat nemenitelnu kopiu nasej najlepsej kliky return Collections.unmodifiableList(maxClique); } }
Príklad použitia
MaximumClique c = new MaximumClique(g); System.out.println("Maximalna klika: " + c.maxClique().toString());
Rýchlejšia verzia
- Rekurzívne skúšame každý vrchol najprv pridať do kliky, potom vynechať
- Vrchol pridávame do kliky iba ak je spojený so všetkými vrcholmi, ktoré už sú v klike
- Vrchol v klike veľkosti k má stupeň (počet susedov) aspoň k-1
- Preto do kliky skúšame dať iba vrcholy, ktoré majú dosť veľký stupeň na to, aby mohli patriť do kliky väčšej ako zatiaľ najväčšia nájdená (to neznamená, že do takej kliky aj patria)
/** Trieda, ktora umoznuje najst maximalnu kliku v danom grafe. */ class MaximumClique { /** samotny graf */ private Graph g; /** zoznam vrcholov v najvacsej doteraz najdenej klike */ private LinkedList<Integer> maxClique; /** zoznam vrcholov v aktualnej klike */ private LinkedList<Integer> clique; /** Konstruktor, ktory dostane graf a spusti rekurzivne vyhladavanie */ public MaximumClique(Graph g) { this.g = g; // uloz vstupny graf // vytvor dve prazdne kliky clique = new LinkedList<Integer>(); maxClique = new LinkedList<Integer>(); search(0); // zavolaj rekurziu } /** Hlavna rekurzivna metoda volana z konstruktora. * Metoda skusi pouzit aj vynechat vrchol vertex * potom skusa vsetky moznosti pre vrcholy vertex+1...n-1. */ private void search(int vertex) { // ak aktualna klika je vacsia ako doterajsie maximum, uloz ju if (clique.size() > maxClique.size()) { // konstruktor LinkedListu moze vytvorit kopiu inej Collection maxClique = new LinkedList<Integer>(clique); } // ak uz sme vycerpali vsetky vrcholy, nie je co skusat dalej if (vertex == g.getNumberOfVertices()) { return; } // otestuj, ci sa vrchol vertex da pridat do kliky // a ci ma sancu byt vo vacsej klike ako doteraz najdena if (isConnected(g, vertex, clique) && degree(g, vertex) + 1 > maxClique.size()) { // ak ano, pridaj ho do kliky a zavolaj rekurziu clique.addLast(vertex); search(vertex + 1); // odober vertex x kliky clique.removeLast(); } // preskoc vertex a zavolaj rekurziu na ostatne vrcholy search(vertex + 1); } /** pomocna metoda, ktora overi, ci vrchol vertex je v grafe g * spojeny s kazdym z vrcholov v mnozine vertexSet. */ private static boolean isConnected(Graph g, int vertex, Collection<Integer> vertexSet) { // iterujeme cez mnozinu vertexSet for(int v : vertexSet) { if (!g.existsEdge(vertex, v)) { // over hranu return false; // hrana nie je } } return true; // vsetky hrany najdene } /** pomocna metoda, ktora zisti stupen vrchola vertex v grafe g */ private static int degree(Graph g, int vertex) { // iterujeme cez susedov vrchola, zvysujeme pocitadlo result int result = 0; for(int x : g.adjVertices(vertex)) { result++; } return result; } /** vrati maximalnu kliku najdenu v grafe g */ public List<Integer> maxClique() { // vrat nemenitelnu kopiu nasej najlepsej kliky return Collections.unmodifiableList(maxClique); } }
Cvičenia:
- Čo by program robil, ak by sme vynechali test na stupeň vrchola z rekurzívnej funkcie?
- Čo by program robil, ak by sme vynechali aj test isConnected?
- Čo program robí, ak ho spustíme na grafe bez hrán?
- Ktorá reprezentácia grafu je vhodnejšia pre tento algoritmus?
Základy OOP v C++
- Skrátená verzia, trochu viac detailov viď archív z 2016
Viac informácií nájdete napríklad v týchto dvoch tutoriáloch:
- http://pages.cs.wisc.edu/~hasti/cs368/CppTutorial/
- http://www.horstmann.com/ccj2/ccjapp3.html
- Základy použitia STL knižnice (generické triedy) (mali sme v zimnom semestri, Prednáška 23)
Ilustračný príklad: trieda Interval
/** Trieda reprezentujuca uzavrety interval s celociselnymi * suradnicami oboch koncov. */ class Interval { public: /** Konstruktor so zadanymi suradnicami zaciatku a konca */ Interval(int newStart, int newEnd); /** Vrati lavy koniec intervalu */ int getStart() const; /** Vrati pravy koniec intervalu */ int getEnd() const; /** Vrati dlzku intervalu */ int length() const; /** Porovna intervaly najprv podla laveho konca, * pri rovnosti podla praveho. */ bool operator <(const Interval &other) const; private: int start; int end; }; Interval::Interval(int newStart, int newEnd) : start(newStart), end(newEnd) { } int Interval::getStart() const { return start; } int Interval::getEnd() const { return end; } int Interval::length() const { return end - start; } bool Interval::operator<(const Interval& other) const { return start < other.start || (start == other.start && end < other.end); }
Niektoré rozdiely medzi triedami v Jave a C++:
- V C++ oddeľujeme deklaráciu triedy (premenné a hlavičky metód) od implementácie metód
- Implementáciu niektorých veľmi krátkych metód môžeme dať priamo do deklarácie
- V dlhšom programe dáme deklaráciu napr. do súboru Interval.h a implementácie metód do súboru Interval.cpp
- Ak chceme používať intervaly, dáme do hlavičky #include "Interval.h"
- Na rozdiel od Javy pomenovanie súborov nemusí sedieť s menani tried
- Deklarácia triedy rozdelená na časti public, private, protected, za deklaráciou bodkočiarka
- Pri implementácii musíme pred názov dať meno triedy napr. Interval::length
- Štruktúra struct je skoro to isté ako trieda, iba default prístup je public
Konštruktor
- premenné objektu môžeme inicializovať pred začiatkom tela konštruktora
Interval::Interval(int newStart, int newEnd) : start(newStart), end(newEnd) { }
- môžeme to však spraviť aj obyčajnými priradeniami
Interval::Interval(int newStart, int newEnd) { start = newStart; end = newEnd; }
- prvý spôsob vhodný ak premenná je objekt - do zátvorky dáme parametre konštruktora
class SomeClass { Interval myInterval; SomeClass(int start, int end); ... }; SomeClass::SomeClass(int start, int end) : myInterval(start, end) { }
- použitie konštruktora:
- inicializácia lokálnej premennej i1: Interval i1(2, 4)
- vytvorenie nového objektu pomocou new: Interval *pi = new Interval(2, 4);
- vytvorenie anonymnej dočasnej premennej, ktorá sa inicializuje a nakopíruje do vektora: a.push_back(Interval(5, 8));
Preťaženie operátorov
- Väčšinu operátorov, ktoré majú aspoň jeden operand objekt, môžeme preťažiť, teda vymyslieť im vlastný význam
- V našom príklade preťažujeme < (využije sa pri triedení)
- Viac Prednáška 23
Const
- Niektoré metódy sú označené ako const, čo znamená, že nemenia objekt
- Naopak parametre metód môžu byť const referencie
- Nemusia sa kopírovať, ale máme zaručené, že nebudú zmenené
bool Interval::operator<(const Interval& other) const { return start < other.start || (start == other.start && end < other.end); }
Aritmetický strom v C++
- verzia v Jave, pozri Prednáška 26
class Node { public: virtual int evaluate() const = 0; // abstraktna metoda virtual void print() const = 0; // abstraktna metoda virtual ~Node() { } // destruktor s prazdnym telom }; class BinaryNode : public Node { public : BinaryNode(Node *newLeft, Node *newRight); virtual ~BinaryNode(); protected: Node *left, *right; }; BinaryNode::BinaryNode(Node *newLeft, Node *newRight) : left(newLeft), right(newRight) { } BinaryNode::~BinaryNode() { delete left; delete right; } class ConstantNode : public Node { public : ConstantNode(int newValue); virtual int evaluate() const; virtual void print() const; private: int value; }; ConstantNode::ConstantNode(int newValue) : Node(), value(newValue) { } int ConstantNode::evaluate() const { return value; } void ConstantNode::print() const { cout << value; } class PlusNode : public BinaryNode { public: PlusNode(Node *newLeft, Node *newRight); virtual int evaluate() const; virtual void print() const; }; PlusNode::PlusNode(Node* newLeft, Node* newRight) : BinaryNode(newLeft, newRight) { } int PlusNode::evaluate() const { return left->evaluate()+right->evaluate(); } void PlusNode::print() const { cout << "("; left->print(); cout << "+"; right->print(); cout << ")"; } void treeTest() { Node * tree = new PlusNode(new PlusNode(new ConstantNode(2), new ConstantNode(1)), new ConstantNode(5)); tree->print(); cout << endl << tree->evaluate() << endl; delete tree; }
Dedenie
- BinaryNode je podtriedou Node:
class BinaryNode : public Node { public : BinaryNode(Node *newLeft, Node *newRight); virtual ~BinaryNode(); protected: Node *left, *right; };
- V hornej časti konštruktora môžeme zavolať konštruktor nadtriedy:
PlusNode::PlusNode(Node* newLeft, Node* newRight) : BinaryNode(newLeft, newRight) { }
- Pozor, ak chceme, aby sa metódy správali polymorfne, musia byť deklarované ako virtual
- Abstraktné metódy sa píšu takto: virtual int evaluate() const = 0;
Deštruktor
- Špeciálna metóda, opak konštruktora
- spúšťa sa automaticky pri zániku objektu (keď na objekt spustíme delete alebo ak objekt v lokálnej premennej a funkcia končí)
- zvyčajne sa používa na odalokovanie pamäte, prípadne ďalšie upratovanie
- deštruktory sú často virtuálne
- po skončení deštruktora sa zavolá deštruktor nadtriedy
BinaryNode::~BinaryNode() { delete left; // rekurzivne zavolá deštruktor na ľavý podstrom delete right; }
Uloženie v pamäti
V Jave:
- každá premenná buď primitívny typ alebo referencia na objekt/pole
- všetky objekty alokované cez new
- odalokované automaticky
- do premennej typu nejaká trieda môžeme priradiť aj referenciu na objekt z podtriedy
V C++:
- premenná môže priamo obsahovať objekt, alebo referenciu na objekt alebo smerník
- naopak smerník môže ukazovať aj na primitívny typ
- objekty alokované cez new treba zmazať
- do premennej typu nejaká trieda sa objekty kopírujú
- môžeme predefinovať operátor priradenia a copy konštruktor, aby fungovali ako treba
- nemôžeme priradiť objekt podtriedy, lebo by sa nemusel zmestiť
- smerníky fungujú podobne ako referencie v Jave, použijeme ak chceme využiť polymorfizmus
Cvičenia 22
Rozcvička
Riešte rozcvičku na testovači.
Príprava na skúšku
Úlohou tohto cvičenia je zoznámiť sa s knižnicou GraphGUI, ktorú budete používať na skúške a tiež si precvičiť prehľadávanie s návratom na grafoch.
Úloha A: Stiahnite si graphgui.zip a rozzipujte ho. Potom si ho otvorte v Netbeans pomocou New project, ako typ projektu zvoľte Java Project with Existing Sources, na ďalšej obrazovke vyplňte Project Name graphgui a na ďalšej pomocou Add Folder pridajte adresár s rozzipovanými súbormi. Je dobré si tento postup vyskúšať v učebni, aby ste na skúške nemali problémy. Projekt skúste skompilovať, spustiť a pozrite si, čo program robí.
Úloha B: Do súboru Editor.java doprogramujte, aby sa po stlačení tlačidla Run Editor otvorilo editovacie okienko, v ktorom je pre každý vrchol jeden ovládací prvok s číslom vrcholu a používateľ môže pre každý vrchol nastaviť, aby sa jeho farba zmenila na zelenú. Ovládacie prvky majú byť umiestnené pod sebou a na spodku bude ovládací prvok Button s nápisom OK, ktorý po stlačení označené vrcholy prefarbí. Ak bude okno zavreté bez stlačenia OK, zmeny sa nevykonajú. Pomôcky:
- Na zmenu farby použite metódu setColorName("green") rozhrania Vertex.
- Ako vhodný Layout aplikácie odporúčame GridPane
- Odporúčané ovládacie prvky sú RadioButton (zaujímavé metódy setSelected, isSelected), pripadne ListView (vhodný SelectionModel je SelectionMode.MULTIPLE a jeho metódy getSelectedIndices() resp. getSelectedItems()).
Úloha C: Do súboru GraphAlgorithm.java doprogramujte, aby po stlačení tlačidla Action program spustil algoritmus hľadania najväčšej kliky a vrcholy v tejto klike aby boli vyfarbené žltou farbou (nastavte im meno farby "yellow") a ostatné vrcholy mimo kliky bielou farbou (nastavte im meno farby "white"). Algoritmus upravte z program na hľadanie maximálnej kliky (trieda MaximumClique). Metóda performAlgorithm vráti počet vrcholov v nájdenej klike premenený na reťazec bez ďalšieho sprievodného textu.
Úloha D: Do súboru GraphAlgorithm.java doprogramujte hľadanie dominujúcej množiny z ukážkových príkladov na skúšku.
Ďalšie príklady na grafy
- Napíšte program, ktorý dostane orientovaný graf a v prípade, že je v grafe cyklus, vypíše ho (ak je cyklov viac, vypíše hociktorý). Odporúčame začať z programu na topologické triedenie pomocou prehľadávania do hĺbky (trieda TopologicalSort), pričom si pre každý vrchol do poľa uložte, z ktorého iného vrcholu ste ho objavili (podobne ako pole prev pri prehľadávaní do šírky). V momente, keď program nastavuje acyclic=false, by ste mali vedieť nájsť cyklus.
- Vo vašom programe pre hľadanie dominujúcej množiny (viď príprava na skúšku vyššie, úloha D) pridajte orezávanie neperspektívnych vetiev výpočtu. Skúste napríklad niektorú z týchto stratégií:
- ak ste už našli dominujúcu množinu určitej veľkosti, nemá zmysel skúmať a rozširovať množiny, ktoré majú zaručene viac prvkov
- alebo nechceme pridať do X vrchol, ktorý nijako nepomôže, lebo aj on aj všetci jeho susedia už majú suseda v X
- alebo nechceme vynechať vrchol, ak niektorý z jeho susedov nemá ešte suseda v X a ani nemá suseda s väčším číslom, ktorý ešte môže byť pridaný do X v ďalších rozhodnutiach
- V príklade s koňom na šachovnici z minulých cvičení použite prehľadávanie do šírky namiesto prehľadávania do hĺbky.
- Pre každé prázdne políčko v šachovnici spočítajte, na aký najmenší počet ťahov na neho vie kôň doskákať. Ak naňho vôbec nevie doskákať, vypíšte -1. Umožnite tiež vypísať postupnosť polí, na ktoré kôň pri presune skočí.
- Prehľadávanie do šírky nájdete v programe pre prednášku 33.