Programovanie (2) v Jave
1-INF-166, letný semester 2023/24

Prednášky · Pravidlá · Softvér · Testovač
· Vyučujúcich predmetu možno kontaktovať mailom na adresách uvedených na hlavnej stránke. Hromadná mailová adresa zo zimného semestra v letnom semestri nefunguje.


2018/19 Programovanie (2) v Jave

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
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 · #Textový editor: kompletný zdrojový kód · #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

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

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 E-prg.png.
  • 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:
  • 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.


CV22-graf.png
  • 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.
PROG-C24-graf.png
  • 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.

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

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:
(a) Situácia na začiatku vykonávania metódy pokus, (b) situácia na konci vykonávania metódy pokus.
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.

Čí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

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ú
  • Objektyinš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

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

Ú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);
    }
}
Okno zobrazené po spustení aplikácie.

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);
    }
}
Hlavné okno výslednej aplikácie.

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í.

Výsledná aplikácia.
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.
Výsledný vzhľad aplikácie.

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
Vzhľad aplikácie po nastavení okrajov.

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:

Vzhľad aplikácie po dokončení návrhu rozloženia ovládacích prvkov v mriežke.
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);
Vzhľad aplikácie po pridaní pravej časti.

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

  1. Vytvorte JavaFX aplikáciu realizujúcu prevod uhla v stupňoch na radiány a naopak.
  2. 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.
  3. 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()).
  4. 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.
  5. 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).
  6. 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

    ...
}
Vzhľad aplikácie po pridaní hlavnej ponuky.

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.

Výsledný vzhľad kontextovej ponuky.
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

Výsledný vzhľad dialógového okna.

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

Textový editor: kompletný zdrojový kód

package editor;

import java.io.*; 
import java.nio.file.*;
import java.nio.charset.*;
import java.util.*;
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.text.*;
import javafx.scene.input.*;
import javafx.beans.value.*;

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;
    }
}

class FontDialog {
    private final Stage stage; 
    
    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;
    
    private final ToggleGroup tg1;
    private final ToggleGroup tg2;
    
    private boolean confirmed;
    
    private void okAction() {
        confirmed = true;
        stage.close();
    }
    
    private void cancelAction() {
        stage.close();
    }
    
    public FontDialog() {
        BorderPane border = new BorderPane();
        
        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());
        lboxFamilies.setPrefHeight(200);
        
        tg1 = new ToggleGroup();
        
        rbSizes = new ArrayList<>();
        for (int i = 8; i <= 24; i += 2) {
            RadioButton rb = new RadioButton(Integer.toString(i));
            rb.setToggleGroup(tg1);
            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);
             
        tg2 = new ToggleGroup();
        
        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);
        rbBold.setToggleGroup(tg2);
        rbItalic.setToggleGroup(tg2);
        rbBoldItalic.setToggleGroup(tg2);
        
        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);
        
        btnOK.setOnAction((ActionEvent event) -> {
            okAction();
        });
        btnCancel.setOnAction((ActionEvent event) -> {
            cancelAction();
        });
        
        Scene scene = new Scene(border, 500, 360);
        
        stage = new Stage();
        stage.initStyle(StageStyle.UTILITY);
        stage.initModality(Modality.APPLICATION_MODAL);
        stage.setScene(scene);
        stage.setTitle("Formát písma");
    }
    
    public FontAttributes showFontDialog(FontAttributes 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);
            } 
        }

        confirmed = false;
        stage.showAndWait();

        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;
        }
    }    
}

public class Editor extends Application {
    private final int N = 1000; 
    
    private File openedFile; 
    private boolean openedFileChanged; 
    
    private Stage primaryStage;
    private TextArea textArea;
    private FontDialog fontDialog;
    
    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;    
    
    private ContextMenu cm;
    private MenuItem cmiFormatFont;
    private CheckMenuItem cmiFormatWrap;
    
    private String fontFamily = "Tahoma";
    private FontWeight fontWeight = FontWeight.NORMAL;
    private FontPosture fontPosture = FontPosture.REGULAR;
    private double fontSize = 16;
    
    private boolean newEmptyAction() {
        if (saveBeforeClosingAlert()) {                
            updateOpenedFileInformation(null, true);
            textArea.clear();
            return true;
        } else {
            return false;
        }
    }
    
    private boolean newRandomAction() {
        if (newEmptyAction()) {
            StringBuilder sb = new StringBuilder();
            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());
            return true;
        } else {
            return false;
        }
    }
    
    private boolean openAction() {
        if (saveBeforeClosingAlert()) {
            File file = chooseFileToOpen();
            if (file != null) {
                try {
                    textArea.setText(loadFromFile(file));
                    updateOpenedFileInformation(file, false);
                } catch (IOException e) {
                    System.err.println("Nieco sa pokazilo.");
                }
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
    
    private boolean saveAction() {
        if (openedFile == null) {
            return saveAsAction();
        } else {
            try {
                writeToFile(openedFile, textArea.getText());
                updateOpenedFileInformation(openedFile, false);
            } catch (IOException e) {
                System.err.println("Nieco sa pokazilo.");
            }
            return true;
        }
    }    
    
    private boolean saveAsAction() {
        File file = chooseFileToSave();
        if (file != null) {
            try {
                writeToFile(file, textArea.getText());
                updateOpenedFileInformation(file, false);
            } catch (IOException e) {
                System.err.println("Nieco sa pokazilo.");
            }
            return true;
        } else {
            return false;
        }
    }
    
    private boolean exitAction() {
        if (saveBeforeClosingAlert()) {
            Platform.exit();
            return true;
        } else {
            return false;
        }
    }    
    
    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;
    }
    
    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);
    }
    
    private FileChooser prepareFileChooser() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
        fileChooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter("Textové súbory (*.txt)", "*.txt"), 
                new FileChooser.ExtensionFilter("Všetky súbory (*.*)", "*.*"));
        return fileChooser;
    }
    
    private File chooseFileToOpen() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Otvoriť");
        File file = fileChooser.showOpenDialog(primaryStage);
        return file;
    }
    
    private File chooseFileToSave() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Uložiť");
        File file = fileChooser.showSaveDialog(primaryStage);
        return file;
    }
    
    private boolean handleStageCloseRequest(WindowEvent event) {
        if (saveBeforeClosingAlert()) {
            return true;
        } else {
            event.consume();
            return false;
        }
    }
    
    private boolean saveBeforeClosingAlert() {
        if (openedFileChanged) {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Uložiť súbor?");
            alert.setHeaderText(null);
            alert.setContentText("Uložiť zmeny v súbore?");

            ButtonType buttonTypeYes = new ButtonType("Áno");
            ButtonType buttonTypeNo = new ButtonType("Nie");
            ButtonType buttonTypeCancel = new ButtonType("Zrušiť", ButtonBar.ButtonData.CANCEL_CLOSE);

            alert.getButtonTypes().setAll(buttonTypeYes, buttonTypeNo, buttonTypeCancel);
                        
            Optional<ButtonType> result = alert.showAndWait();
            if (result.get() == buttonTypeYes) {
                return saveAction();
            } else if (result.get() == buttonTypeNo) {
                return true;
            } else {
                return false;
            }
        } else {
            return true;
        }
    }
    
    private void handleTextAreaChange() {
        if (!openedFileChanged) {
            updateOpenedFileInformation(openedFile, true);
        }
    }
    
    private void updateOpenedFileInformation(File file, boolean hasChanged) { 
        openedFile = file;
        openedFileChanged = hasChanged;
        String changeIndicator;
        if (hasChanged) {
            changeIndicator = "*";
            miFileSave.setDisable(false);
        } else {
            changeIndicator = "";
            miFileSave.setDisable(true);
        }
        String paren;
        if (file == null) {
            paren = "(neuložené v žiadnom súbore)" + changeIndicator;
        } else {
            paren = "(" + file.getName() + ")" + changeIndicator;
        }
        primaryStage.setTitle("Textový editor " + paren);
    }
    
    private void applyFont() {
        textArea.setFont(Font.font(fontFamily, fontWeight, fontPosture, fontSize));
    }
    
    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;
        fontDialog = new FontDialog();
        
        BorderPane border = new BorderPane();
                
        textArea = new TextArea();
        border.setCenter(textArea);
        textArea.setPadding(new Insets(5,5,5,5));
        applyFont();
        
        textArea.textProperty().addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) -> { 
            handleTextAreaChange();
        });       
        
        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);
        
        cmiFormatFont.setOnAction((ActionEvent event) -> {
            fontAction();
        });
        
        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);
        mFormat.getItems().addAll(miFormatFont, miFormatWrap);
        
        menuBar.getMenus().add(mFile);
        menuBar.getMenus().add(mFormat);
        
        miFileNewEmpty.setAccelerator(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN));
        miFileOpen.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN));
        miFileSave.setAccelerator(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN));
        
        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();
        });
        miFormatWrap.selectedProperty().bindBidirectional(cmiFormatWrap.selectedProperty());
        miFormatWrap.selectedProperty().bindBidirectional(textArea.wrapTextProperty());
        
        Scene scene = new Scene(border, 800, 600);

        primaryStage.setOnCloseRequest((WindowEvent event) -> {
            handleStageCloseRequest(event);
        });
        primaryStage.setScene(scene);
        updateOpenedFileInformation(null, true); 
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

}

Cvičenia 19

  1. Ak ste na minulom cvičení nestihli všetky príklady, môžete začať s nimi.
  2. 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.
  3. 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.
  4. 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.

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

  1. Riešte rozcvičku na testovači.
  2. 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

  1. Riešte bonus na testovači.
  2. 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

Neorientovaný graf s kostrou najkratších ciest pre start == 0.

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);
PROG-P36-graf1.png

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

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");
        }

    }

}

Príklad vstupu bez cyklu:

4 4
1 0
2 0
2 1
3 1

Príklad vstupu 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++

Viac informácií nájdete napríklad v týchto dvoch tutoriáloch:

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++

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.