Programovanie (1) v C/C++
1-INF-127, ZS 2024/25

Úvod · Pravidlá · Prednášky · Softvér · Testovač
· Kontaktujte nás pomocou e-mailovej adresy E-prg.png (bude odpovedať ten z nás, kto má príslušnú otázku na starosti alebo kto má práve čas).
· Prosíme študentov, aby si pravidelne čítali e-maily na @uniba.sk adrese alebo aby si tieto emaily preposielali na adresu, ktorú pravidelne čítajú.


2020/21 Programovanie (2) v Jave

Z Programovanie
Verzia z 18:32, 16. september 2021, ktorú vytvoril Brona (diskusia | príspevky)
(rozdiel) ← Staršia verzia | Aktuálna úprava (rozdiel) | Novšia verzia → (rozdiel)
Skočit na navigaci Skočit na vyhledávání

Obsah

Letný semester

Kontakt na vyučujúcich:

miestnosť M-227
E-pk2.png
  • Lukáš Kiss (cvičenia):
miestnosť M-25
E-lk.png

Rozvrh:

  • Prednášky:
pondelok, 9:00 – 10:30, MS Teams
  • Cvičenia:
streda, 11:30 – 13:00, MS Teams
  • Konzultácie po dohode mailom

Prednášky a cvičenia v letnom semestri

Týždeň 15.2. – 21.2. Úvod do jazyka Java
Prednáška 1 · Cvičenia 1 · Pravidlá · Softvér
Týždeň 22.2. – 28.2. Základy objektovo orientovaného programovania I
Prednáška 2 · Cvičenia 2 · Test pre pokročilých
Týždeň 1.3. – 7.3. Základy objektovo orientovaného programovania II
Prednáška 3 · Cvičenia 3
Týždeň 8.3. – 14.3. Výnimky, generické programovanie
Prednáška 4 · Cvičenia 4 · Test č. 1 · Riešenia úloh z testu
Týždeň 15.3. – 21.3. Java Collections, lokálne a anonymné triedy, iterátory
Prednáška 5 · Cvičenia 5
Týždeň 22.3. – 28.3. Javadoc, Testovanie, JUnit, Grafy a grafové algoritmy I
Prednáška 6 · Cvičenia 6 · Test č. 2 · Riešenia úloh z testu
Týždeň 29.3. – 4.4. Grafy a grafové algoritmy II
Prednáška 7 · Cvičenia 7
Týždeň 5.4. – 11.4. Veľkonočný pondelok
Cvičenia 8 · Test č. 3 · Riešenia úloh z testu
Týždeň 12.4. – 18.4. Grafy a grafové algoritmy III
Prednáška 9 · Cvičenia 9 · Test č. 4 · Riešenia úloh z testu
Týždeň 19.4. – 25.4. Lambda výrazy, JavaFX I
Prednáška 10 · Cvičenia 10
Týždeň 26.4. – 2.5. JavaFX II
Prednáška 11 · Cvičenia 11 · Test č. 5 · Riešenia úloh z testu
Týždeň 3.5. – 9.5. JavaFX III
Prednáška 12 · Cvičenia 12 · Náhradný test č. 1 · Riešenia úloh z testu
Týždeň 10.5. – 16.5. JavaFX IV
Prednáška 13 · Cvičenia 13 · #Textový editor: kompletný zdrojový kód · Náhradný test č. 2 · Riešenia úloh z testu

Letný semester, prednáška č. 1

Úvodné informácie

  • V prípade potreby kontaktujte vyučujúcich mailom (kontakty možno nájsť na úvodnej stránke).
  • Predmet sa bude riadiť pravidlami pre letný semester.
  • Okrem bodovaných úloh budú na testovači zverejňované aj nebodované cvičenia.
  • Konzultácie po predchádzajúcej dohode mailom.
  • Heslo na testovači: rovnaké ako minulý semester; u tých, čo minulý semester nechodili, zmenené na prog2. V prípade problémov s prihlasovaním dajte vedieť.
  • Softvér potrebný na tomto predmete:
    • Java SE 15 (alebo ľubovoľná verzia od 11 vyššie).
    • Platforma JavaFX pre vývoj aplikácií s grafickým používateľským rozhraním.
    • Vývojové prostredie (IDE) pre Javu, odporúčame IntelliJ IDEA Community Edition.
    • Informácie o inštalácii a základnom použití uvedeného softvéru.
    • Vhodnou príležitosťou na vyriešenie prípadných problémov môžu byť prvé cvičenia.
  • Základné zdroje informácií o jazyku Java:
  • Na začiatku stredajších cvičení bude zverejnená prvá bonusová domáca úloha.

Základné črty jazyka Java a programovania v ňom

Prvoradým cieľom tohto predmetu je zvládnutie základov objektovo orientovaného programovania (OOP) prostredníctvom programovacieho jazyka Java. Na úvod je užitočné vedieť o tomto jazyku nasledujúce:

  • Základné syntaktické konštrukcie jazyka Java sú v mnohom veľmi podobné jazykom C a C++. Už tento týždeň by sme mali zvládnuť prepísať do Javy väčšiu časť programov z minulého semestra; budúci týždeň by to už mali byť úplne všetky programy.
  • Jazyk Java je silne (aj keď nie čisto) objektovo orientovaný. Použite tried – ktoré sú popri objektoch základným konceptom OOP – je nutné na napísanie aj toho najjednoduchšieho programu.
  • Na druhej strane je triedu možné použiť aj ako obyčajný „obal” pre niekoľko metód (t.j. funkcií) podobného typu ako v minulom semestri. Takýmto spôsobom budeme s Javou pracovať na dnešnej prednáške. S ozajstným objektovo orientovaným programovaním začneme až budúci týždeň.
  • Programy v jazyku Java sa obvykle nekompilujú do strojového kódu, ale do tzv. javovského bytekódu. Po skompilovaní programu teda nedostávame bežný spustiteľný súbor, ale súbor, ktorý možno spustiť na javovskom virtuálnom stroji (angl. Java Virtual Machine; skr. JVM). Vykonávanie takýchto programov je síce o niečo pomalšie, zato sú však prenositeľné medzi rôznymi operačnými systmémami a architektúrami.

Celkovo ide o jazyk omnoho vyššej úrovne, než jazyk C: v oveľa väčšej miere sa tu abstrahuje od počítačovej architektúry. Java napríklad neumožňuje priamy prístup k pamäťi počítača a o uvoľňovanie alokovanej pamäte sa stará JVM automaticky prostrednictvom mechanizmu tzv. garbage collection. Hoci teda jazyk nie je príliš vhodný na nízkoúrovňové programovanie, tvorba „bežných” používateľských programov je tu podstatne pohodlnejšia, než napríklad v jazyku C. Okrem toho Java disponuje veľkou knižnicou štandardných tried (Java Class Library; skr. JCL), v ktorej je okrem iného implementované aj množštvo algoritmov a dátových štruktúr. Orientáciu v možnostiach ponúkaných touto knižnicou značne uľahčuje dokumentácia k nej.

Rozdiel v úrovni abstrakcie medzi jazykmi C a Java sa premieta aj do typického programátorského štýlu. Od efektívnosti samotnej implementácie sa dôraz obvykle posúva k aspektom softvérového inžinierstva, ako sú napríklad čitateľnosť, rozšíriteľnosť a „spravovateľnosť” kódu. S niektorými elementárnymi princípmi softvérového inžinierstva sa zoznámime aj na tomto predmete.

Prvý program v jazyku Java

Tradične začneme programom, ktorý na konzolu vypíše text Hello, World!.

public class Hello {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }

}
  • Názov súboru sa musí zhodovať s názvom triedy, ku ktorému sa pridá prípona .java – v našom prípade teda musí byť zdrojový kód uložený v súbore Hello.java.
  • Program je potrebné skompilovať javovským kompilátorom a následne spustiť na javovskom virtuálnom stroji – návod možno nájsť na osobitnej stránke. Po skompilovaní programu získame súbor java.class spustiteľný na JVM.

Rozoberme postupne jednotlivé časti uvedeného programu:

  • Funkcia main, ktorá sa podobne ako v C/C++ začne vykonávať bezprostredne po spustení programu, je „obalená” v triede, ktorú sme nazvali Hello. V jazyku Java musí byť všetok kód súčasťou nejakej triedy (význam tried pre OOP zatiaľ ponechajme bokom).
  • Hlavička funkcie main musí byť vždy tvaru ako vyššie. Modifikátory public a static si vysvetlíme neskôr; nasleduje návratový typ void, názov funkcie main a argumenty funkcie main, ktorými sú argumenty programu z príkazového riadku (v podobe poľa reťazcov).
  • Samotný výpis na konzolu realizuje metóda System.out.println.

Keďže jeden väčší program typicky pozostáva z množstva rôznych tried (v rôznych súboroch), je možné triedy ďalej umiestniť do balíkov; program tak často pozostáva z niekoľkých balíkov navzájom súvisiacich tried. Umiestnenie triedy do balíka možno realizovať príkazom na začiatku zdrojového súboru.

package somepackage;

public class Hello {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }

}
  • V našom prípade sme triedu Hello umiestnili do balíka somepackage. Pri práci s IDE ako napr. IntelliJ je často potrebné pridať balík aj do projektu; rozdiel je tiež pri kompilovaní a spúšťaní triedy z príkazového riadku. Viac sa možno dočítať tu.
  • Pokiaľ deklarácia balíka chýba, považuje sa trieda za súčasť nepomenovaného balíka (angl. unnamed package alebo default package). Používanie nepomenovaného balíka vo všeobecnosti nie je dobrá prax, ale je zmysluplné pri menších programoch pre vlastnú potrebu (alebo pre potreby tohto predmetu).
  • Za dobrú prax sa naopak považuje pomenúvanie balíkov tak, aby boli celosvetovo jednoznačne identifikovateľné – za týmto účelom preto boli vybracované špeciálne konvencie, ktoré ale na tomto predmete nebudeme dodržiavať.

Konvencie pomenúvania identifikátorov

Za účelom sprehľadnenia zdrojového kódu a často pomerne rozsiahlych knižníc sa pri pomenúvaní identifikátorov v Jave používajú (okrem iných aj) nasledujúce konvencie:

  • Názvy tried by mali vždy začínať veľkým písmenom (pri viacslovných názvoch začína veľkým písmenom aj každé ďalšie slovo).
  • Názvy premenných a metód by naopak mali vždy začínať malým písmenom (pri viacslovných názvoch začínajú ďalšie slová veľkým písmenom).

Nedodržiavanie týchto konvencií budeme na tomto predmete považovať za chybu. O ďalších konvenciách používaných v tomto smere sa možno dočítať tu.

Príklad o niečo rozsiahlejšieho programu

Nižšie je príklad o niečo rozsiahlejšieho programu v jazyku Java. Pokúste sa s využitím skúseností z minulého semestra uhádnuť, čo tento program robí.

import java.util.*;

public class Program {

    /* Spocita sucet prvkov pola a.
     */
    public static int sum(int[] a) {
        int result = 0;
        for (int i = 0; i <= a.length - 1; i++) {
            result += a[i];
        }
        return result;
    }

    /* Spocita priemer hodnot prvkov pola a.
     */
    public static double average(int[] a) {
        return (double) sum(a) / a.length;
    }

    /* Najde najvacsi prvok pola a.
     */
    public static int max(int[] a) {
        int max = Integer.MIN_VALUE;  // Premenna ma rovnaky nazov ako metoda
        for (int i = 0; i <= a.length - 1; i++) {
            if (a[i] >= max) {
                max = a[i];
            }
        }
        return max;
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int n = scanner.nextInt();
        int[] a = new int[n];

        for (int i = 0; i <= n - 1; i++) {
            a[i] = scanner.nextInt();
        }
        scanner.close();

        System.out.println("Sucet: " + sum(a));
        System.out.println("Priemer: " + average(a));
        System.out.println("Maximum: " + max(a));
    }

}
  • Okamžite vidieť množstvo podobností medzi jazykmi C/C++ a Java, ale aj niekoľko drobných rozdielov.
  • Konštrukcie jazyka Java použité v tomto programe podrobnejšie rozoberieme nižšie.

Základné konštrukcie jazyka Java

Primitívne typy

Jazyk Java podporuje celkovo osem primitívnych dátových typov (void sa za primitívny typ nepovažuje):

  • int: 32-bitové celé čísla so znamienkom, v rozsahu od -231 po 231 - 1; ďalšie celočíselné typy sú byte, short a long (na rozdiel od C/C++ už teda long a short nie sú modifikátory, ale priamo celočíselné typy).
  • double: 64-bitové desatinné čísla s pohyblivou desatinnou čiarkou; ďalší typ desatinných čísel je 32-bitový float.
  • boolean: logické hodnoty true alebo false.
  • char: 16-bitové znaky v kódovaní Unicode (UTF-16); typ teda napríklad podporuje slovenskú diakritiku.

Deklarácie premenných a priradenia zapisujeme rovnako ako v C/C++. Na rozdiel od C/C++ možno napríklad definovať aj lokálnu premennú s rovnakým názvom ako niektorá metóda.

int x = 0;

Kompilátor pritom nedovolí použitie neinicializovanej lokálnej premennej.

int x;
System.out.println(x); // variable x might not have been initialized

Neskôr uvidíme, že prvky polí a premenné tried a ich inštancií sa inicializujú automaticky.

Pretypovanie tiež funguje podobne ako v C/C++, avšak implicitné (automatické) pretypovanie je možné pre menší počet dvojíc typov, než v C/C++. Číselné typy možno usporiadať ako byte ≺ short ≺ int ≺ long ≺ float ≺ double a implicitné pretypovanie je možné iba v smere tohto usporiadania. Ďalej je možné implicitne pretypovať char na int, long, float a double (nie však naopak). Zvyšné pretypovania je nutné robiť explicitne, rovnako ako v C/C++.

int x = 65;
char c = (char) x;
System.out.println(c);

Možno ale napríklad priamo písať char c = 65.

Operátory

Jazyk Java podporuje podobnú sadu operátorov ako jazyky C a C++; ich kompletný zoznam vrátanie precedencií možno nájsť tu. Spomeňme predovšetkým:

  • Operátory priradenia =, +=, *=, atď.
  • Aritmetické operátory +, -, *, /, %. Na celočíselných typoch sa operátor / správa, rovnako ako v C/C++, ako celočíselné delenie.
  • Prefixové a sufixové operátory ++, --.
  • Relačné a porovnávacie operátory ==, !=, <, >, <=, >=.
  • Logické operátory ||, &&, !.

Cykly a podmienky

Nasledujúce konštrukcie sú v Jave veľmi podobné ako v C/C++:

  • Podmienky: if, else a switch.
  • Cykly: for, while, do ... while.
  • Príkazy break a continue.

Drobný rozdiel pri ich použití vyplýva zo skutočnosti, že v Jave nedochádza k automatickému pretypovaniu z napr. číselných typov na boolean. Napríklad nasledujúci kus kódu, ktorý by bol v C/C++ korektný, vyústi v Jave chybu.

int n = 1;
if (n) {  // incompatible types: int cannot be converted to boolean
    // ...           
}

Nápravu dosiahneme nahradením celočíselného výrazu n výrazom logickým.

int n = 1;
if (n != 0) {
    // ...           
}

Jednorozmerné polia

V Jave sa oproti C/C++ používa trochu iná syntax pre deklaráciu poľa.

int[] a; // Deklaracia premennej a reprezentujucej pole celych cisel

Samotné vytvorenie poľa a priradenie referencie naň do premennej a následne možno realizovať podobne ako pri dynamicky alokovaných poliach v C++.

a = new int[10];

Pamäť alokovanú pri vytvorení poľa nie je potrebné manuálne uvoľňovať; stará sa o to automatický „smetiarsky” mechanizmus (angl. garbage collection). Prípadne tiež možno spojiť deklaráciu premennej s vytvorením poľa do jediného príkazu.

int[] a = new int[rozsah];

Namiesto int[] a možno písať aj int a[]; prvý spôsob je ale prirodzenejší, keďže typom premennej a je v oboch prípadoch int[].

Pri vytvorení poľa sa všetky jeho prvky automaticky inicializujú – a to na 0 pri poliach čísel resp. znakov (kde ide o znak s kódom 0, nie o znak '0'), na false pri poliach booleovských hodnôt a na null pri poliach objektov a polí (poľami takéhoto typu sa budeme zaoberať až neskôr). V prípade potreby inicializovať pole vopred známej dĺžky na iné hodnoty možno použiť nasledujúcu skratku.

int[] a = {0, 1, 2, 3, 4, 5};

Týmto príkazom sa do premennej a priradí referencia na šesťprvkové pole obsahujúce postupne hodnoty 0, 1, 2, 3, 4, 5. Takéto priradenie možno realizovať iba na riadku, na ktorom je príslušná premenná deklarovaná. Neskôr možno do premennej a priradiť takto „vymenované” pole nasledovne.

int[] a;

// ...

a = new int[]{0, 1, 2, 3, 4, 5};

Súčasťou poľa je v Jave aj informácia o jeho dĺžke, ku ktorej možno pre pole a pristúpiť cez a.length. Nasledujúci kus kódu tak napríklad do stoprvkového poľa a uloží postupnosť čísel 0, 1, 2, ..., 99.

int[] a = new int[100];
for (int i = 0; i <= a.length - 1; i++) {
    a[i] = i;
}

V prípade pokusu o prístup k prvku poľa mimo jeho rozsah Java za behu programu vyhodí výnimku java.lang.ArrayIndexOutOfBoundsException. Mechanizmom výnimiek a ich spracovaním sa na tomto predmete budeme zaoberať neskôr; v tomto momente je podstatná skutočnosť, že v Jave nie je možné pomocou prístupu k danému poľu prepísať pamäť mimo jeho rozsah (čo je jedna z najnepríjemnejších chýb v C/C++).

Hodnoty vs. referencie v jazyku Java

Napriek tomu, že v jazyku Java neexistuje mechanizmus smerníkov (ani žiadna obdoba smerníkovej aritmetiky), sú všetky premenné okrem premenných primitívnych typov v skutočnosti referenciami predstavujúcimi adresy v pamäti. Presnejšie:

  • Premenné primitívnych typov obsahujú hodnoty týchto primitívnych typov.
  • Premenné všetkých zvyšných typov obsahujú referencie na pole alebo na objekt (aj polia sú v skutočnosti veľmi špeciálne objekty, ale to teraz ponechajme bokom).

Premenné obsahujúce referencie – zatiaľ vieme pracovať iba s premennými ukazujúcimi na pole – sa tak správajú veľmi podobne ako smerníky v C/C++. Operátor = aplikovaný na takéto premenné nekopíruje hodnoty, ale referencie; podobne operátor == neporovnáva hodnoty, ale referencie.

int[] a = {1, 2, 3, 4, 5};
int[] b = a;                   // Premenne a, b ukazuju na to iste patprvkove pole
b[0] = 10;
System.out.println(a[0]);      // Vypise 10
b = new int[a.length];         // Premenne a, b uz teraz ukazuju na dve rozne polia
for (int i = 0; i <= b.length - 1; i++) {
    b[i] = a[i];               // Hodnoty v oboch poliach su odteraz rovnake, polia ako take su rozne
}
System.out.println(a == b);    // Vypise false

Premenná obsahujúca referenciu môže nadobúdať špeciálnu hodnotu null; v takom prípade referencia neukazuje na žiadnu pamäť.

Cyklus for each

V Jave existuje špeciálny variant cyklu for, tzv. cyklus for each, umožňujúci postupne prejsť cez všetky hodnoty prvkov poľa a aj bez indexovej premennej. Napríklad kus kódu

int[] a = {6, 5, 4, 3, 2, 1};
for (int x : a) {
    System.out.println(x);
}

je ekvivalentný nasledujúcemu kódu.

int[] a = {6, 5, 4, 3, 2, 1};
for (int i = 0; i <= a.length - 1; i++) {
    System.out.println(a[i]);
}

Vo všeobecnosti možno povedať, že cyklus for each s iteračnou premennou x pracuje nasledovne:

  • Prechádza postupne pole od začiatku po jeho koniec.
  • V každej iterácii najprv skopíruje hodnotu na danej pozícii poľa do premennej x a následne vykoná príkazy v tele cyklu.

Z toho vyplýva, že pomocou cyklu for each nemožno meniť hodnoty prechádzaného poľa. Napríklad v cykle

for (int x : a) {
    x = 0;
}

sa nemenia jednotlivé prvky poľa, ale iba lokálna premenná x. Avšak v prípade, že sú prvkami poľa referencie, možno pomocou cyklu for each meniť hodnoty, na ktoré tieto referencie ukazujú (to je vlastnosť, ktorú oceníme až neskôr).

Viacrozmerné polia

Popri jednorozmerných poliach možno v Jave pracovať aj s dvojrozmernými poľami, ktoré sa správajú podobne ako polia smerníkov resp. smerníky na smerníky v C/C++. V javovskej terminológii môžeme povedať, že dvojrozmerné pole je poľom polí. Obdĺžnikové pole vytvoríme napríklad nasledovne.

int[][] a = new int[3][4];  // Vytvori pole s troma riadkami a troma stĺpcami

Takéto pole si možno predstaviť ako trojprvkové pole jednorozmerných polí dĺžky 4. Napríklad a[0] je teda jednorozmerné pole zodpovedajúce nultému riadku dvojrozmerného poľa a. S dvojrozmerným poľom potom pracujeme prirodzeným spôsobom.

for (int i = 0; i <= a.length - 1; i++) {
    for (int j = 0; j <= a[i].length - 1; j++) {
        a[i][j] = i + j;
    }
}

Výsledné pole je znázornené na nasledujúcom obrázku.

JPamat1.png

Alternatívne možno najprv inicializovať iba pole jednotlivých riadkov a následne každý z riadkov zvlášť. Takto môžeme vytvoriť aj iné ako obdĺžnikové polia, napríklad „trojuholník” z nasledujúceho príkladu.

int[][] a = new int[3][]; // V tomto momente su a[0], a[1] aj a[2] rovne null (vdaka automatickej inicializacii prvkov pola)
for (int i = 0; i <= a.length - 1; i++) {
    a[i] = new int[i + 1];
}
for (int i = 0; i <= a.length - 1; i++) {
    for (int j = 0; j <= a[i].length - 1; j++) {
        a[i][j] = j;
    }
}

Výsledné pole je znázornené na nasledujúcom obrázku.

JPamat2.png

Z tejto predstavy o reprezentácii dvojrozmerných polí by malo byť zrejmé, že napríklad inicializácia int[][] a = new int[][10]; nie je korektná.

Rovnako ako s dvojrozmernými poľami možno v Jave pracovať aj s poľami o ľubovoľnom konečnom počte rozmerov. Pri inicializácii takýchto polí platí, že je potrebné určiť prvých niekoľko rozmerov (kde „niekoľko” v tomto prípade znamená „aspoň jeden”).

int[][][] a = new int[3][4][5]; // OK
int[][][] b = new int[3][4][];  // OK
int[][][] c = new int[3][][];   // OK
int[][][] d = new int[][4][5];  // Chyba
int[][][] e = new int[3][][5];  // Chyba
int[][][] f = new int[][][];    // Chyba

Statické metódy

Obdobou funkcií, ako ich poznáme z minulého semestra, sú v jazyku Java statické metódy triedy.

  • Definujú sa vždy vo vnútri nejakej triedy s použitím podobnej syntaxe ako v C/C++. Pred návratový typ je potrebné napísať kľúčové slovo static, vďaka ktorému pôjde o statickú metódu triedy (čiže zjednodušene povedané „obyčajnú funkciu”) a nie o metódu inštancie triedy, čo je koncept objektovo orientovaného programovania, s ktorým sa zoznámime na budúcej prednáške.
  • Pred klúčové slovo static ešte možno pridať modifikátor prístupu ako napr. public alebo private hovoriaci o viditeľnosti metódy z iných tried a balíkov. Modifikátormi prístupu sa budeme detailnejšie zaoberať neskôr.
  • Statické metódy voláme rovnako ako funkcie v C/C++ (pri statickej metóde metoda z inej triedy Trieda pri jej volaní píšeme Trieda.metoda). Kľúčové slovo return sa tiež správa podobne ako v C/C++, avšak program obsahujúci metódu s návratovým typom rôznym od void a chýbajúcim príkazom return nie je možné skompilovať. Rovnako ako v C/C++ funguje aj rekurzia.

Príklad:

public class Trieda {

    static long faktorial(int n) {
        if (n == 0) {
            return 1;
        } else {
            return n * faktorial(n - 1);
        }
    }

    public static void main(String[] args) {
        System.out.println(faktorial(18));
    }
}

Metóda main

  • Špeciálnou statickou metódou je metóda main, ktorá sa vykoná bezprostredne po spustení programu (presnejšie danej triedy). Tá musí mať návratový typ void, modifikátor prístupu public a jediný argument typu String[] reprezentujúci pole argumentov programu z príkazového riadku (prípadné metódy main s inou hlavičkou sa považujú za „obyčajné” metódy a po spustení programu sa nevykonávajú).
public class Trieda {

    public static void main() {
        System.out.println("Tento text sa nikdy nevypise.");
    }

    public static void main(String[] args) {
        System.out.println("Pocet argumentov: " + args.length);
    }
}
  • Každá trieda, od ktorej po skompilovaní očakávame spustiteľnosť na JVM, musí mať definovanú hlavnú metódu main; často však píšeme aj triedy, ktoré slúžia len na použitie z iných tried.
  • V IDE ako napr. IntelliJ je zvyčajne potrebné vybrať hlavnú triedu projektu, čo je trieda, ktorú sa prostredie bude snažiť spúšťať po spustení celého projektu. Táto trieda tak musí mať definovanú metódu main.

Predávanie argumentov metód

Argumenty metód sa v jazyku Java vždy predávajú hodnotou.

  • Pre argumenty sa tedy vždy vytvoria nové lokálne premenné, do ktorých sa skopírujú hodnoty argumentov, s ktorými bola metóda volaná.
  • Nie je teda možné napísať metódu, ktorá pozmení premenné z volajúcej metódy. Ak sú ale argumentmi metódy referencie (polia alebo objekty), je možné pozmeniť hodnoty, na ktoré táto referencia ukazuje (t.j. napríklad zmeniť obsah poľa alebo premenné objektu).

Príklad:

public class Trieda {

    static void f(int n, int[] a) {
        n = 6;
        a[0] = 7;
        a = new int[]{8, 9, 10, 11, 12};
    }

    public static void main(String[] args) {
        int n = 0;
        int[] a = {1, 2, 3, 4, 5};
        f(n, a);
        System.out.println(n);     // Vypise 0
        System.out.println(a[0]);  // Vypise 7
    }
}
  • Špeciálne napríklad v Jave nie je možné napísať funkciu swap, ktorá vymení hodnoty dvoch premenných primitívnych typov.

Práca s reťazcami

Zvyšok tejto prednášky sčasti presahuje jej rámec tým, že začneme pracovať s niektorými špeciálnymi objektmi bez toho, aby sme si vysvetlili mechanizmus objektov vo všeobecnosti. Už pomerne elementárne úkony, ako práca s reťazcami alebo so vstupom a výstupom, sa totiž v Jave realizujú s využitím objektov. Zvyšok tejto prednášky je motivovaný praktickou potrebou zvládnutia týchto úkonov; hlbšie pochopenie nasledujúceho materiálu nadobudneme budúci týždeň.

Trieda String

Reťazce znakov sú v Jave objektmi triedy String. Premenné typu String teda majú charakter referencií na samotný objekt. Objekty triedy String sú konštantné reťazce – po vytvorení ich už teda nemožno meniť. Premennú typu String ale samozrejme môžeme meniť tak, že do nej priradíme referenciu na iný objekt typu String.

  • Text ohraničený úvodzovkami sa považuje za reťazec typu String.
  • Operátor + realizuje zreťazenie reťazcov. Stačí dokonca, aby bol aspoň jeden z operandov reťazec, zvyšné sa na String skonvertujú automaticky.

Príklad:

String s = "Hello, World!";
System.out.println(s);

int n = 42;
s = "Hodnota premennej n je " + n + ".";
System.out.println(s);

Keďže sú premenné typu String referenciami na objekt, operátor = tiež kopíruje iba referencie a operátor == neporovnáva hodnoty reťazcov (t.j. samotné texty), ale adresy v pamäti. Vzhľadom na konštantnosť reťazcov je tu častejším zdrojom chýb nesprávne použitie operátora ==. Porovnanie reťazcov správne realizuje metóda equals.

String str1 = "nejaky text";
String str2 = str1;             // str2 a str1 ukazuju na to iste miesto v pamati
String str3 = str1 + "";        // str3 a str1 ukazuju na rozne miesta v pamati, ktore obsahuju rovnake retazce
String str4 = new String(str1); // str4 a str1 ukazuju na rozne miesta v pamati, ktore obsahuju rovnake retazce
if (str2 == str1) {
    System.out.println("str2 == str1");
}
if (str3 == str1) {
    System.out.println("str3 == str1");
}
if (str4 == str1) {
    System.out.println("str4 == str1");
}
if (str2.equals(str1)) {
    System.out.println("str2.equals(str1)");
}
if (str3.equals(str1)) {
    System.out.println("str3.equals(str1)");
}
if (str4.equals(str1)) {
    System.out.println("str4.equals(str1)");
}

Z ďalších metód triedy String, ktoré možno použiť na manipuláciu s reťazcami, spomeňme aspoň tieto:

  • Metóda length vráti dĺžku daného reťazca. Pozor: keďže ide o metódu (bez argumentov), na rozdiel od polí pre reťazec s píšeme s.length() so zátvorkami na konci.
  • Metóda charAt s jedným celočíselným argumentom vráti znak na danej pozícii.
String s = "retazec";
for (int i = 0; i <= s.length() - 1; i++) {
    System.out.println(s.charAt(i));
}

Trieda StringBuilder

V prípade potreby daný reťazec často modifikovať by bolo s použitím triedy String potrebné pri každej modifikácii vytvoriť nový objekt, čo je pomerne pomalé. Rýchlejšou alternatívou je použitie triedy StringBuilder reprezentujúcej modifikovateľný reťazec, ktorý je tiež konvertovateľný na String.

Napríklad reťazec abc...z obsahujúci všetky písmená malej anglickej abecedy tak môžeme vytvoriť dvoma rôznymi spôsobmi: buď pomocou pomalého vytvárania 27 rôznych String-ov

String abeceda = "";
for (char c = 'a'; c <= 'z'; c++) {
    abeceda = abeceda + c;  
}

alebo pomocou rýchlejšieho vytvorenia jedného objektu typu StringBuilder, jeho postupných modifikácií a následného vytvorenia jedného objektu typu String.

StringBuilder buffer = new StringBuilder();
for (char c = 'a'; c <= 'z'; c++) {
     buffer.append(c);  
}
String abeceda = buffer.toString();

Reťazec reprezentovaný objektom typu StringBuilder môžeme aj priamo vypísať na konzolu.

System.out.println(buffer);

Naopak v prípadoch, keď reťazec nie je potrebné modifikovať, je implementácia cez String o niečo efektívnejšia.

Vstup a výstup

Zameriame sa teraz na základy práce s textovým vstupom a výstupom – či už na konzole, alebo v podobe textových súborov.

Výstupné prúdy: trieda PrintStream

Textový výstup možno v Jave najjednoduchšie produkovať pomocou triedy PrintStream, ktorú je nutné importovať z balíka java.io; prípadne je možné importovať aj kompletný balík java.io.

import java.io.*; // alebo: import java.io.PrintStream;

public class Trieda {
    
    public static void main(String[] args) {
        // ...
    }
}

Výstupný prúd pre zápis do textového súboru vystup.txt vytvoríme napríklad nasledovne.

PrintStream out = new PrintStream("vystup.txt");
  • V prípade, že súbor vystup.txt existuje, premaže sa týmto volaním jeho obsah; v prípade potreby zapisovať na koniec existujúceho súboru možno použiť PrintStream out = new PrintStream(new FileOutputStream("vystup.txt", true)), kde o pridávaní na koniec súboru hovorí booleovský parameter true (viac v dokumentácii).
  • Adresár, od ktorého sa počíta takáto relatívna adresa súboru, závisí od prostredia. Pri spúšťaní programu z príkazového riadku sa súbor vystup.txt vytvorí v adresári, z ktorého bol spustený interpreter java. Pri práci s IntelliJ ide pri východzích nastaveniach o koreňový adresár projektu (obsahujúci podadresáre ako src a out).
  • Vytvorenie inštancie triedy PrintStream môže spôsobiť výnimku typu IOException, ktorú je nutné ošetriť. Spracovaním výnimiek sa na tomto predmete budeme zaoberať až neskôr – zatiaľ teda zvolíme najjednoduchšie riešenie, pri ktorom iba do hlavičky volajúcej metódy (v príklade nižšie ide o metódu main) pridáme upozornenie, že v nej môže vzniknúť neošetrená výnimka typu IOException (v prípade, že sme neimportovali celý balík java.io, je z neho potrebné importovať triedu java.io.IOException).
public static void main(String[] args) throws IOException {
    PrintStream out = new PrintStream("vystup.txt");
    // ...
}

Po vytvorení výstupného prúdu môžeme používať jeho metódy na zápis do súboru, ako napríklad nasledujúce.

  • print: zapíše do súboru svoj argument (prakticky ľubovoľného typu).
  • println: to isté, len s novým riadkom na konci (pri volaní bez argumentov vypíše iba znak pre nový riadok).
  • format alebo printf: zapíše text podľa formátovacieho reťazca podobného ako v C (viac tu).
  • close: metóda bez argumentu, ktorá zavrie otvorený výstupný prúd a je potrebné ju zavolať akonáhle so súborom prestaneme pracovať.

Kompletný program zapisujúci do súboru vystup.txt tak môže vyzerať napríklad nasledovne.

import java.io.*; 

public class Trieda {

    public static void main(String[] args) throws IOException {
        PrintStream out = new PrintStream("subor.txt");
        out.print("Nejaky text");
        out.println();
        out.println("Nejaky iny text");
        out.close();
    }
}

Špeciálnym výstupným prúdom typu PrintStream je aj štandardný výstupný prúd System.out. Pri práci s ním tak možno používať rovnaké metódy ako vyššie. Nie je pritom potrebné importovať triedu PrintStream (pretože pracujeme iba s jej inštanciou System.out a trieda System sa importuje automaticky), ani špecifikovať throws IOException (pretože nevoláme konštruktor triedy PrintStream, ktorý túto výnimku môže spôsobiť).

public class Trieda {

    public static void main(String[] args) {
        System.out.print("Nejaky text");
        System.out.println();
        System.out.println("Nejaky iny text");
    }
}

Vstupné prúdy

Základná trieda pre vstupné prúdy je v Jave InputStream. Tohto typu je aj štandardný vstupný prúd System.in pre čítanie z konzoly. Vstupné prúdy pre čítanie zo súboru sú reprezentované triedou FileInputStream; pre účely tejto prednášky možno FileInputStream považovať za špeciálny prípad InputStream. Obe tieto triedy sú definované v balíku java.io.

Používanie týchto vstupných prúdov však nie je veľmi pohodlné, pretože umožňujú iba čítanie po bytoch. Preto sa zvyknú používať nadstavbové triedy, ktoré tieto jednoduché vstupné prúdy „obalia” a programátorovi poskytnú aj pokročilejšie funkcie na prácu so vstupom. V nasledujúcom preskúmame dve z takýchto nadstavbových tried: Scanner a BufferedReader.

Trieda Scanner

Trieda Scanner – definovaná v balíku java.util, z ktorého je nutné túto triedu importovať – umožňuje rozkladať vstupný prúd na reťazce oddelené bielymi znakmi, pričom kompatibilné reťazce dokáže konvertovať aj na číselné typy. Alternatívne možno Scanner použiť aj na čítanie vstupu po riadkoch.

Scanner možno vytvoriť na základe vstupného prúdu typu InputStream, ktorým môže byť napríklad štandardný vstupný prúd System.in pre čítanie z konzoly, alebo inštancia triedy FileInputStream pre čítanie z textového súboru; Scanner čítajúci zo vstupného súboru možno vytvoriť aj priamo na základe inštancie triedy File reprezentujúcej cestu k súboru. Triedy FileInputStream aj File sú definované v balíku java.io, z ktorých ich je potrebné importovať. Pri obidvoch spôsoboch vytvárania Scanner-u pre textový súbor môže vzniknúť výnimka typu IOException, ktorú je potrebné ošetriť (napríklad cez throws IOException v hlavičke volajúcej metódy). V prípade čítania z textového súboru je tiež potrebné na konci Scanner zavrieť jeho metódou close.

import java.util.*; // Kvoli triede Scanner

public class Trieda {
    
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // ...
    } 
}
import java.io.*;   // Kvoli triede FileInputStream
import java.util.*; // Kvoli triede Scanner

public class Trieda {
    
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(new FileInputStream("vstup.txt"));
        // ...
        scanner.close(); 
    }
}
import java.io.*;   // Kvoli triede File
import java.util.*; // Kvoli triede Scanner

public class Trieda {
    
    public static void main(String[] args) throws IOException {
        Scanner scanner = new Scanner(new File("vstup.txt"));
        // ...
        scanner.close();
    }
}

Inak je použitie Scanner-a prakticky totožné, nech už čítame z konzoly alebo zo súboru. Kompletný zoznam metód poskytovaných touto triedou možno nájsť v jej dokumentácii. Tu spomeňme aspoň nasledujúce metódy:

  • next: vráti nasledujúci reťazec oddelený od zvyšku vstupu bielymi znakmi; ak žiaden neexistuje, vyhodí výnimku (pri čítaní z konzoly sa to môže stať len pri manuálnom zadaní znaku konca súboru; v IntelliJ sa koniec súboru zadáva ako Ctrl+D bez ohľadu na operačný systém, t.j. aj pod Windowsom, avšak časť vstupu „nepotvrdená” klávesou Enter sa v takom prípade odignoruje).
  • hasNext: zistí, či na vstupe zostáva neprečítaný nebiely reťazec, ktorý by mohla vrátiť funkcia next (pri čítaní z konzoly sa samozrejme vždy čaká na ďalší vstup zadaný používateľom).
  • nextInt: prečíta nasledujúci reťazec oddelený od zvyšku vstupu bielymi znakmi, pričom ale predpokladá, že ide o celé číslo a na výstup vráti tento reťazec už skonvertovaný na číslo; ak nasledujúci reťazec nebielych znakov neexistuje, alebo nie je korektnou reprezentáciou celého čísla, vyhodí výnimku.
  • hasNextInt: zistí, či na vstupe nasleduje reťazec interpretovateľný ako celé číslo, ktorý by mohla prečítať funkcia nextInt.
  • Podobne napríklad nextLong, nextDouble, hasNextLong, hasNextDouble, atď.
  • nextLine: prečíta riadok až po jeho koniec a výsledný reťazec (bez prípadného \n na konci) vráti na výstupe.
  • hasNextLine: zistí, či na vstupe nasleduje ďalší riadok.

Príklad 1: nasledujúci program prečíta z konzoly prirodzené číslo n a za ním n reťazcov oddelených bielymi znakmi, ktoré uloží do pola a. Následne na konzolu vypíše tieto reťazce, každý na osobitnom riadku, v opačnom poradí ako na vstupe.

import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        String[] a = new String[n];
        for (int i = 0; i <= n - 1; i++) {
            a[i] = scanner.next();
        }
        for (int i = n - 1; i >= 0; i--) {
            System.out.println(a[i]);
        }
    }
}

Príklad 2: nasledujúci program číta textový súbor vstup.txt obsahujúci niekoľko čísel oddelených bielymi znakmi. Po prečítaní celého vstupného súboru vypíše súčet týchto čísel na konzolu.

import java.io.*;
import java.util.*;

public class Trieda {

    public static void main(String[] args) throws IOException {
        int sucet = 0;
        Scanner scanner = new Scanner(new File("vstup.txt"));
        while (scanner.hasNextInt()) {
            sucet += scanner.nextInt();
        }
        scanner.close();
        System.out.println(sucet);
    }
}

Trieda BufferedReader

Trieda BufferedReader – definovaná v balíku java.io – umožňuje čítať vstup po znakoch a po riadkoch. Jej metódy spravidla vyhadzujú výnimky typ IOException, ktoré je nutné ošetriť (aspoň cez throws IOException v hlavičke volajúcej funkcie).

  • Vytvorenie inštancie triedy BufferedReader pre čítanie z konzoly:
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
// ...
  • Vytvorenie inštancie triedy BufferedReader pre čítanie zo súboru:
BufferedReader in = new BufferedReader(new FileReader("vstup.txt"));
// ...
in.close();

Trieda BufferedReader pritom poskytuje dve kľúčové metódy:

  • read: prečíta jeden znak zo vstupu a vráti ho na výstupe (v prípade konca súboru vráti -1); jej návratový typ je int.
  • readLine: prečíta jeden riadok zo vstupu; jej návratový typ je String (reťazec bez prípadného \n na konci riadku).

Príklad 1: nasledujúci program prečíta z konzoly jeden riadok a následne na konzolu vypíše ten istý riadok, pričom ale malé písmená abecedy prevedie na veľké.

import java.io.*;

public class Trieda {

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String s = in.readLine();
        System.out.println(s.toUpperCase());
    }
}

Príklad 2: nasledujúci program prečíta textový súbor vstup.txt po znakoch a jeho obsah vypíše na konzolu.

import java.io.*;

public class Trieda {

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader("vstup.txt"));
        int c;
        while ((c = in.read()) != -1) {
            System.out.print((char) c);
        }
        in.close();
    }
}

Ďalšie užitočné štandardné triedy

  • Trieda Math obsahuje množštvo statických metód realizujúcich matematické funkcie a operácie, napríklad:
public class Trieda {

    public static void main(String[] args) {
        System.out.println(Math.pow(2, 0.5));
        System.out.println(Math.cos(Math.PI));
        System.out.println(Math.log(Math.E));
        System.out.println(Math.max(1, 2));
        System.out.println(Math.abs(-1));
        System.out.println(Math.toDegrees(Math.acos(Math.sqrt(3) / 2)));
        // ..
    }
}
  • Trieda Random reprezentuje generátor náhodných čísel. Je definovaná v balíku java.util. Napríklad nasledujúci program simuluje 10 hodov vyváženou hracou kockou.
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Random random = new Random();
        for (int i = 1; i <= 10; i++) {
            System.out.println(random.nextInt(6) + 1); // vygeneruj nahodne prirodzene cislo MENSIE ako 6 a zvys ho o 1
        }
    }
}
  • Trieda Arrays z balíka java.util obsahuje statické metódy na prácu s poľami; napr. equals na porovnávanie polí (a nie iba referencií), copyOf na kopírovanie polí (a nie iba referencií), sort na utriedenie poľa, binarySearch na binárne vyhľadávanie, atď.

Letný semester, cvičenia č. 1

  • Úvod cvičenia: dokončenie prvej prednášky. Videozáznam bude k dispozícii v MS Teams.
  • Na testovači zverejnených päť nebodovaných cvičení a jedna bonusová úloha s odovzdaním do stredy 24. februára, 11:30.

Letný semester, pravidlá

Na úspešné absolvovanie predmetu je potrebné:

  • Získať za semester a praktickú časť skúšky dohromady aspoň 50 bodov z plného počtu 100 bodov.
  • Uspieť na skúške.

Body budú prideľované podľa nasledujúceho kľúča:

  • Najviac 35 bodov za praktické testy na cvičeniach (5 testov po 7 bodov).
  • Najviac 35 bodov za domáce úlohy (5 úloh po 7 bodov).
  • Najviac 30 bodov za praktickú časť skúšky.
  • Kladný počet bodov za bonusové úlohy.

Všetky zadania na tomto predmete musia byť riešené individuálne.

Testy na cvičeniach

Počas piatich spomedzi stredajších cvičení bude prebiehať praktický test, za ktorý bude možné získať najviac 7 bodov.

  • Test bude pozostávať z niekoľkých (typicky troch) programátorských úloh s odovzdávaním riešení na testovač.
  • (Podobný princíp ako rozcvičky v zimnom semestri, avšak bude potrebné vyriešiť viac ako jednu úlohu a pomoc od vyučujúcich bude limitovaná na otázky konkrétneho charakteru.)
  • Termíny testov: 10. marca, 24. marca, 7. apríla, 14. apríla, 28. apríla.
  • Na každom z testov sa môžu objaviť úlohy k ľubovoľnej v tom čase už prebranej látke (upresní sa neskôr).
  • Na získanie bodov z testu je potrebná účasť na cvičeniach v danom týždni. To v online forme znamená účasť na príslušnom stretnutí v MS Teams počas práce na riešení testu.

Koncom semestra budú počas stredajších cvičení dva náhradné termíny testov:

  • Ide predovšetkým o náhradu za prípadné neabsolvované testy, ale možno ich využiť aj na opravu absolvovaného testu.
  • Termíny náhradných testov: 5. mája, 12. mája (študenti BIN môžu druhý náhradný test namiesto 12. mája absolvovať počas prvého riadneho skúškového termínu).
  • Maximálne teda možno riešiť sedem testov, pričom v takom prípade sa do hodnotenia započíta päť najlepších výsledkov. Je ale potrebné využiť výhradne riadne a náhradné termíny uvedené vyššie.
  • Na náhradných testoch sa môžu objaviť úlohy k ľubovoľnej v tom čase už prebranej látke. Nepôjde teda o náhradu konkrétneho z piatich riadnych testov.

V prípade odôvodnenej a ospravedlnenej neúčasti na viac ako dvoch testoch bude mechanizmus ich náhrady stanovený individuálne.

Domáce úlohy

Počas semestra bude zverejnených päť bežných domácich úloh, pričom za každú bude možné získať najviac 7 bodov.

  • Riešenia sa budú odovzdávať na testovač.
  • Termín odovzdania bude vždy aspoň 10 dní po zverejnení úlohy. Prvá domáca úloha bude zverejnená v pondelok 1. marca.

Okrem toho bude počas semestra zverejnených niekoľko bonusových úloh (za rôzne počty bodov a s rôznymi termínmi odovzdania).

Skúška

Skúška bude pozostávať z:

  • Praktickej časti spočívajúcej v riešení programátorských úloh s odovzdávaním na testovač. Za praktickú časť skúšky bude možné získať najviac 30 bodov do celkového hodnotenia.
  • Ústnej časti spočívajúcej v diskusii o praktickej časti skúšky a o teórii z prednášok.

Na úspešné absolvovanie skúšky je potrebné:

  • Získať aspoň 15 bodov z praktickej časti.
  • Uspieť na ústnej časti skúšky.

Prípadné opakovanie skúšok sa riadi študijným poriadkom. Body z praktickej časti skúšky sa do prípadných ďalších termínov neprenášajú (v prípade opakovania skúšky je teda vždy nutné opakovať aj praktickú časť).

Výsledná známka z predmetu

Po úspešnom absolvovaní (oboch častí) skúšky sa výsledná známka z predmetu za daný skúškový termín stanoví, na základe celkového počtu bodov získaných za semester a praktickú časť skúšky, podľa nasledujúceho kľúča:

90 bodov a viac A
aspoň 80 a menej ako 90 bodov B
aspoň 70 a menej ako 80 bodov C
aspoň 60 a menej ako 70 bodov D
aspoň 50 a menej ako 60 bodov E
menej ako 50 bodov FX

V ojedinelých prípadoch môže byť táto známka na základe ústnej časti skúšky pozmenená o jeden stupeň. V prípade neúspechu na skúške získava študent v rámci daného skúškového termínu hodnotenie FX.

Režim pre pokročilých

Počas druhých cvičení, t.j. v stredu 24. februára o 11:30, sa bude konať praktický test pre pokročilých.

  • V prípade záujmu je potrebné sa na test aspoň 24 hodín vopred prihlásiť cez AIS.
  • Forma rovnaká ako pri piatich bežných testoch.
  • Úlohy môžu byť zamerané na ľubovoľnú tému zo syláb predmetu.
  • Každý, kto z testu získa aspoň 80%, bude mať možnosť predmet absolvovať v režime pre pokročilých.

Študenti v režime pre pokročilých:

  • Počas semestra nemusia získať žiadne body a nemusia sa zúčastniť ani na praktickej časti skúšky.
  • Namiesto toho počas semestra vypracujú softvérový projekt, ktorého prezentácia bude súčasťou ústnej skúšky.
    • Projekt musí byť napísaný v jazyku Java s použitím platformy JavaFX.
    • Rozsah a náročnosť projektu musia byť primerané tomu, že pôjde o celosemestrálnu prácu.
    • Očakáva sa tiež istá úroveň programátorského štýlu a objektovo orientovaného návrhu, rozumná miera prehľadnosti kódu a dodržiavanie základných konvencií jazyka Java.
  • Najneskôr do začiatku cvičení 3. marca prostredníctvom testovača odovzdajú krátky neformálny návrh projektu a najneskôr do začiatku cvičení 10. marca sa na definitívnej téme projektu dohodnú s prednášajúcim. V opačnom prípade zvyšok semestra absolvujú v štandardnom režime.
  • Zdrojový kód projektu odovzdajú najneskôr 48 hodín pred začiatkom skúšky prostredníctvom testovača a projekt v rámci ústnej skúšky predvedú na svojom počítači.

Letný semester, softvér

Pre účely tohto predmetu budeme potrebovať nasledujúci softvér:

  • Java SE Development Kit 15 (stačí ľubovoľná verzia od 11 vyššie); pod Linuxom možno použiť aj balíky openjdk.
  • Platformu JavaFX 15 SDK (na stránke ako „Latest Release”, ale opäť by mala stačiť aj verzia 11) pre vývoj aplikácií s grafickým používateľským rozhraním.
  • Vývojové prostredie IntelliJ IDEA Community Edition 3.2, prípadne vhodnú alternatívu ako napríklad Eclipse alebo Visual Studio Code.

Odporúčame pracovať vo vývojom prostredí, avšak je dôležité naučiť sa kompilovať a spúšťať programy aj z príkazového riadku (minimálne za účelom riešenia rôznych problémov).

Inštalácia softvéru

  • Stiahnite si Java SE Development Kit 15 odtiaľto a postupujte podľa inštalačných pokynov.
  • Zistite, či inštalácia správne pridala adresár obsahujúci javovský kompilátor a interpreter do systémovej premennej PATH. V prípade úspechu by napríklad nasledujúce príkazy zadané do príkazového riadku mali vypísať verziu javovského kompilátora resp. interpretra zhodnú s nainštalovanou verziou Javy.
javac --version
java --version
  • V prípade, že tieto príkazy nefungujú, alebo sa vypísala stará verzia Javy, pridajte adresár obsahujúci programy javac a java do systémovej premennej PATH manuálne (a prípadný adresár starej verzie Javy odtiaľ odoberte).
  • Stiahnite si ZIP archív s JavaFX odtiaľto (pre najnovšiu verziu Javy odporúčame „Latest Release”, čiže verziu 15) a rozbaľte ho do vhodného priečinka. Žiadne ďalšie úkony nie sú v rámci inštalácie JavaFX nutné, ale zapamätajte si adresu podadresára lib, keďže ju neskôr budete potrebovať. Nižšie v tomto texte budeme na túto cestu odkazovať ako na <cesta k lib>.
  • Stiahnite si IntelliJ IDEA Community Edition 3.2 odtiaľto a postupujte podľa inštalačných pokynov.
  • IntelliJ by mal automaticky rozoznať nainštalovanú verziu Javy. V prípade, že sa tak neudeje, pridajte ju manuálne podľa pokynov tu.

Skompilovanie a spustenie programu v Jave

Tento oddiel opisuje proces skompilovania a spustenia javovskej triedy Hello vypisujúcej na konzolu text Hello, World! v dvoch variantoch:

  • V prvom je trieda Hello súčasťou nepomenovaného balíka.
  • V druhom je trieda Hello súčasťou balíka somepackage.

Z príkazového riadku

Uvažujme najprv prípad, keď je trieda Hello súčasťou nepomenovaného balíka, t.j. jej zdrojový kód vyzerá nasledovne.

public class Hello {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }

}
  • Trieda Hello musí byť uložená v súbore Hello.java (názov súboru bez prípony .java sa musí zhodovať s názvom triedy).

Postup skompilovania a spustenia triedy Hello z príkazového riadku je potom nasledovný:

  1. V príkazovom riadku sa nastavíme do priečinka obsahujúceho súbor Hello.java.
  2. Triedu skompilujeme príkazom javac Hello.java, v dôsledku čoho by mal v rovnakom priečinku vzniknúť súbor Hello.class spustiteľný na JVM.
  3. Triedu Hello spustíme príkazom java Hello (prípadné argumenty môžeme triede Hello zadávať obvyklým spôsobom).
Pozor: kým pri volaní kompilátora javac je potrebné uviesť aj príponu kompilovaného súboru .java, pri volaní interpretra java príponu spúšťanej triedy .class nepíšeme (kým argumentom kompilátora je bežná cesta k súboru, argumentom interpretra je tzv. classpath danej triedy).

Ďalej uvažujme prípad, keď je trieda Hello súčasťou balíka somepackage a jej kód tak vyzerá nasledovne.

package somepackage;

public class Hello {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }

}
  • V tomto prípade by mala byť trieda Hello opäť uložená v súbore Hello.java, ktorý by mal navyše byť uložený v priečinku somepackage.

Skompilovanie a spustenie triedy Hello – alebo presnejšie somepackage.Hello – z príkazového riadku realizujeme nasledovne:

  1. Nastavíme sa do priečinka obsahujúceho priečinok somepackage.
  2. Triedu skompilujeme príkazom javac somepackage/Hello.java (s príponou).
  3. Triedu spustíme príkazom java somepackage.Hello (ide o tzv. classpath triedy Hello).

V IntelliJ IDEA

  1. Vytvoríme nový projekt pomocou voľby New Project. V prvých dvoch krokoch použijeme východzie nastavenia (jazyk Java, bez použitia šablóny ani ďalších voliteľných možností), v treťom kroku zadáme ľubovoľný názov projektu.
  2. Ak sa nezobrazuje, zobrazíme panel so štruktúrou projektu cez View --> Tool Windows --> Project (alebo s použitím klávesovej skratky Alt + 1).
  3. Rozbalíme modul pomenovaný rovnako ako projekt (tučným) a zvolíme priečinok src.
  4. V ponuke File --> New (prípadne v podponuke New kontextovej ponuky, ktorá sa zobrazí po kliknutí na src pravou myšou) by sa teraz mala objavovať možnosť Java Class.
  5. Po zvolení tejto možnosti zadáme názov triedy (presnejšie celú classpath vrátane balíkov), t.j. Hello pre triedu v nepomenovanom balíku a somepackage.Hello pre triedu v balíku somepackage.
  6. V závislosti od vybranej alternatívy skopírujeme do editora vhodnú variantu triedy Hello z predchádzajúceho oddielu.
  7. Triedu skompilujeme a spustíme napríklad kliknutím na zelenú šípku vedľa hlavičky metódy main a zvolením možnosti Run 'trieda.main()'. Následné ďalšie spustenia možno realizovať skratkou Shift + F10.

Dôvod, prečo skratka Shift + F10 (ekvivalentná možnosti Run --> Run 'Hello') nefunguje hneď od začiatku je ten, že sme zatiaľ nevytvorili tzv. konfiguráciu behu, v ktorej sa napríklad zadáva verzia Javy použitá pri kompilovaní, hlavná trieda projektu (t.j. trieda, ktorá sa má spustiť po spustení projektu), prípadné argumenty z príkazového riadku, parametre pre kompilátor a interpreter, atď. V prípade kliknutia na zelenú šípku vedľa metódy main a zvolenia príslušnej možnosti sa automaticky vytvorí tzv. dočasná konfigurácia, pri ktorej sa spustí daná trieda s východzími nastaveniami.

Pre neskoršie účely je dôležité vedieť vytvárať aj vlastné (a permanentné) konfigurácie behu (v jednom projekte možno definovať aj viacero konfigurácií, medzi ktorými sa potom možno prepínať pomocou prepínača v pravej časti nástrojovej lišty). Postup vytvorenia novej konfigurácie behu je nasledujúci:

  1. Zvolíme Run --> Edit Configurations....
  2. Klikneme na tlačidlo + a zvolíme možnosť Application.
  3. Následne môžeme v hlavnej časti okna napravo zadať názov konfigurácie, napríklad Konfiguracia.
  4. Zadáme potrebné údaje: predvolená by mala byť správna verzia Javy (napr. java 15), do políčka Main class zadáme názov hlavnej triedy projektu (napr. Hello), do políčka Program arguments môžeme zadať prípadné argumenty pre spúšťanú triedu z príkazového riadku (políčko môžeme ponechať prázdne). Prípadné ďalšie možnosti môžeme pridať kliknutím na Modify options.
  5. Ukončíme dialóg tlačidlom OK.
  6. Na nástrojovej lište sa uistíme, že je vybraná práve vytvorená konfigurácia a projekt spustíme skratkou Shift + F10.

Viac sa o konfiguráciách behu možno dočítať tu.

Skompilovanie a spustenie programu v JavaFX

Ukážeme si teraz, ako z príkazového riadku a v IntelliJ IDEA skompilovať a spustiť nasledujúcu triedu pracujúcu s JavaFX, ktorá by mala zobraziť okno s titulkom Hello, World!. Táto trieda musí byť uložená v súbore HelloFX.java.

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;

public class HelloFX 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);
    }
}

V nasledujúcom <cesta k lib> odkazuje na cestu k adresáru lib rozbaleného ZIP archívu JavaFX, o ktorej sme sa už zmienili vyššie. Viac detailov možno nájsť tu.

Z príkazového riadku

  1. Nastavíme sa do priečinku obsahujúceho súbor HelloFX.java.
  2. Triedu skompilujeme príkazom
javac --module-path <cesta-k-lib> --add-modules javafx.controls,javafx.fxml HelloFX.java
  1. Triedu spustíme príkazom
java --module-path <cesta-k-lib> --add-modules javafx.controls,javafx.fxml HelloFX

V IntelliJ IDEA

  1. Vytvoríme projekt a v ňom triedu HelloFX rovnakým spôsobom ako vyššie (dá sa prípadne vytvoriť aj projekt typu Java FX).
  2. Otvoríme dialóg File --> Project Structure.... V ňom otvoríme záložku Libraries, klikneme na tlačidlo + a zvolíme možnosť Java. Otvorí sa dialógové okno umožňujúce vytvoriť knižnicu z priečinka. Za tento priečinok vyberieme priečinok <cesta k lib>.
  3. Vytvoríme konfiguráciu behu podobným spôsobom ako vyššie. Za hlavnú triedu vyberieme HelloFX.
  4. Klikneme na Modify options a zaškrtneme možnosť Add VM options.
  5. Následne by sa malo objaviť políčko VM options, do ktorého zadáme
--module-path <cesta-k-lib> --add-modules javafx.controls,javafx.fxml
  1. Dialóg potvrdíme kliknutím na OK.
  2. Projekt spustíme klávesovou skratkou Shift + F10.

Letný semester, prednáška č. 2

Oznamy

  • Na test pre pokročilých sa v prípade záujmu treba prihlásiť do utorka 23. februára, 11:30.
  • Prvú bonusovú úlohu treba odovzdať do stredy 24. februára, 11:30.

Základné koncepty objektovo orientovaného programovania

Objekty a triedy

Dvoma najzákladnejšími konceptmi objektovo orientovaného programovania (OOP) sú triedy a objekty.

  • Trieda (angl. class) je typ, ktorý podobne ako struct v C/C++ môže združovať údaje rôznych typov. Okrem toho ale obvykle obsahuje aj definície metód na manipuláciu s týmito údajmi.
  • Objekt (angl. object) je inštancia triedy – obsahuje teda už nejakú konkrétnu sadu údajov vyhovujúcu definícii triedy, na ktorú možno aplikovať metódy triedy.
  • Triedu teda možno chápať ako „vzor”, podľa ktorého sa vytvárajú objekty.

Príklad: nasledujúca trieda Fraction reprezentuje zlomky. Obsahuje dve premenné numerator a denominator zodpovedajúce čitateľu a menovateľu zlomku a metódu na vyhodnotenie zlomku.

public class Fraction {
    int numerator;
    int denominator;

    double evaluate() {
        return (double) numerator / denominator;
    }
}

Inštanciami tejto triedy, t.j. objektmi typu Fraction, sú konkrétne realizácie triedy Fraction (napr. zlomok s čitateľom 2 a menovateľom 3). O spôsobe ich vytvorenia si povieme o chvíľu. Avšak v prípade, že už máme nejakú inštanciu fraction triedy Fraction vytvorenú, môžeme hodnotu zlomku vypísať napríklad nasledovne:

Fraction fraction;

// Sem pride vytvorenie instancie triedy Fraction a jej priradenie do premennej fraction.

System.out.println(fraction.evaluate());

Príklad: časť triedy reprezentujúcej zásobník implementovaný pomocou poľa (čo je v Jave značne suboptimálne riešenie) by mohla vyzerať napríklad nasledovne.

public class MyStack {
    int data[];
    int count;

    int pop() {
       count--;
       return data[count];
    }

    // Dalsie metody (napr. push) ...
}

Ak si opäť odmyslíme vytvorenie samotného zásobníka, môžeme so zásobníkom typu MyStack pracovať napríklad takto:

MyStack stack;

// Sem pride vytvorenie zasobnika a napriklad niekolko prikazov push.

int x = s.pop();

Neskôr uvidíme, že medzi štandardnými triedami jazyka Java možno nájsť aj množstvo dátových štruktúr a medzi nimi aj triedu Stack pre zásobníky. Príklad vyššie je teda iba ilustračný a tvorbe tried podobného druhu je vo všeobecnosti lepšie sa vyvarovať.

Príklad: v Jave sú inštancie všetkých typov okrem primitívnych objektmi a samotné tieto typy sú často triedami. S výnimkou veľmi špecifického prípadu polí (o ktorom si viac povieme neskôr) viac-menej pôjde o triedy a objekty v podobe, v akej si ich predstavíme na tejto prednáške.

Referencie na objekty

Premenná, ktorej typom je trieda, obsahuje referenciu na objekt, ktorý je inštanciou tejto triedy.

  • Podľa toho sa teda správajú operátory = a ==.
  • K premenným a metódam objektu, na ktorý príslušná referencia ukazuje, pristupujeme pomocou operátora . a píšeme napríklad fraction.numerator alebo fraction.evaluate().
Fraction fraction1, fraction2;

// ...

fraction1 = fraction2;    // Obidve premenne ukazuju na to iste miesto v pamati.
fraction1.numerator = 3;  // Zmeni sa aj hodnota fraction2.numerator.
  • Do premennej, ktorej typom je trieda, možno priradiť hodnotu null – v takom prípade ide o referenciu, ktorá neukazuje na žiaden objekt.

Konštruktory a inicializácia objektov

Často je potrebné súčasne s vytvorením objektu vykonať rôzne inicializačné úkony – napríklad pri zásobníku typu MyStack alokovať pole, pri zlomkoch typu Fraction inicializovať premenné na vhodné hodnoty, a pod. Na takúto inicializáciu objektov v Jave slúžia špeciálne kusy kódu podobné metódam – takzvané konštruktory. Volanie konštruktorov je neodmysliteľne späté s vytváraním objektov.

  • Názov konštruktora je vždy rovnaký ako názov triedy, ku ktorej patrí (na rozdiel od bežných metód teda ich názov podľa konvencie začína veľkým písmenom).
  • Do hlavičky konštruktora sa nepíše návratový typ (konštruktor žiaden nemá; nepíše sa ale ani void). V opačnom prípade pôjde o bežnú metódu (nie o konštruktor), čo môže viesť k pomerne nepríjemným chybám.
  • Prípadné argumenty sa zapisujú rovnako ako pri bežných metódach.
  • Pre jednu triedu možno definovať aj viacero konštruktorov, ktoré sa však musia líšiť postupnosťou typov argumentov (aby bolo pri volaní jasné, o ktorý z konštruktorov ide).

Príklad: pre triedu Fraction môžeme napísať napríklad nasledujúce dva konštruktory.

public Fraction() {
    numerator = 0;
    denominator = 1;
}

public Fraction(int num, int denom) { // Neskor uvidime, ze nie je nutne pre argumenty konstruktora a premenne instancie volit ine nazvy
    numerator = num;
    denominator = denom;
}
  • Ak pre triedu nedefinujeme žiaden konštruktor, automaticky sa vytvorí konštruktor bez parametrov, ktorý v princípe sám o sebe neurobí nič, ale je možné ho zavolať (bez čoho objekt nevytvoríme).

Samotné vytvorenie inštancie triedy sa realizuje pomocou operátora new, za ktorým nasleduje volanie niektorého konštruktora.

Fraction f1 = new Fraction();
Fraction f2;
f2 = new Fraction(2, 3);

System.out.println(f1.evaluate());
System.out.println(f2.evaluate());
  • Operátor new dynamicky alokuje pamäť pre objekt, zavolá príslušný konštruktor a vráti referenciu na vytvorený objekt.
  • Nie je potrebné starať sa o neskoršie odalokovanie pamäte – túto úlohu v JVM vykonáva tzv. garbage collector.

Na rozdiel od lokálnych premenných sú premenné inštancií alokované automaticky, a to na hodnoty 0, false, alebo null v závislosti od typu premennej. Prípadne je možné niektoré premenné inštancií inicializovať aj explicitne na odlišné hodnoty:

public class Fraction {
    int numerator;        // Inicializuje sa na nulu.
    int denominator = 1;

    // ...
}

Alternatívne možno premenné inicializovať v rámci konštruktora, rovnako ako v jednom z vyššie uvedených príkladov. Pri vytváraní inštancie triedy pomocou operátora new sa jednotlivé procesy vykonajú v nasledujúcom poradí:

  • Najprv sa vykoná automatická alebo explicitná inicializácia premenných (a to aj v prípade, že nebol definovaný žiaden konštruktor triedy a je tak volaný jej východzí konštruktor bez parametrov).
  • Až následne sa spustí volaný konštruktor.

Kľúčové slovo this

V rámci (nestatických) metód a konštruktorov tried možno používať kľúčové slovo this, ktoré sa pre každú inštanciu tejto triedy interpretuje ako referencia na seba, t.j. na objekt, na ktorom bola metóda obsahujúca toto kľúčové slovo volaná. Kľúčové slovo this sa používa predovšetkým nasledujúcimi troma spôsobmi:

  • Pre ľubovoľnú premennú premenna alebo metódu metoda inštancie triedy možno na prístup k tejto premennej resp. metóde použiť zápis this.premenna resp. this.metoda. Často sú teda tieto zápisy ekvivalentné kratším zápisom premenna resp. metoda. Niekedy sa však môže stať, že sa niektorá premenná inštancie prekryje napríklad argumentom alebo lokálnou premennou s rovnakým názvom. V takom prípade možno k premennej inštancie pristúpiť iba prostredníctvom this.premenna. Naša trieda Fraction by teda napríklad mohla vyzerať aj takto:
public class Fraction {
    int numerator;
    int denominator = 1;

    public Fraction() {

    }

    public Fraction(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public double evaluate() {
        return (double) numerator / denominator;
    }
}
  • Kľúčové slovo this možno využiť aj na predanie danej inštancie ako argument nejakej metódy.
public class Fraction {
    // ... 

    @Override                      // Tuto znacku si zatial nevsimajme, kod by fungoval aj bez nej.
    public String toString() {
        return numerator + " / " + denominator;
    }

    public void print() {
        System.out.println(this);  // Rovnake spravanie ako s argumentom toString() resp. this.toString().
    }
}
  • Na prvom riadku konštruktora možno pomocou this( ... ) zavolať iný konštruktor tej istej triedy. Často napríklad potrebujeme spraviť niektoré úkony pri inicializácii objektu vždy, t.j. v ľubovoľnom konštruktore, zatiaľ čo iné sú žiadúce iba v niektorých konštruktoroch. V takom prípade by z návrhového hľadiska nebolo rozumné opakovať spoločné časti kódu v každom z konštruktorov. (Čo sa stane v prípade, keď v tomto kóde bude treba niečo zmeniť?) Namiesto toho je vhodnejšie v „pokročilejších” konštruktoroch zavolať nejaký „menej pokročilý” konštruktor (ale iba na prvom riadku).
public class Circle {
    double centerX = 0; // Netreba explicitnu inicializaciu, ale moze to byt prehladnejsie
    double centerY = 0; // Netreba explicitnu inicializaciu, ale moze to byt prehladnejsie
    double radius = 1;
    
    public Circle() {
        System.out.println("Vytvaram kruh.");
    }   
    
    public Circle(double centerX, double centerY) {
        this();                  
        this.centerX = centerX;
        this.centerY = centerY;
    }
    
    public Circle(double centerX, double centerY, double radius) {
        this(centerX, centerY);
        this.radius = radius;
    }
}

Občas ale môže byť užitočné použiť kľúčové slovo this aj iným spôsobom, napríklad vrátiť this na výstupe.

Modifikátory prístupu

Premenným a metódam, ako aj triedam samotným, možno v Jave nastavovať tzv. modifikátory prístupu určujúce viditeľnosť týchto súčastí z iných tried. Modfikátory prístupu sú v Jave štyri:

  • private: k premennej alebo metóde možno pristúpiť iba v rámci jej triedy; na triedy sa tento modifikátor použiť nedá.
  • (žiadny modifikátor): premenná, metóda, alebo trieda je viditeľná len v rámci jej balíka.
  • protected: podobné ako v predchádzajúcom prípade; rozdiel uvidíme na budúcej prednáške.
  • public: premennú, metódu, alebo triedu možno použiť z ľubovoľnej triedy.

Každá trieda Trieda s modifikátorom public musí byť uložená v zdrojovom súbore, ktorého názov musí byť Trieda.java. Názvy tried s inými modifikátormi prístupu sa nemusia zhodovať s názvom súboru a jeden zdrojový súbor môže obsahovať aj viacero tried (najviac jedna z nich však môže byť public, pričom v takom prípade sa názov tejto triedy musí zhodovať s názvom súboru). Za dobrú prax sa ale považuje pre každú triedu vytvoriť samostatný súbor, ktorého názov sa zhoduje s názvom triedy.

Ako sme už spomenuli na minulej prednáške, modifikátor prístupu statickej metódy main musí byť vždy public.

Zapuzdrenie

Jedným z hlavných metodických princípov objektovo orientovaného programovania je zapuzdrenie (angl. encapsulation). Ide o „zabalenie” dát a metód na manipuláciu s nimi do spoločného „puzdra” – inštancie nejakej triedy.

  • Kód z iných tried by mal s dátami „zabalenými” v objekte manipulovať iba pomocou jeho metód na to určených.
  • To sa obvykle zabezpečí tak, že sa modifikátor public priradí iba tým metódam, ktoré sú určené na použitie „zvonka”. Premenným a pomocným metódam sa priradí iný modifikátor, napríklad private.
  • Verejné metódy tak tvoria akúsi „sadu nástrojov”, ktorú trieda poskytuje iným triedam na prácu s jej inštanciami. Napríklad trieda pre zásobník by mohla mať (okrem konštruktora) verejné metódy push, pop, isEmpty a peek, pričom jej premenné a prípadné pomocné metódy by boli súkromné.
  • Výhodou tohto prístupu je, že možno zmeniť vnútornú implementáciu triedy bez toho, aby to nejako ovplyvnilo ostatné triedy. Jediné, čo musí zostať zachované, je správanie verejných funkcií (čo zvyčajne ide zariadiť aj pri zmenenej implementácii zvyšku triedy). Napríklad v triede pre zásobník by sme mohli namiesto poľa použiť spájaný zoznam a zvyšné triedy by to nijak neovplyvnilo.
  • Zapuzdrenie tak umožňuje rozdeliť projekt na relatívne nezávislé logické celky s dobre definovaným rozhraním.

Metódy get a set

Premenné inštancií tried sú teda zvyčajne súkromné. Existujú však prípady, keď je opodstatnené k niektorým z nich umožniť prístup aj iným triedam. Napríklad v našom príklade so zlomkami by sa v prípade nemožnosti pristúpiť k čitateľu alebo k menovateľu zlomku podstatne obmedzila funkcionalita triedy Fraction. Obvyklé riešenie takýchto situácií je ponechať samotnú premennú súkromnú, ale poskytnúť verejné metódy na čítanie a zmenu hodnoty tejto premennej. Takéto metódy pre premennú hodnota sa zvyknú konvenčne pomenúvať ako getHodnota a setHodnota. Podstatná časť našej triedy Fraction by tak mohla vyzerať napríklad nasledovne:

public class Fraction {
    private int numerator;
    private int denominator = 1;

    public Fraction() {

    }

    public Fraction(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public int getNumerator() {
        return numerator;
    }

    public void setNumerator(int numerator) {
        this.numerator = numerator;
    }

    public int getDenominator() {
        return denominator;
    }

    public void setDenominator(int denominator) {
        this.denominator = denominator;
    }

    public double evaluate() {
        return (double) numerator / denominator;
    }
}
  • Určite nie je vhodné bezmyšlienkovite vytvárať metódy get a set pre všetky premenné. Opodstatnené to je iba vtedy, keď je daná premenná podstatnou charakteristikou triedy navonok, t.j. pokiaľ prístup k nej môže byť zaujímavý aj v prípade, že sa na samotnú triedu pozeráme ako na „čiernu skrinku”.

Výhody použitia metód get a set oproti použitiu verejných premenných sú napríklad nasledujúce:

  • Môžeme poskytnúť iba metódu get. Tým sa premenná stane „určenou iba na čítanie”. Ak by sme napríklad v našej triede Fraction zmazali metódy setNumerator a setDenominator, dostali by sme triedu reprezentujúcu nemodifikovateľné zlomky (podobne ako napríklad String reprezentuje nemodifikovateľné reťazce).
  • V rámci metódy set možno kontrolovať, či sa do premennej ukladá rozumná hodnota. Napríklad metóda setDenominator vyššie by mohla vyhodiť výnimku (ešte nevieme ako) v prípade, že by sme sa pokúsili nastaviť menovateľ na nulu.
  • Metódy get a set nemusia presne korešpondovať s premennými, a teda môžu ostať zachované aj po zmene vnútornej reprezentácie triedy. Uvažujme napríklad podstatnú časť našej triedy Circle z príkladu vyššie, v ktorej premenné nastavíme na súkromné a pridáme metódy get.
public class Circle {
    private double centerX = 0; 
    private double centerY = 0; 
    private double radius = 1;
    
    public double getCenterX() {
        return centerX;
    }
    
    public void setCenterX(double centerX) {
        this.centerX = centerX;
    }

    public double getCenterY() {
        return centerY;
    }

    public void setCenterY(double centerY) {
        this.centerY = centerY;
    }
    
    public double getRadius() {
        return radius;
    }
    
    public void setRadius(double radius) {
        this.radius = radius;
    }

    // ...
}
Predpokladajme, že sa rozhodneme kruh reprezentovať namiesto jeho stredom a polomerom napríklad jeho stredom a ľubovoľným bodom na jeho hranici (čo je tiež jednoznačná reprezentácia kruhu). V takom prípade určite nie je dobré ponechať aj premennú radius, pretože by sme v celej triede museli zabezpečiť jej konzistenciu s premennými pre bod na hranici kruhu, a to by mohlo byť potenciálnym zdrojom chýb. Keby sme teda polomer ostatným triedam zverejňovali priamo ako premennú, spôsobila by naša malá zmena v implementácii triedy Circle nutnosť zmeny aj vo všetkých triedach s triedou Circle pracujúcich, čo je známkou zlého návrhu. Metódy getRadius a setRadius však ľahko prerobíme tak, aby pracovali zmysluplným spôsobom aj pri novom spôsobe reprezentácie kruhu.
public class Circle {
    private double centerX = 0;
    private double centerY = 0;
    private double boundaryPointX = 1;
    private double boundaryPointY = 0;

    private double distance(double aX, double aY, double bX, double bY) {
        return Math.sqrt((aX - bX) * (aX - bX) + (aY - bY) * (aY - bY));
    }

    public double getRadius() {
        return distance(centerX, centerY, boundaryPointX, boundaryPointY);
    }

    public void setRadius(double radius) {
        double currentRadius = getRadius();
        boundaryPointX = centerX + (boundaryPointX - centerX) * radius / currentRadius;
        boundaryPointY = centerY + (boundaryPointY - centerY) * radius / currentRadius;
    }

    // ...
}
Možnosť takéhoto zmysluplného prerobenia metódy get alebo set je ale do veľkej miery daná tým, že polomer je prirodzeným parametrom kruhu; stále teda platí, že metódy get a set treba implementovať s mierou. Pokiaľ ide o zvyšok triedy, zmena reprezentácie kruhu by si pravdepodobne vyžadovala pridanie nových konštruktorov (hoci možno polomer vypočítať z bodu na hranici kruhu, opačne by to bolo minimálne nejednoznačné; po úprave teda trieda reprezentuje viac informácií a bolo by preto vhodné pridať konštruktor umožňujúci bod na hranici kruhu pri jeho vytvorení zadať). Konštruktor na báze polomeru, opísaný v týchto poznámkach vyššie, však nie je potrebné mazať – jeho nová implementácia by napríklad mohla využívať metódu setRadius.

Preťažovanie metód

Podobne ako môže mať trieda viacero konštruktorov, môže obsahovať aj viacero metód s rovnakým názvom. Podmienkou aj tu je, aby mali metódy s rovnakými názvami rôzne postupnosti typov argumentov (t. j. rôzne signatúry). Takéto vytváranie viacerých metód s rovnakým názvom sa nazýva ich preťažovaním (angl. overloading).

Príklad:

public class Circle {
    
    public void draw() {
        // ...
    }
    
    public void draw(String color) {
        // ...
    }
    
    public void draw(int r, int g, int b) {
        // ...
    }
    
    public void draw(String color, int penWidth) {
        // ...
    }

    public void draw(int r, int g, int b, int penWidth) {
        // ...
    }
    
    // ...
}

(Veľmi ľahké) cvičenie: nájdite príklady preťažovania metód v štandardných triedach jazyka Java.

Statické vs. nestatické metódy a premenné

Doposiaľ sme sa na tejto prednáške zaoberali výhradne metódami a premennými inštancií, t.j. nestatickými metódami a premennými. Na minulej prednáške sme naopak tvorili statické metódy a podobne možno definovať aj statické premenné – statickosť pritom znamená, že nepôjde o metódy resp. premenné inštancií, ale o metódy resp. premenné samotných tried, ktoré sa v mnohom správajú ako bežné funkcie a globálne premenné, ako ich poznáme z minulého semestra. Statickú metódu alebo premennú definujeme pomocou modifikátora static; bez jeho použitia sa metóda alebo premenná považuje za nestatickú.

V jednej triede možno kombinovať statické metódy a premenné s nestatickými.

  • Nestatické prvky príslušia inštanciám tejto triedy a statické samotnej triede.
  • Uvažujme teda napríklad triedu Trieda, jej inštanciu instancia, statickú metódu public static void statickaMetoda() a nestatickú metódu public void nestatickaMetoda(). Potom možno písať instancia.nestatickaMetoda() a Trieda.statickaMetoda(), zápis Trieda.nestatickaMetoda() ale nedáva zmysel a nemal by sa používať ani zápis instancia.statickaMetoda() (hoci technicky je ekvivalentný zápisu Trieda.statickaMetoda()).
  • Zo statických metód (v statickom kontexte) nemôžeme pristupovať k nestatickým premenným a volať nestatické metódy.
  • Z nestatických metód môžeme pristupovať ako k nestatickým prvkom (ktoré sa týkajú príslušnej inštancie), tak aj k prvkom statickým (ktoré sa týkajú triedy samotnej).

Príklad 1:

public class Trieda {

    static int a = 1;
    int b = 2;

    static void f() {
        System.out.println("Som staticka funkcia f.");
    }

    void g() {
        System.out.println("Som nestaticka funkcia g.");
    }

    public static void main(String[] args) {
        f();
        Trieda.f();     // To iste ako na predchadzajucom riadku, ale pouzitelne aj z inych tried.
        Trieda instancia = new Trieda();
        instancia.g();
        
        instancia.f();  // To iste ako iba f(), ale IDE O VELMI SKAREDY ZAPIS, pretoze f je staticka a "patri" triede
        // g();         // Chyba: non-static method g() cannot be referenced from a static context

        System.out.println(a);
        System.out.println(instancia.b);
    }
}

Príklad 2: v Jave neskôr narazíme na situácie, keď je potrebné za každých okolností pracovať s objektmi. Aj za týmto účelom Java definuje špeciálne „baliace” triedy (angl. wrapper classes) pre všetky hodnoty primitívnych typov, na ktoré sa možno pozerať ako na „primitívne typy zabalené do objektov”. „Baliaca” trieda pre typ int má názov Integer, pre typ char má názov Character a pre ostatné primitívne typy ide o názov daného typu, avšak s veľkým začiatočným písmenom.

V Jave funguje automatická konverzia medzi primitívnymi typmi a príslušnými „baliacimi” triedami, tzv. boxing a unboxing. Možno teda písať napríklad

Integer i1 = 1;
int i2 = 2;
i1 = i2; // boxing
i2 = i1; // unboxing

Avšak pozor: operátory == a != síce možno použiť na porovnanie „zabaleného” celého čísla typu Integer s „nezabaleným” celým číslom typu int, avšak pri aplikácii na dva objekty i,j typu Integer sa, rovnako ako pri ľubovoľnej inej dvojici objektov, porovnávajú referencie. Správne sa porovnanie týchto hodnôt realizuje napríklad prostredníctvom i.equals(j).

Pohľad do dokumentácie triedy Integer ukazuje, že táto trieda obsahuje tri metódy toString konvertujúce celé čísla na reťazce:

public String toString();
public static String toString(int i);
public static String toString(int i, int radix);

Prvá z týchto metód je nestatická a možno ju teda aplikovať na inštancie triedy Integer:

Integer n = 42;
String s = n.toString(); // s teraz obsahuje textovu reprezentaciu cisla n

Druhá z nich je naopak statická a ako parameter berie celé číslo, ktoré prevedie na reťazec:

int n = 42;
String s = Integer.toString(n); // s teraz obsahuje textovu reprezentaciu cisla n

Posledná je tiež statická; ako parameter berie okrem čísla aj základ pozičnej číselnej sústavy a výsledný reťazec bude reprezentáciou daného čísla v sústave o danom základe:

int n = 42;
String s = Integer.toString(n, 12); // s teraz obsahuje textovu reprezentaciu cisla n v dvanastkovej sustave

Odkazy

Letný semester, prednáška č. 3

Oznamy

  • Dnes po prednáške bude zverejnené zadanie prvej domácej úlohy, ktorej riešenia bude potrebné odovzdať najneskôr do pondelka 15. marca, 9:00 (čiže do začiatku piatej prednášky).

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 (angl. 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 premenných a metód pomocou modifikátorov, atď.
  • Konštruktory sú špeciálne kusy kódu slúžiace na vytvorenie objektu (inštancie triedy).
  • Základným princípom objektovo orientovaného programovania je zapuzdrenie (angl. encapsulation): spojenie dát a súvisiaceho kódu do koherentného logického celku.
    • 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.

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, alebo že trieda Pes triedu Zviera rozširuje. V prípade vhodne zvolených prístupových modifikátorov (detaily neskôr) totiž inštancia triedy Pes zdedí metódy a premenné (nie konštruktory) definované v triede Zviera a tie sa potom správajú tak, ako keby boli priamo definované aj v triede Pes.

Dedenie umožňuje vyhnúť sa nutnosti písať podobný kód viackrát. Namiesto implementácie podobných metód v niekoľkých triedach možno vytvoriť nadtriedu týchto tried 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:

public class Rectangle {
    private double x, y;           // Suradnice laveho horneho rohu
    private double width, height;  // Vyska a sirka obdlznika

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public void move(double deltaX, double deltaY) {
        x += deltaX;
        y += deltaY;
    }
}
public class Circle {
    private double x, y;    // Suradnice stredu
    private double radius;  // Polomer kruznice

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    public void move(double deltaX, double deltaY) {
        x += deltaX;
        y += deltaY;
    }
}

Vidíme, že obidve triedy obsahujú pomerne veľa spoločného kódu. Teraz to isté s dedením – spoločné premenné a metódy presunieme do spoločnej nadtriedy Shape:

public class Shape {
    private double x, y;  // Suradnice nejakeho vyznacneho bodu geometrickeho utvaru

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public void move(double deltaX, double deltaY) {
        x += deltaX;
        y += deltaY;
    }
}
public class Rectangle extends Shape {
    private double width, height;

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    // Pripadne dalsie metody pre obdlznik
}
public class Circle extends Shape {
    private double radius;

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    // Pripadne dalsie metody pre kruznicu
}

V rámci triedy možno používať aj verejné metódy a premenné nadtriedy, ako keby boli jej vlastné – a často aj metódy a premenné s inými modifikátormi rôznymi od private (o tom neskôr). V metódach a konštruktoroch triedy Circle tak napríklad môžeme používať metódy getX, setX, getY, setY a move.

class Rectangle extends Shape {
    // ...
    
    public Rectangle(double x, double y, double width, double height) {
        setX(x);
        setY(y);
        setWidth(width);
        setHeight(height);
    }

    // ...
}
class Circle extends Shape {
    // ...
    
    public Circle(double x, double y, double radius) {
        setX(x);
        setY(y);
        setRadius(radius);
    }
    
    @Override                    // Vyznam tejto znacky si vysvetlime o chvilu
    public String toString() {
        return "Stred: [" + getX() + "," + getY() + "]; Polomer: " + getRadius() + ".";
    }

    // ...
}
  • Inštanciu c triedy Circle teraz môžeme nielen vypísať na konzolu cez System.out.println(c) (použije sa metóda toString), ale môžeme pre ňu zavolať aj ľubovoľnú metódu triedy Shape, napríklad c.move(1, 1) alebo c.setX(2).

Dedenie a typy

  • Premenná typu Shape môže obsahovať referenciu na objekt triedy Shape alebo jej ľubovoľnej podtriedy. Vždy teda treba rozlišovať medzi typom referencie (premennej obsahujúcej referenciu na objekt) a samotným typom objektu:
Circle circle = new Circle(0, 0, 5);
Shape shape = circle;     // toto je korektne priradenie
// circle = shape;        // toto neskompiluje, kedze shape nemusi byt kruznica
circle = (Circle) shape;  // po pretypovani to uz skompilovat pojde; ak shape nie je instanciou Circle alebo null, vyhodi sa vynimka
  • Istejší prístup je pri priraďovaní premennej typu Shape do premennej typu Circle najprv overiť, či premenná typu Shape obsahuje referenciu na inštanciu triedy Circle. Na to slúži operátor instanceof. Platí pritom, že ak je objekt inštanciou nejakej triedy, je súčasne aj inštanciou ľubovoľnej jej nadtriedy (samotný typ objektu je však daný iba najnižšou triedou v tomto usporiadaní). Napríklad podmienka shape instanceof Shape je splnená kedykoľvek je splnená podmienka shape instanceof Circle. Pre ľubovoľnú triedu Trieda má výraz null instanceof Trieda vždy hodnotu false (a rovnako pre premennú obsahujúcu null).
if (shape instanceof Circle) {  
    circle = (Circle) shape;
}
  • 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):
public static void moveAll(Shape[] shapes, double deltaX, double 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;  // O chvilu uvidime, ze toto pretypovanie kvoli nasledujucemu riadku nie je nutne
        System.out.println(c);
    }
    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).
public class Shape {
    // ...    

    public Shape(double x, double y) {
        this.x = x; // alebo setX(x);
        this.y = y; // alebo setY(y);
    }
    
    // ...
}
public class Rectangle extends Shape {
    // ...
    
    public Rectangle(double x, double y, double width, double height) {
        super(x, y);
        this.width = width;   // alebo setWidth(width);
        this.height = height; // alebo setHeight(height);
    }
    
    // ...
}
public class Circle extends Shape {
    // ...         
    
    public Circle(double x, double y, double radius) {
        super(x, y);
        this.radius = radius; // alebo setRadius(radius);
    }
    
    // ...
}
  • Ak nezavoláme konštruktor nadtriedy 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:

public class Segment extends Shape {
    private double x2, y2;

    // ...

    public Segment(double x, double y, double x2, double y2) {
        super(x, y);
        this.x2 = x2;
        this.y2 = y2;
    }

    @Override
    public void move(double deltaX, double deltaY) {
        super.move(deltaX, deltaY);
        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ň.
  • Takto to ale funguje iba pri nestatických metódach (keďže statické metódy príslušia samotným triedam, rozdiel medzi typom referencie a typom inštancie tam nemožno využiť). Pri statických metódach preto nehovoríme o ich prekrývaní, ale o ich skrývaní.
  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];

  //...

  for(Shape x : shapes) {
      x.move(deltaX, deltaY);  // kazdy prvok pola 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:

public class SuperClass {
    void doX() { 
        System.out.println("doX in Super");
    }
   
    void doXTwice() { 
        doX();
        doX();
    }    
}
public 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ším príkladom takéhoto správania bude metóda approximateArea 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 a je určená na prekrytie v podtriedach (musí ísť o nestatickú metódu).
  • 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()), ale za účelom volania z podtried môže obsahovať definície konštruktorov. 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:

public abstract class Shape {
    // ...
    
    public abstract double area();

    public double approximateArea() {
        return Math.round(area());
    }
}
public class Rectangle extends Shape {
    // ...
    
    @Override
    public double area() {
        return width * height;
    }
}
public class Circle extends Shape {
    // ...
    
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}
public 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) {
        System.out.println(x.area() + " " + x.approximateArea());
    }
}

potom vypíše nasledujúci výstup:

2.0 2.0
3.141592653589793 3.0
0.0 0.0

Hierarchia tried a trieda Object

  • V Jave môže každá trieda dediť iba od jednej triedy (na rozdiel od niektorých iných jazykov, 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íklad toString alebo equals – ktoré je často užitočné prekrývať.
  • To vysvetľuje, prečo sme pri metóde toString triedy Circle použili anotáciu Override: prekryli sme totiž jej definíciu z triedy Object.
  • Vypisovanie kružnice Circle circle cez System.out.println(circle) je zas ukážkou polymorfizmu. Ide tu o použitie verzie metódy println, ktorá ako argument očakáva inštanciu triedy Object a na výpis používa metódu toString tejto inštancie. V prípade, že metódu println zavoláme pre inštanciu podtriedy triedy Object, použije sa pri výpise prekrytá verzia metódy toString.
  • Veľmi špeciálnym druhom objektov v Jave sú polia, pričom polia typu T (kde T je trieda alebo primitívny typ) sa považujú za inštancie triedy T[], ktorá je podtriedou triedy Object. Aj na polia teda v princípe možno aplikovať metódu toString triedy Object, avšak od jej použitia nemožno očakávať žiadne rozumné správanie, keďže nebola zmysluplným spôsobom prekrytá.

Rozhrania

Rozhranie (angl. interface) je podobným konceptom ako abstraktná trieda. Existuje však medzi nimi niekoľko rozdielov, z ktorých najpodstatnejšie sú tieto:

  • Rozhranie slúži predovšetkým ako zoznam abstraktných metód – kľúčové slovo abstract tu netreba uvádzať. Pri triedach implementujúcich rozhranie je garantované, že na prácu s nimi bude možné použiť metódy deklarované v rozhraní (odtiaľ aj termín „rozhranie”). Napríklad rozhranie pre zásobníky by mohlo deklarovať metódy ako push, pop a isEmpty a triedy pre zásobníky implementované pomocou polí resp. spájaných zoznamov by toto rozhranie mohli implementovať.
  • Naopak implementované metódy musia byť v rozhraní označené kľúčovým slovom default, prípadne musia byť statické (obidve tieto možnosti sa však typicky využívajú iba vo veľmi špeciálnych situáciách).
  • Rozhranie nemôže definovať konštruktory, ani iné ako finálne premenné (t. j. konštanty).
  • 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é metódy): v takom prípade používame kľúčové slovo extends.
  • Všetky položky v rozhraní sa chápu ako verejné (modifikátor public teda nie je potrebné explicitne uvádzať).
  • Podobne ako abstraktná trieda, môže byť aj rozhranie typom referencie.
  • Hoci nejde o prekrývanie v pravom slova zmysle, možno aj pri implementovaní metód z rozhraní použiť anotáciu @Override.

Príklad použitia:

public interface Movable {
    void move(double deltaX, double deltaY);
}
public interface Measurable {
    double area();
    double approximateArea();
}
public abstract class Shape implements Movable, Measurable {
    // ...

    @Override
    public void move(double deltaX, double deltaY) {
        x += deltaX;
        y += deltaY;
    }

    @Override
    public abstract double area();

    @Override
    public double approximateArea() {
        return Math.round(area());
    }

    // ...
}
    public static void main(String[] args) {
        Measurable[] elements = new Shape[3];  // Podobne ako abstraktne triedy mozu byt aj rozhrania typmi referencie
        elements[0] = new Rectangle(0, 0, 1, 2);
        elements[1] = new Circle(0, 0, 1);
        elements[2] = new Segment(1, 1, 2, 2);

        for (Measurable x : elements) {
            System.out.println(x.area() + " " + x.approximateArea());
        }
    }

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 (nedá sa aplikovať na triedu samotnú).

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ť (možno ju ale inicializovať aj za behu).
  • static:
    • Statické premenné a metódy príslušia triede samotnej, nie jej inštanciám.
    • Statické triedy vo vnútri inej triedy nie sú viazané na jej konkrétnu inštanciu (viac neskôr).

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 NullaryNode extends Node {
}
abstract class UnaryNode extends Node {
    private Node child;
    
    public Node getChild() {
        return child;
    }

    public UnaryNode(Node child) {
        this.child = child; 
    }
}
abstract class BinaryNode extends Node {
    private Node left;
    private Node right;

    public Node getLeft() {
        return left;
    }

    public Node getRight() {
        return right;
    }

    public BinaryNode(Node left, Node right) { 
        this.left = left;
        this.right = right; 
    }
}
class Constant extends NullaryNode {
    private 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 -getChild().evaluate();
    }

    @Override 
    public String toString() { 
        return "(-" + getChild().toString() + ")";
    }
}
class Plus extends BinaryNode { 
    public Plus(Node left, Node right) { 
        super(left, right);
    }

    @Override
    public int evaluate() { 
        return getLeft().evaluate() + getRight().evaluate();
    }

    @Override 
    public String toString() { 
        return "(" + getLeft().toString() + "+" + getRight().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

Letný semester, prednáška č. 4

Oznamy

  • Počas najbližších cvičení, čiže v stredu 10. marca od 11:30 do 13:00 bude prebiehať prvý test. Bude pozostávať z troch úloh zameraných na látku z prvých troch týždňov, ktorých riešenia bude potrebné odovzdávať na testovač. Po dobu riešenia testu je potrebná účasť na stretnutí „Cvičenia” v MS Teams.
  • V stredu tiež budú zverejnené dve nebodované úlohy k štvrtým cvičeniam, zamerané na látku z dnešnej prednášky. Ďalšie podobne zamerané úlohy budú zverejnené na cvičeniach budúci týždeň.
  • Prvú domácu úlohu je potrebné odovzdať najneskôr do začiatku budúcej prednášky, čiže do pondelka 15. marca, 9:00.

Výnimky

Mechanizmus výnimiek (angl. exceptions) slúži v Jave na spracovanie chýb a iných výnimočných udalostí, ktoré môžu počas vykonávania programu nastať. Doposiaľ sme v našich programoch takéto situácie viac-menej ignorovali – napríklad sme obvykle predpokladali, že vstup vždy spĺňa požadované podmienky, že súbor, z ktorého sa pokúšame čítať, vždy existuje, atď. Dôvodom bola predovšetkým prílišná prácnosť ošetrovania chýb pomocou podmienok a neprehľadnosť programov, ktoré takéto podmienky obsahujú.

Uvažujme napríklad nasledujúci jednoduchý program, ktorý zo vstupu načíta prirodzené číslo n nasledované n celými číslami, ktoré postupne poukladá do poľa dĺžky n. Následne číta zo vstupu postupnosť indexov v rozmedzí 0n-1 a po každom načítaní indexu i zvýši hodnotu a[i] o jedna. Načítavanie vstupu sa ukončí po načítaní reťazca "KONIEC".

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int n = scanner.nextInt();
    int a[] = new int[n];
    for (int i = 0; i <= n - 1; i++) {
        a[i] += scanner.nextInt();
    }
    String next = scanner.next();
    while (!next.equals("KONIEC")) {
        a[Integer.parseInt(next)]++;
        next = scanner.next();
    }
}

Vykonávanie tohto programu môže skončiť chybou viacerými rôznymi spôsobmi: používateľ napríklad môže namiesto niektorého čísla zadať nečíselný reťazec; číslo n môže ďalej zadať ako záporné, čím vznikne chyba pri pokuse o alokovanie poľa; niektorý z ním zadaných indexov do poľa tiež nemusí byť v požadovanom rozmedzí. Po pokuse o ošetrenie týchto chýb pomocou podmienok dostávame nasledujúci horibilný program.

/** Nasledujuci kod je ukazkou toho, ako sa osetrovanie chyb v Jave NEMA robit. */

/** Metoda, ktora zisti, ci je retazec nebielych znakov reprezentaciou celeho cisla. */
private static boolean isInteger(String s) {
    Scanner stringScanner = new Scanner(s);
    return stringScanner.hasNextInt();
}

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int n = 0;
    if (scanner.hasNextInt()) {
        n = scanner.nextInt();
        if (n < 0) {
            System.err.println("Pocet prvkov pola nemoze byt zaporny.");
            System.exit(2);
        }
    } else {
        System.err.println("Vstup sa nezacina cislom.");
        System.exit(1);
    }
    int a[] = new int[n];
    for (int i = 0; i <= n - 1; i++) {
        if (scanner.hasNextInt()) {
            a[i] += scanner.nextInt();
        } else {
            System.err.println("Niektory z prvkov pola nebol zadany ako cele cislo.");
            System.exit(3);
        }
    }
    String next = scanner.next();
    while (!next.equals("KONIEC")) {
        if (isInteger(next)) {
            int i = Integer.parseInt(next);
            if (i >= 0 && i <= n - 1) {
                a[i]++;
            } else {
                System.err.println("Niektory z indexov do pola je mimo povoleneho rozmedzia.");
                System.exit(5);
            }
        } else {
            System.err.println("Niektory z indexov do pola nebol zadany ako cele cislo.");
            System.exit(4);
        }
        next = scanner.next();
    }
}

Vidíme, že už aj pri takto jednoduchej úlohe dostávame pomerne rozsiahly program, v ktorom väčšinu kódu zaberá práve spracovanie chýb. Ešte nepríjemnejšou je však skutočnosť, že ošetrovanie chýb je potrebné implementovať v mieste programu, kde táto chyba vznikla. To vedie k prelínaniu podstatných častí programu s časťami, ktoré slúžia iba na spracovanie chýb, v dôsledku čoho sa kód stáva veľmi neprehľadným. Z uvedených dôvodov sme až doposiaľ podobné chybové udalosti väčšinou ignorovali.

V praxi je ale nutné podobné chyby náležite poošetrovať – nie je totiž napríklad prípustné, aby textový editor spadol zakaždým, keď sa jeho používateľ pokúsi otvoriť súbor s neexistujúcim názvom. Výnimky poskytujú spôsob, ako spracovanie chybových udalostí realizovať podstatne efektívnejším spôsobom, než v ukážke vyššie. Medzi ich základné prednosti totiž patria:

  • Možnosť oddelenia kódu na spracovanie chýb od zvyšku kódu.
  • Možnosť jednoduchým spôsobom ponechať spracovanie chyby na volajúcu metódu v prípade, že sa to javí ako vhodnejšie riešenie.
  • Možnosť využívať pri spracúvaní chýb prostriedky objektovo orientovaného programovania.

Mechanizmus výnimiek v Jave

Pod výnimkou (angl. exception) sa v Jave rozumie inštancia špeciálnej triedy Exception, prípadne jej nadtriedy Throwable, reprezentujúca nejakú výnimočnú udalosť. Trieda Exception má pritom množstvo podtried reprezentujúcich rôzne typy chybových udalostí.

  • Výnimka môže počas vykonávania nejakej metódy f vzniknúť buď tak, že ju vyhodí JVM (napríklad pri delení nulou alebo pri prístupe k neexistujúcemu prvku poľa), alebo tak, že ju vyhodí sama táto metóda pomocou príkazu throw (detaily nižšie).
  • Vzniknutá výnimka môže byť priamo odchytená a ošetrená v rámci danej metódy f. V opačnom prípade je vykonávanie metódy ukončené a výnimka je posunutá metóde g, ktorá metódu f volala (posunieme sa teda v zásobníku volaní metód o úroveň nižšie).
  • V metóde g sa na celú situáciu dá pozerať tak, akoby výnimka vznikla pri vykonávaní príkazu, v rámci ktorého sa volala metóda f. Opäť teda môže byť výnimka buď odchytená a ošetrená, alebo rovnakým spôsobom predaná metóde, ktorá volala metódu g (aj v takom prípade hovoríme, že metóda g vyhodila výnimku, hoci reálne výnimka vznikla už v metóde f).
  • Takto sa v zásobníku volaní metód pokračuje nižšie a nižšie, až kým je nájdená metóda, ktorá danú výnimku odchytí a spracuje.
  • Ak sa takáto metóda na zásobníku volaní nenájde – čiže je výnimka vyhodená aj metódou main na jeho dne – typicky dôjde k ukončeniu vykonávania programu a k vypísaniu informácií o výnimke (vrátane zásobníka volaní metód) na štandardný chybový výstup; ide tu ale o zjednodušenie.
Vynimky.png

Chytanie a ošetrovanie výnimiek

Odchytenie a spracovanie výnimky možno v Jave realizovať nasledujúcim spôsobom:

  • Kód, v ktorom môže výnimka nastať, obalíme do bloku try.
  • Kód spracúvajúci výnimku umiestnime do bezprostredne nasledujúceho bloku catch. Za samotným kľúčovým slovom catch nasleduje jeden argument reprezentujúci výnimku, ktorá sa má odchytiť. Napríklad blok catch (Exception e) odchytí ľubovoľnú výnimku, ktorá je inštanciou triedy Exception (alebo nejakej jej podtriedy).
  • Kedykoľvek nastane v bloku try výnimka, okamžite sa vykonávanie tohto bloku ukončí. Ak je vzniknutá výnimka kompatibilná s argumentom bloku catch, pokračuje sa blokom catch a výnimka sa považuje za odchytenú. (Ak výnimka s týmto argumentom nie je kompatibilná, bude správanie rovnaké ako pri absencii bloku try.)

Príklad: Uvažujme program, ktorý prečíta zo vstupného súboru vstup.txt prirodzené číslo n a za ním n celých čísel. Napokon vypíše na konzolu súčet načítaných čísel. V takomto programe môže nastať výnimka predovšetkým z dvoch dôvodov: súbor vstup.txt nemusí existovať alebo môže nastať iná chyba pri pokuse o vytvorenie inštancie triedy Scanner; nemusí byť dodržaný požadovaný formát vstupného súboru a môže tak vzniknúť výnimka pri volaní metódy nextInt triedy Scanner. Kód teda môžeme obaliť do bloku try a prípadnú výnimku môžeme spracovať napríklad jednoduchým výpisom textu "Nieco sa pokazilo." do štandardného chybového výstupu. Všetok kód nasledujúci za blokom catch sa vykoná bez ohľadu na to, či výnimka nastala alebo nenastala.

import java.io.*;
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        try {
            Scanner scanner = new Scanner(new File("vstup.txt"));
            int n = scanner.nextInt();
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += scanner.nextInt();
            }
            System.out.println("Sucet je: " + sum + ".");
            scanner.close();
        } catch (Exception e) {
            System.err.println("Nieco sa pokazilo.");
        }
        System.out.println("Aj po odchytenej vynimke pokracujem dalej a vypisem tento text...");
    }
}

Namiesto výpisu textu "Nieco sa pokazilo." by sme napríklad mohli vypísať aj informácie o vzniknutej výnimke použitím metódy

e.printStackTrace();

Uvedené riešenie má ale dva menšie nedostatky:

  • V prípade vyhodenia výnimky nikdy nedôjde uzatvoreniu vstupného súboru, pretože vykonávanie bloku try sa preruší ešte predtým, než príde na rad príslušný príkaz. O niečo vhodnejšou alternatívou by bolo presunutie príkazu na zatvorenie scanner-a až za koniec bloku try; ale aj vtedy by sa mohlo stať, že sa tento príkaz nevykoná kvôli nejakej výnimke, ktorá nebola odchytená (napríklad inštancia typu Throwable, alebo výnimka vyhodená v bloku catch). Riešením je pridať za bloky catch blok finally, ktorý sa vykoná bez ohľadu na to, či nastala výnimka a či sa nám ju podarilo odchytiť (dokonca sa vykoná aj v prípade, že sa v try bloku úspešne vykonal príkaz return).
import java.io.*;
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(new File("vstup.txt"));
            int n = scanner.nextInt();
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += scanner.nextInt();
            }
            System.out.println("Sucet je: " + sum + ".");
        } catch (Exception e) {
            System.err.println("Nieco sa pokazilo.");
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
        System.out.println("Aj po odchytenej vynimke pokracujem dalej a vypisem tento text...");
    }
}
  • Nerozlišujeme medzi dvoma najpravdepodobnejšími príčinami vyhodenia výnimky: medzi chybou nejakým spôsobom súvisiacou s manipuláciou so súborom vstup.txt a medzi chybou spôsobenou zlým formátom vstupu. To sa dá napraviť s využitím skutočnosti, že výnimky pre rôzne udalosti sú typicky rôzneho typu (vždy ale ide o inštancie podtried Throwable a zvyčajne aj Exception). V prvom prípade sa vyhodí výnimka, ktorá je inštanciou triedy IOException z balíka java.io alebo nejakej jej podtriedy (napríklad FileNotFoundException). V druhom prípade pôjde o výnimku typu NoSuchElementException z balíka java.util, vyhodenú metódou nextInt triedy Scanner. Za blok try môžeme pridať aj viacero blokov catch pre viacero typov výnimiek. V prípade, že je niektorá výnimka „kompatibilná” s viacerými takýmito blokmi, bude odchytená prvým z nich.
import java.io.*;
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(new File("vstup.txt"));
            int n = scanner.nextInt();
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += scanner.nextInt();
            }
            System.out.println("Sucet je: " + sum + ".");
        } catch (IOException e) {
            System.err.println("Chyba suvisiaca s pristupom k suboru vstup.txt.");
        } catch (NoSuchElementException e) {
            System.err.println("Zly format vstupneho suboru.");
        } catch (Exception e) {
            // Takto spracovat vynimku, o ktorej nic netusime, nie je uplne najlepsi napad (takze len na ukazku).
            System.err.println("Nejaka ina vynimka.");   
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
        System.out.println("Aj po odchytenej vynimke pokracujem dalej a vypisem tento text...");
    }
}
  • Pokiaľ používateľ zadá číslo n ako záporné, správa sa program rovnako ako keby bolo n rovné nule. Možno vhodnejšie by ale aj v takom prípade bolo vyhodiť chybu, ktorá by používateľa upozornila na to, že zadal zlý vstup. Túto fukncionalitu doplníme neskôr, pretože budeme potrebovať vedieť hádzať „úplne nové” výnimky.

Hierarchia výnimiek

V Jave existuje množstvo tried pre rôzne druhy výnimiek. Malá časť hierarchie týchto tried je znázornená na nasledujúcom obrázku.

Vynimky hierarchia.png

Všetky výnimky v Jave dedia od triedy Throwable. Tá má dve podtriedy:

  • Exception, od ktorej dedí väčšina „bežných” výnimiek.
  • Error, od ktorej dedia triedy reprezentujúce závažné, v rámci aplikácie ťažko predpovedateľné systémové chyby (napríklad nedostatok pamäte). Jednou z najznámejších podtried triedy Error je StackOverflowError.

Podtriedy triedy Exception možno ďalej rozdeliť na dve základné kategórie:

  • RuntimeException a jej podtriedy. Výnimky tohto typu obvykle reprezentujú rozličné programátorské chyby, ako napríklad prístup k neexistujúcemu prvku poľa (ArrayIndexOutOfBoundsException), delenie nulou (ArithmeticException), použitie kódu „očakávajúceho” objekt na referenciu null (NullPointerException), atď. Do tejto kategórie patria aj výnimky typu NoSuchElementException, hoci tie sme v príklade vyššie možno trochu zneužili na ošetrenie zlého formátu vstupného súboru. Vo všeobecnosti platí, že výnimky tohto typu by buď nemali vôbec nastať (programátorské chyby, ktoré je nutné odladiť), alebo by mali byť ošetrené priamo v metóde, v ktorej vzniknú (ako napríklad NoSuchElementException v našom príklade vyššie).
  • Zvyšné podtriedy triedy Exception. Tie často reprezentujú neočakávateľné udalosti, ktorým sa na rozdiel od výnimiek typu RuntimeException nedá úplne vyhnúť (napríklad FileNotFoundException a jej nadtrieda IOException). Dobre napísaný program by sa mal vedieť z výnimiek tohto typu zotaviť (nie je napríklad dobré ukončiť program vždy, keď sa mu nepodarí otvoriť súbor).

S týmto rozdelením podľa druhov chýb reprezentovaných jednotlivými výnimkami súvisí aj nasledujúca nutnosť:

  • Výnimka ľubovoľného typu okrem RuntimeException, Error a ich podtried musí byť v metóde, v ktorej môže vzniknúť, vždy buď odchytená, alebo v opačnom prípade ich musí táto metóda deklarovať vo svojej hlavičke ako neodchytené. Napríklad:
void f() throws FileNotFoundException, UnsupportedEncodingException {
    // ...
}
  • Popri vykonaní príkazu return je totiž vyhodenie výnimky ďalším možným spôsobom ukončenia vykonávania volanej metódy. Preto musí byť táto možnosť v hlavičke metódy explicitne špecifikovaná rovnako ako napríklad návratový typ.
  • Pri inštanciách triedy RuntimeException a jej podtried sa od tejto požiadavky upúšťa, pretože – ako bolo spomenuté vyššie – ide väčšinou o programátorské chyby, ktoré je nutné odladiť, alebo sú tieto výnimky odchytené priamo v metóde, v ktorej vzniknú. Pri inštanciách triedy Error a jej podtried zas často ide o systémové chyby, zotavenie z ktorých principiálne nie je možné. Často je teda najlepším riešením ukončiť samotný program.
  • Hoci môže hádzané výnimky prostredníctvom throws deklarovať aj metóda main (s príkladmi sme sa už stretli), pri reálnych programoch sa to nepovažuje za dobrú prax – všetky výnimky, ktoré sú inštanciami Exception, by mali byť ošetrené.

Hádzanie výnimiek a výnimky nových typov

Vyhodenie novej výnimky možno pre inštanciu e triedy Throwable alebo jej podtried realizovať príkazom

throw e;

Najčastejšie sa pritom throw aplikuje na novovytvorenú inštanciu nejakej triedy, napríklad

throw new IOException();

alebo

throw new IOException("Sprava");

Pre úplne nové typy chybových udalostí sa vo všeobecnosti neodporúča používať výnimky existujúcich typov. Naopak je vhodnejšie napísať novú podtriedu triedy Exception reprezentujúcu výnimky požadovaného typu a následne pomocou throw vyhadzovať inštancie tejto triedy. Napríklad pre účely nášho ukážkového programu vyššie môžeme napísať triedu výnimiek NegativeElementCountException, ktorej inštancie budeme vyhadzovať zakaždým, keď používateľ zadá ako počet čísel zápornú hodnotu. Táto trieda môže vyzerať napríklad nasledovne.

public class NegativeElementCountException extends Exception {
    Integer number;

    public NegativeElementCountException() {
        number = null;  // Netreba
    }

    public NegativeElementCountException(int number) {
        this.number = number;
    }

    @Override
    public String getMessage() {
        if (number != null) {
            return "Zaporny pocet cisel: " + number + ".";
        } else {
            return "Zaporny pocet cisel.";
        }
    }
}

Použitie tejto výnimky v samotnom programe potom môže vyzerať napríklad takto.

import java.io.*;
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(new File("vstup.txt"));
            int n = scanner.nextInt();
            if (n < 0) {
                throw new NegativeElementCountException(n);
            }
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += scanner.nextInt();
            }
            System.out.println("Sucet je: " + sum + ".");
        } catch (IOException e) {
            System.err.println("Chyba suvisiaca s pristupom k suboru vstup.txt.");
        } catch (NoSuchElementException e) {
            System.err.println("Zly format vstupneho suboru.");
        } catch (NegativeElementCountException e) {
            System.err.println(e);
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
        System.out.println("Aj po odchytenej vynimke pokracujem dalej a vypisem tento text...");
    }
}

Generické programovanie

V minulom semestri sme videli viacero abstraktných dátových typov a dátových štruktúr, ako napríklad zásobník alebo rad hodnôt typu T, či binárny strom uchovávajúci v uzloch hodnoty typu T. Typ T mohol byť často ľubovoľný, inokedy sa naň kládli určité podmienky (napríklad pri binárnych vyhľadávacích stromoch mohla byť takouto podmienkou existencia úplného usporiadania na T). Zakaždým sme ale boli schopní napísať implementácie týchto dátových štruktúr iba pre jeden pevne zvolený typ; v C++ sme síce pomocou typedef tento typ vedeli rýchlo meniť, ale mohli sme sa dostať do problémov v situáciách, keď sme potrebovali pracovať s dvoma inštanciami tej istej dátovej štruktúry pre dva rozdielne typy.

Pri využití nástrojov objektovo orientovaného programovania sa ponúka jedno rýchle východisko z tejto situácie: mohli by sme napríklad napísať zásobník, rad, alebo strom uchovávajúci inštancie triedy Object – inštanciami triedy Object sú totiž úplne všetky objekty. Aj tento prístup má ale svoje veľké nevýhody: napríklad pri zásobníku reťazcov by sme museli všetky objetky vyberané zo zásobníka pretypovať na String, čo by bolo nielen prácne, ale aj náchylné na chyby (niekto napríklad mohol omylom vložiť na zásobník objekt, ktorý nie je inštanciou triedy String). Ukážeme si teraz elegantnejšie riešenie podobných situácií pomocou generického programovania, ktoré umožňuje parametrizovať triedy a metódy typovými parametrami. Tento nástroj vo veľkej miere využívajú aj dátové štruktúry zo štandardnej knižnice tried jazyka Java.

Generické triedy

Triedu, ktorá závisí od typového parametra T – takzvanú generickú triedu – možno v Jave napísať takto:

public class GenerickaTrieda<T> {
    // ...
}

Trieda môže závisieť aj od viacerých typových parametrov, napríklad:

public class GenerickaTrieda<T1, T2, T3, T4> {
    // ...
}

Vo vnútri generickej triedy možno v nestatickom kontexte s typovými parametrami pracovať podobne, ako keby išlo o bežnú triedu (pri určitých obmedzeniach; nemožno napríklad tvoriť nové inštancie typového parametra). Napríklad:

import java.util.*;

public class GenerickaTrieda<T> {
    private T[] a;

    public GenerickaTrieda(T[] a) {
        this.a = Arrays.copyOf(a, a.length);
    }

    // ...
}

Pri vytváraní inštancií generickej triedy za typový parameter dosadíme typový argument, ktorým môže byť ľubovoľný neprimitívny typ (čiže napríklad trieda alebo rozhranie):

Integer a[] = {1, 2, 3, 4, 5};
GenerickaTrieda<Integer> g = new GenerickaTrieda<Integer>(a);

alebo skrátene

Integer a[] = {1, 2, 3, 4, 5};
GenerickaTrieda<Integer> g = new GenerickaTrieda<>(a);
  • Zápis <> (takzvaný diamant) možno použiť kedykoľvek je typový argument zrejmý z kontextu (využíva sa tu mechanizmus automatickej inferencie typov).

Pre každú konkrétnu triedu KonkretnaTrieda teda máme parametrizovaný typ GenerickaTrieda<KonkretnaTrieda>, ktorý sa chová podobne ako trieda (možno z neho tvoriť inštancie a pod., ale existujú určité drobné obmedzenia jeho použitia).

  • Ak je pritom KonkretnaTrieda podtriedou triedy InaKonkretnaTrieda, tak každý objekt typu KonkretnaTrieda je súčasne aj inštanciou triedy InaKonkretnaTrieda. Táto relácia sa neprenáša na typy GenerickaTrieda<KonkretnaTrieda> a GenerickaTrieda<InaKonkretnaTrieda>.
  • Napríklad inštanciu typu GenerickaTrieda<Integer> teda nemožno použiť v situácii, keď sa očakáva inštancia typu GenerickaTrieda<Object>.
  • Aj generická trieda ale môže dediť od inej triedy, rovnako ako každá iná trieda:
class Trieda1 {
    
}

class Trieda2<T> extends Trieda1 {  
    // OK, v tele triedy mozeme pouzivat typovy parameter T.
} 

class Trieda3<T> extends Trieda2<T> {  
    // OK, pre kazde T je instancia parametrizovanej triedy Trieda3<T> sucasne aj instanciou Trieda2<T>.
}

class Trieda4<T> extends Trieda2<Integer> {  
    // OK, pre vsetky T je instancia Trieda4<T> sucasne aj instanciou Trieda2<Integer>.
    // Parameter T pre Trieda4 nijak nesuvisi s parametrom T pre Trieda2.
}

class Trieda5 extends Trieda2<Integer> {
    // OK.
}

class Trieda6 extends Trieda2<T> {
    // CHYBA: nie je jasne, co je T.
}
  • Podobne ako generické triedy možno tvoriť aj generické rozhrania.

Príklad: uzol binárneho stromu ako generická trieda

Základ generickej triedy Node<T> reprezentujúcej uzol binárneho stromu uchovávajúci inštanciu value nejakej triedy T môže vyzerať napríklad nasledovne.

public class Node<T> {
    private T value;
    private Node<T> left, right;

    public Node(T value, Node<T> left, Node<T> right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    @Override
    public String toString() {
        String leftString = "";
        String rightString = "";
        if (left != null) {
            leftString = "(" + left.toString() + ") ";
        }
        if (right != null) {
            rightString = " (" + right.toString() + ")";
        }
        return leftString + this.value + rightString;
    }
}
  • Trieda Node<T> teda poskytuje konštruktor, ktorý ako parameter berie uchovávanú inštanciu triedy T a referencie na ľavého a pravého syna.
  • Trieda taktiež implementuje metódu toString, ktorá vráti „infixovú textovú reprezentáciu” stromu.

Program využívajúci triedu Node<T> môže vyzerať napríklad takto:

public static void main(String[] args) {
    Node<Integer> integerRoot = new Node<>(4,
            new Node<>(3,
                    new Node<>(9, null, null),
                    new Node<>(2, null, new Node<>(5, null, null))),
            new Node<>(0, null, null));
    Node<String> stringRoot = new Node<>("koren",
            new Node<>("lavy syn korena",
                    new Node<>("nieco", null, null),
                    new Node<>("nieco ine", null, new Node<>("nieco este ine", null, null))),
            new Node<>("pravy syn korena", null, null));
    System.out.println(integerRoot);
    System.out.println(stringRoot);
}

Generické metódy

Podobne ako generické triedy možno tvoriť aj jednotlivé generické metódy, pri ktorých sa typové parametre píšu pred návratový typ. Tieto parametre sú „viditeľné” iba v rámci danej metódy. Pri volaní metódy možno za tieto parametre buď explicitne dosadiť konkrétne triedy, alebo sa o to postará automatický mechanizmus inferencie typov.

Príklad: nasledujúca generická statická metóda createFullTree vytvorí plný binárny strom danej výšky height, ktorého uzly všetky obsahujú rovnakú inštanciu value triedy dosadenej ako argument za typový parameter T.

public class Trieda {

    public static <T> Node<T> createFullTree(int height, T value) {
        if (height == 0) {
            return new Node<>(value, null, null);
        } else {
            return new Node<>(value, createFullTree(height - 1, value), createFullTree(height - 1, value));
        }
    }

    public static void main(String[] args) {
        Node<Integer> root1 = Trieda.<Integer>createFullTree(4, 0);
        Node<String> root2 = createFullTree(4, "retazec");  // Skrateny zapis spoliehajuci sa na automaticku inferenciu typu
        System.out.println(root1);
        System.out.println(root2);
    }
}

Ohraničené typové parametre

Často nastáva situácia, keď typový parameter nemôže byť úplne ľubovoľný, ale musí spĺňať určité podmienky. Predpokladajme napríklad, že by sme do našej generickej triedy Node<T> chceli pridať metódu, ktorá nájde najmenší prvok typu T uložený v niektorom z uzlov podstromu, ktorého je daný uzol koreňom. Takáto úloha dáva zmysel iba vtedy, keď je na T definované úplné usporiadanie – čiže keď možno inštancie triedy T navzájom porovnávať. V štandardných triedach jazyka Java býva táto možnosť vyjadrená tým, že daná trieda T implementuje rozhranie Comparable<T>.

Trieda T implementujúca Comparable<U> pre nejakú – vo všeobecnosti aj inú – triedu U, musí poskytovať metódu

public int CompareTo(U o);

ktorá vráti záporné číslo, nulu, alebo kladné číslo podľa toho, či je inštancia this triedy T menšia, rovná, alebo väčšia ako inštancia o triedy U. To samozrejme dáva najväčší zmysel, keď je U to isté ako T – v takom prípade je možné inštancie triedy T porovnávať medzi sebou. Triedy T implementujúce rozhranie Comparable<T> vždy poskytujú metódu

public int CompareTo(T o);

s významom opísaným vyššie.

Chceli by sme teda obmedziť typový parameter pre generickú triedu Node<T> tak, aby zaň mohla byť dosadená iba inštancia triedy T implementujúcej rozhranie Comparable<T>. To možno urobiť pomocou takzvaného ohraničeného typového parametra. Pre ľubovoľnú triedu AnyClass a pre ľubovoľné rozhranie AnyInterface možno typový parameter písať ako

<T extends AnyClass>

resp.

<T extends AnyInterface>

čím sa vyjadrí, že typový parameter T musí byť podtriedou triedy AnyClass resp. musí implementovať rozhranie AnyInterface (aj pri rozhraniach sa naozaj píše slovo extends).

Upravená trieda pre uzol binárneho stromu tak môže vyzerať napríklad nasledovne.

public class Node<T extends Comparable<T>> {
    private T value;
    private Node<T> left, right;

    public Node(T value, Node<T> left, Node<T> right) {
        this.value = value;
        this.left = left;
        this.right = right;
    }

    @Override
    public String toString() {
        String leftString = "";
        String rightString = "";
        if (left != null) {
            leftString = "(" + left.toString() + ") ";
        }
        if (right != null) {
            rightString = " (" + right.toString() + ")";
        }
        return leftString + this.value + rightString;
    }

    public T minValue() {
        T result = value;
        if (left != null && result.compareTo(left.minValue()) > 0) {
            result = left.minValue();
        }
        if (right != null && result.compareTo(right.minValue()) > 0) {
            result = right.minValue();
        }
        return result;
    }
}
public class Trieda {

    public static <T extends Comparable<T>> Node<T> createFullTree(int height, T value) {
        if (height == 0) {
            return new Node<>(value, null, null);
        } else {
            return new Node<>(value, createFullTree(height - 1, value), createFullTree(height - 1, value));
        }
    }

    public static void main(String[] args) {
        // ...
        System.out.println(root.minValue());
    }
}

Divoké karty

Vieme už, že ak je trieda SubClass podtriedou triedy SuperClass, tak napríklad inštancia parametrizovanej triedy Node<SubClass> už nebude inštanciou parametrizovanej triedy Node<SuperClass>. Hoci teda každý uzol prvého typu svojím spôsobom reprezentuje aj uzol druhého typu, nemožno v situácii, keď sa požaduje inštancia typu Node<SuperClass> použiť inštanciu typu Node<SubClass>. Keby sme teda napríklad chceli napísať (bežnú negenerickú) metódu f, ktorá berie ako argument uzol binárneho stromu uchovávajúci inštanciu triedy SuperClass, alebo niektorej jej podtriedy, nestačilo by napísať metódu

public static void f(Node<SuperClass> root) {
    // ...
}

Keby sme totiž skúsili zavolať túto metódu s parametrom typu Node<SubClass>, program by neskompiloval. Riešením v takýchto situáciách je použitie tzv. divokej karty (angl. wildcard) reprezentovanej symbolom „?”. Napríklad Node<? extends SuperClass> root reprezentuje argument metódy ľubovoľného z typov Node<SomeClass>, kde SomeClass je podtriedou SuperClass kompatibilnou s ohraničením typového parametra danej generickej triedy. Rovnako ako s nadtriedami môžeme použiť túto notáciu aj s rozhraniami, ktoré majú byť implementované.

public static void f(Node<? extends SuperClass> root) {
    SuperClass minValue = root.minValue();  
    // ...
}

Spolu s takýmito „zhora ohraničenými” divokými kartami možno používať aj „zdola ohraničené” divoké karty v tvare <? super SubClass>, ktorými sa vyjadrí požiadavka, že T je ľubovoľná nadtrieda triedy SubClass kompatibilná s ohraničením typového parametra danej generickej triedy. Podobne možno používať aj „neohraničenú” divokú kartu <?> vyjadrujúcu ľubovoľnú triedu kompatibilnú s ohraničením typového parametra danej generickej triedy (pre generickú triedu Node<T extends Comparable> je teda napríklad parameter Node<?> root ekvivalentný parametru Node<? extends Comparable> root).

Úvod do Java Collections

V štandardných knižniciach jazyka Java možno nájsť viacero generických tried reprezentujúcich rôzne užitočné dátové štruktúry. Tieto triedy sú spoločne známe ako Java Collections, sú poväčšine definované v balíku java.util a vyznačujú sa predovšetkým tým, že implementujú generické rozhranie Collection<E> reprezentujúce nejakú skupinu objektov. Je užitočné tieto triedy poznať a v čo najväčšej miere ich používať. Na tejto prednáške si iba v rýchlosti predstavíme niektoré z nich; podrobnejšie sa nimi budeme zaoberať na nasledujúcej prednáške. Nebudeme tu uvádzať obsiahle zoznamy metód poskytovaných jednotlivými týmito triedami – tieto informácie možno ľahko dohľadať v dokumentácii – a namiesto toho sa zakaždým sústredíme iba na krátku ukážku ich použitia.

Trieda ArrayList

Generická trieda ArrayList<E> reprezentuje dynamické pole prvkov typu E. Toto pole mení svoju veľkosť podľa momentálnej potreby.

  • Prvok na koniec poľa pridáva metóda add.
  • Prvok na danej pozícii dostaneme metódou get.
  • Metóda set mení prvok na danej pozícii na určenú hodnotu.
  • Dĺžku poľa vracia metóda size.

Príklad:

import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        ArrayList<Integer> a = new ArrayList<>();
        for (int i = 0; i <= 9; i++) {
            a.add(i);
        }
        for (int i = 0; i <= a.size() - 1; i++) {
            a.set(i, i + 1);
        }
        for (int i = 0; i <= a.size() - 1; i++) {
            System.out.print(a.get(i) + " ");
        }
    }
}

Rozhranie List a trieda LinkedList

Veľká časť metód poskytovaných inštanciami triedy ArrayList je deklarovaná v rozhraní List, ktoré je implementované triedami reprezentujúcimi nejaký typ zoznamu. Popri triede ArrayList<E> je ďalšou triedou implementujúcou toto rozhranie trieda LinkedList<E> reprezentujúca obojsmerne spájaný zoznam.

  • Keďže implementuje rovnaké rozhranie List<E> ako ArrayList<E>, možno na prácu s ním používať podobné metódy. Dokonca je často užitočné zvoliť za typ vstupného argumentu metódy očakávajúcej zoznam priamo rozhranie List; v takom prípade možno metódu volať ako pre ArrayList, tak aj pre LinkedList.
  • Treba však pamätať na to, že odlišná implementácia tried ArrayList a LinkedList má za následok aj rozdiely v efektívnosti jednotlivých vykonávaných operácií. V spájanom zozname je oproti poľu podstatne efektívnejšie pridávanie prvkov na jeho začiatok alebo koniec, podobne aj ich odoberanie (LinkedList tu oproti rozhraniu List a triede ArrayList poskytuje aj niekoľko metód navyše). Naopak omnoho menej efektívny je prístup k prvku na danej pozícii pomocou metódy get.
  • Vypísanie všetkých prvkov spájaného zoznamu by sme teda v princípe mohli realizovať aj rovnako ako pre polia:
LinkedList<Integer> list = new LinkedList<>();
// ...
for (int i = 0; i <= list.size() - 1; i++) {
    System.out.print(list.get(i) + " ");
}
Omnoho efektívnejšie je ale použitie iterátora (o tom viac nabudúce):
LinkedList<Integer> list = new LinkedList<>();
// ...
for (Iterator<Integer> it = list.iterator(); it.hasNext(); ) {
    System.out.print(it.next() + " ");
}
Prípadne tiež možno použiť ekvivalentnú konštrukciu
LinkedList<Integer> list = new LinkedList<>();
// ...
for (Integer x : list) {
    System.out.print(x + " ");
}

Niektoré ďalšie dátové štruktúry

Java Collections obsahuje aj množstvo ďalších tried a rozhraní. Napríklad:

  • Rozhranie Set<E> pre množiny a jeho implementácie ako napríklad HashSet<E>.
  • Rozhranie Map<K,V> pre zobrazenia (resp. slovníky alebo asociatívne polia), pri ktorých sú kľúče typu K zobrazované na hodnoty typu V. Implementáciou tohto rozhrania je napríklad trieda HashMap<K,V>.
  • Trieda Stack<E> reprezentujúca zásobník a rozhranie Queue<E> pre rad, ktoré okrem iného implementuje aj trieda LinkedList<E>. Triedu LinkedList<E> možno použiť aj ako zásobník, pretože implementuje rozhranie Deque<E> pre obojstranný rad, ktoré okrem iného deklaruje metódy push a pop (trieda LinkedList je efektívnejšia ako Stack).
  • ...

Odkazy

Riešenia testu č. 1

Úloha č. 1

import java.util.*;

public class CyclicArray {
    private int a[];

    public CyclicArray(int a[]) {
        this.a = Arrays.copyOf(a, a.length);
    }

    public int get(int index) {
        return a[index % a.length];
    }

    public void set(int index, int value) {
        a[index % a.length] = value;
    }

    public int[] toArray() {
        return Arrays.copyOf(a, a.length);
    }
}

Úloha č. 2

package arithmetic;

abstract class Node {

    public abstract int evaluate(int x);

}
package arithmetic;

class Plus extends BinaryNode {

    // ...

    @Override
    public int evaluate(int x) {
        return getLeft().evaluate(x) + getRight().evaluate(x);
    }
}
package arithmetic;

class Times extends BinaryNode {

    // ...

    @Override
    public int evaluate(int x) {
        return getLeft().evaluate(x) * getRight().evaluate(x);
    }
}
package arithmetic;

class Constant extends NullaryNode {
    
    // ...

    @Override
    public int evaluate(int x) {
        return value;
    }
}
package arithmetic;

class VariableX extends NullaryNode {

    // ...

    @Override
    public int evaluate(int x) {
        return x;
    }
}

Úloha č. 3

import java.util.*;

public class Node {
    private int value;
    private Node[] children;

    public Node(int value, Node[] children) {
        this.value = value;
        this.children = Arrays.copyOf(children, children.length);
    }

    public Node(int height, int degree, int value) {
        this(value, new Node[0]);
        if (height >= 1) {
            children = new Node[degree];
            for (int i = 0; i <= degree - 1; i++) {
                children[i] = new Node(height - 1, degree, value);
            }
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (Node node : children) {
            sb.append("(");
            sb.append(node);
            sb.append(") ");
        }
        sb.append(value);
        return sb.toString();
    }
}

Letný semester, prednáška č. 5

Oznamy

  • Dnes po prednáške bude zverejnené zadanie druhej domácej úlohy, ktorú bude potrebné odovzdať do pondelka 29. marca, 9:00 (čiže do začiatku siedmej prednášky).
  • Na stredajších cvičeniach bude – okrem niekoľkých nebodovaných úloh zameraných na látku z tejto a minulej prednášky – zverejnené aj zadanie druhej bonusovej úlohy s odovzdávaním do stredy 24. marca, 11:30 (čiže najneskôr do začiatku šiestych cvičení).

Java Collections

Detailnejšie teraz preskúmame štandardné dátové štruktúry implementované v balíku java.util a známe pod súhrnným názvom Java Collections. Na nasledujúcom obrázku je znázornený diagram niekoľkých spomedzi najdôležitejších tried a rozhraní, ktoré sú súčasťou Java Collections. Plná šípka v tomto diagrame reprezentuje dedenie (aj medzi rozhraniami) a prerušovaná šípka znázorňuje implementáciu rozhrania triedou.

Collections.png

Rozhranie Collection

Veľká časť tried pre dátové štruktúry, ktoré sú súčasťou Java Collections – presnejšie triedy reprezentujúce zoskupenia objektov nejakého typu E – implementuje generické rozhranie Collection<E>. Metódy tohto rozhrania sú tak napríklad poskytované implementáciami zoznamov, či množín. Medzi najdôležitejšie spomedzi metód deklarovaných v rozhraní Collection<E> patria nasledujúce:

  • Metóda boolean contains​(Object o) vráti true práve vtedy, keď dané zoskupenie objektov obsahuje objekt o.
  • Metóda boolean add​(E e) pridá inštanciu e typu E do zoskupenia objektov a vráti booleovskú hodnotu podľa toho, či po vykonaní tejto operácie bolo dané zoskupenie zmenené (napríklad u zoznamov by to tak malo byť vždy; naopak množina sa nezmení v prípade, že je do nej pridaný prvok, ktorý už obsahuje). Pri jednotlivých implementáciách rozhrania Collection<E> môže byť správanie tejto metódy bližšie určené: napríklad u zoznamov sa typicky pridávajú prvky na koniec. Táto metóda je označená ako nepovinná, čo znamená, že niektoré implementácie rozhrania Collection<E> ju môžu implementovať iba ako vyhodenie výnimky typu UnsupportedOperationException.
  • Metóda boolean remove​(Object o) odoberie zo zoskupenia jeden výskyt argumentu o. Opäť ide o nepovinnú metódu, ktorej správanie môže byť v jednotlivých implementáciách rozhrania bližšie špecifikované. (Napríklad pri zoznamoch väčšinou býva užitočnejšia iná verzia metódy remove, ktorá odoberie prvok na danom indexe; táto sa ale v rozhraní pre všeobecné zoskupenia objektov nedeklaruje.)
  • Metóda int size() vráti veľkosť daného zoskupenia objektov.
  • Metóda boolean isEmpty() zistí, či je dané zoskupenie objektov prázdne.
  • Metóda Iterator<E> iterator() vráti tzv. iterátor, ktorý možno použiť na postupné prechádzanie cez všetky prvky daného zoskupenia. Ide tu o dôležitý koncept, pri ktorom sa ešte bližšie pristavíme nižšie.

Použitie metódy equals

Je ešte potrebné ujasniť si, čo napríklad pri metóde contains znamená, že zoskupenie obsahuje objekt o, prípadne kedy sa pri metóde remove nejaký objekt považuje za výskyt jej argumentu. V obdivoch prípadoch sa na porovnávanie objektov používa metóda equals, ktorá porovná dva objekty na rovnosť. S touto metódou sme sa už stretli napríklad pri reťazcoch alebo pri „baliacich” triedach. Ide však o metódu, ktorá je podobne ako toString definovaná už v triede Object – jej východzou implementáciou je porovnávanie referencií pomocou operátora == – a v iných triedach môže byť prekrytá prirodzenejšou implementáciou. Viacero štandardných tried (napr. String a Integer) túto metódu vhodným spôsobom prekrýva.

  • Programátor prekrytej metódy equals by mal vždy zabezpečiť, aby išlo o reláciu ekvivalencie (reflexívnu, symetrickú a súčasne tranzitívnu reláciu).
  • Súčasne by sa výstupy metódy equals pre danú dvojicu objektov v ideálnom prípade nemali meniť v čase. To v praxi znamená, že prekrývanie metódy equals má význam hlavne pri triedach, ktorých inštancie reprezentujú nemodifikovateľné dáta. Napríklad String metódu equals zdedenú od triedy Object vhodným spôsobom prekrýva, ale StringBuilder nie. Hoci prekrytie metódy equals spôsobom porušujúcim túto podmienku nie je striktne zakázané (príkladom môže byť napríklad jej implementácia práve pre väčšinu zoskupení Java Collections), môže použitie takýchto objektov ako prvkov zoskupení často viesť k neočakávaným výsledkom. V takom prípade je teda často potrebné zabezpečiť, aby sa modifikovateľné prvky zoskupení nemodifikovali.
  • Dátové štruktúry, ktoré sú súčasťou Java Collections, sa na tieto vlastnosti metódy equals spoliehajú – iba za ich splnenia je teda garantované, že sa budú správať očakávaným spôsobom.

Príklad: nasledujúca trieda reprezentuje bod v rovine, pričom dva body sa považujú za rovné kedykoľvek sa rovnajú obidve ich súradnice.

public class Point {
    private double x, y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        return o.getClass() == this.getClass() && ((Point) o).x == x && ((Point) o).y == y;
    }
}
public class Trieda {

    public static void main(String[] args) {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);
        System.out.println(p1 == p2);       // false
        System.out.println(p1.equals(p2));  // true
    }
}

Zoznamy

Triedy pre zoznamy sú implementáciami rozhrania List<E>.

  • Prvok na koniec zoznamu pridáva metóda add, prvok na danej pozícii dostaneme metódou get, metóda set mení prvok na danej pozícii na určenú hodnotu a dĺžku zoznamu vracia metóda size.

Dvoma najdôležitejšími triedami implementujúcimi rozhranie List<E> sú:

  • Trieda ArrayList<E> reprezentujúca zoznamy implementované pomocou dynamických polí, ktoré dokážu automaticky meniť objem alokovanej pamäte.
Príklad:
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        ArrayList<Integer> a = new ArrayList<>();
        for (int i = 0; i <= 9; i++) {
            a.add(i);
        }
        for (int i = 0; i <= a.size() - 1; i++) {
            a.set(i, i + 1);
        }
        for (int i = 0; i <= a.size() - 1; i++) {
            System.out.print(a.get(i) + " ");
        }
    }
}
  • Trieda LinkedList<E> reprezentujúca obojsmerne spájaný zoznam.

Každá z týchto dvoch implementácií zoznamov je zameraná na iné operácie, ktoré sú v nej efektívnejšie – z minulého semestra napríklad vieme, že prístup k prvku na konkrétnej pozícii zoznamu je omnoho efektívnejší u polí, zato pri spájaných zoznamoch sa rýchlejšie pridávajú a odoberajú nové prvky (napríklad) na začiatku alebo konci.

  • Kopírovanie zoznamov možno realizovať napríklad pomocou konštruktorov, ktoré ako argument berú iné zoskupenie:
ArrayList<Integer> a1 = new ArrayList<>();

// ... 

ArrayList<Integer> a2 = new ArrayList<>(a1);
LinkedList<Integer> list = new LinkedList<>(a1);

Rad a objostranný rad

  • Trieda LinkedList<E> okrem rozhrania List<E> implementuje aj rozhranie Queue<E> reprezentujúce rady (fronty). Na koniec radu pridávame metódou add rovnako ako u zoznamov; metóda remove (bez parametrov) vyberie a vráti na výstupe prvok zo začiatku radu; metóda peek vráti prvok na začiatku radu bez toho, aby ho z radu vybrala.
  • Okrem toho trieda LinkedList<E> implementuje aj rozhranie Deque<E> (z angl. double-ended queue) pre obojstranné rady, v ktorých možno prvky pridávať a vyberať z obidvoch strán. Toto rozhranie okrem iného deklaruje metódy push a pop umožňujúce pracovať s ním ako so zásobníkom.
  • Javovské spájané zoznamy typu LinkedList<E> teda možno použiť aj ako rad, aj ako zásobník.

Množiny a použitie metódy hashCode

Triedy pre množiny sú implementáciami rozhrania Set<E> a pri práci s nimi sú podstatné predovšetkým metódy add, contains a remove. Najdôležitejšou implementáciou množín je trieda HashSet<E>:

  • Ide o implementáciu množín pomocou hešovania.
  • Pri hešovaní sa využíva metóda hashCode vracajúca pre daný objekt nejakú celočíselnú hodnotu, na základe ktorej sa pre tento objekt počíta index do hešovacej tabuľky. Táto metóda je podobne ako metóda equals definovaná už v triede Object a viaceré štandardné triedy ju prekrývajú. Napríklad String vráti ako výstup tejto metódy číslo, ktoré vznikne sčítaním hodnôt jednotlivých znakov prenásobených rôznymi mocninami čísla 31.
  • Opäť by malo ísť o metódu, ktorá pre ľubovoľný objekt zakaždým vráti rovnakú hodnotu, takže jej prekrývanie má obyčajne zmysel hlavne pri triedach, ktorých inštancie reprezentujú nemodifikovateľné dáta.
  • Ďalšou požiadavkou je, aby pre ľubovoľnú dvojicu objektov, pre ktoré vráti metóda equals hodnotu true, vrátila metóda hashCode rovnaké výstupy. To znamená, že metódu hashCode je žiadúce prekryť kedykoľvek je prekrytá metóda equals.
  • Pre dvojice objektov, pre ktoré equals vráti hodnotu false, metóda hashCode nemusí vrátiť rôzne hodnoty – implementácia by sa ale mala snažiť o čo najmenšiu pravdepodobnosť takejto situácie, aby boli prvky v hešovacej tabuľke rozdelené čo najrovnomernejšie.

Príklad:

public class Name {
    private String givenName;
    private String lastName;

    public Name(String givenName, String lastName) {
        this.givenName = givenName;
        this.lastName = lastName;
    }

    public String getGivenName() {
        return givenName;
    }

    public String getLastName() {
        return lastName;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        return o.getClass() == this.getClass() && ((Name) o).givenName.equals(givenName)
                && ((Name) o).lastName.equals(lastName);
    }

    @Override
    public int hashCode() {
        return givenName.hashCode() + 31 * lastName.hashCode();
    }
}
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Set<Name> set = new HashSet<>();
        while (scanner.hasNext()) {
            String command = scanner.next();
            String givenName = scanner.next();
            String lastName = scanner.next();
            switch (command) {
                case "ADD":
                    set.add(new Name(givenName, lastName));
                    break;
                case "CONTAINS":
                    System.out.println(set.contains(new Name(givenName, lastName)));
                    break;
                case "REMOVE":
                    set.remove(new Name(givenName, lastName));
                    break;
            }
        }
    }
}

Cvičenie: zakomentujte v kóde triedy Name prekrytú metódu hashCode a nájdite príklad vstupu, pre ktorý sa v metóde main na konzolu vypíšu neočakávané výsledky.

Iterátory

Všetky triedy implementujúce rozhranie Collection<E> implementujú aj rozhranie Iterable<E>. Rozhranie Iterable<T> pritom deklaruje jedinú povinnú metódu Iterator<T> iterator(), ktorej výstupom je iterátor – objekt umožňujúci postupne prechádzať cez nejakú sadu prvkov typu T. Pri triedach implementujúcich Collection<E> je výstupom metódy iterator() iterátor, pomocou ktorého možno postupne prechádzať cez všetky prvky daného zoskupenia objektov.

  • Zoznamy iterátor prechádza od začiatku po koniec.
  • Napríklad pri množinách typu HashSet ale nie je určené žiadne špeciálne poradie, v ktorom iterátor prechádza cez ich prvky.

Samotným iterátorom cez prvky typu E je pritom inštancia ľubovoľnej triedy implementujúcej rozhranie Iterator<E>. To deklaruje predovšetkým nasledujúce tri metódy:

  • Metóda E next() vráti nasledujúci spomedzi prvkov, cez ktoré iterátor prechádza. Ak už žiaden ďalší prvok neexistuje, vyhodí výnimku typu NoSuchElementException.
  • Metóda boolean hasNext() vráti true, ak ešte existuje nejaký ďalší prvok – čiže v prípade, že nasledujúce volanie metódy next nevyhodí výnimku.
  • Nepovinná metóda default void remove() z prechádzaného zoskupenia odoberie prvok, ktorý bol dosiaľ posledným výstupom metódy next. Túto metódu nemusia implementovať všetky triedy implementujúce rozhranie Iterator<E> – v takom prípade sa pri pokuse o jej volanie použije východzia implementácia spočívajúca vo vyhodení výnimky typu UnsupportedOperationException.

Iterátor si teda možno predstaviť tak, že v každom momente svojej existencie ukazuje na medzeru medzi nejakými dvoma prechádzanými prvkami (resp. na pozíciu pred prvým alebo za posledným prvkom). Po zavolaní metódy next iterátor preskočí ďalší prvok a tento prvok vráti na výstupe.

Iterator.png

Iterátory sú užitočné napríklad v situáciách, keď je potrebné prejsť všetky prvky nejakého zoskupenia, avšak samotné toto zoskupenie nepotrebujeme výraznejšie meniť (inak ako metódou remove iterátora). Počas iterovania samozrejme môžeme meniť dáta uložené v prechádzaných objektoch; obmedzenie sa týka iba modifikácie referencií na objekty v zoskupení uložených.

Príklad: Vypísanie všetkých prvkov spájaného zoznamu celých čísel list môžeme pomocou iterátora realizovať napríklad nasledovne:

LinkedList<Integer> list = new LinkedList<>();

// ...

for (Iterator<Integer> it = list.iterator(); it.hasNext(); ) {
    System.out.print(it.next() + " ");
}

Takýto cyklus je pre spájané zoznamy omnoho efektívnejší ako cyklus

for (int i = 0; i <= list.size() - 1; i++) {
    System.out.print(list.get(i) + " ");
}

v ktorom je potrebné pre všetky i vykonať pomalú operáciu get(i). Pre ArrayList je naopak efektívnosť oboch cyklov porovnateľná.

Rozhranie List<E> okrem metódy iterator deklaruje aj metódu listIterator, ktorá vráti „vylepšený” iterátor implementujúci rozhranie ListIterator<E>. Takýto iterátor umožňuje pohybovať sa po zozname obidvoma smermi.

Cyklus for each a iterátory

Cez prvky inštancií iterable tried implementujúcich rozhranie Iterable<T> možno, podobne ako pri poliach, prechádzať pomocou cyklu for each:

for (T x : iterable) {
    // ...
}

V takom prípade je (na rozdiel od polí) tento cyklus ekvivalentný cyklu

for (Iterator<T> it = iterable.iterator(); it.hasNext(); ) {
    T x = it.next();
    // ...
}

(za predpokladu, že it je identifikátor rôzny od všetkých identifikátorov použitých v pôvodnom programe).

Definovanie vlastných iterátorov

Príklad: nasledujúca generická trieda MyArrayList<E> sa od bežného ArrayList-u líši iba implementáciou metódy iterator. Tá vráti iterátor novodefinovaného typu NullSkippingArrayListIterator, ktorý pri prechádzaní prvkov ArrayList-u vynecháva všetky výskyty referencie null. Takéto prekrytie metódy iterator v podtriede ArrayList-u nie je dobrá prax, keďže sa ním poruší špecifikácia metódy iterator v rozhraní List<E>; krajšie by bolo napríklad pridať novú metódu nullSkippingIterator za súčasného ponechania pôvodnej metódy iterator. V nasledujúcom ale ide aj o ukážku toho, že prekrytie metódy iterator má vplyv na správanie cyklu for each.

import java.util.*;

public class NullSkippingArrayListIterator<E> implements Iterator<E> {
    private ArrayList<E> a;
    private int nextIndex;

    public NullSkippingArrayListIterator(ArrayList<E> a) {
        this.a = a;
        skipNullValues();
    }

    private void skipNullValues() {
        while (nextIndex <= a.size() - 1 && a.get(nextIndex) == null) {
            nextIndex++;
        }
    }

    @Override
    public boolean hasNext() {
        return nextIndex <= a.size() - 1;
    }

    @Override
    public E next() {
        E result = null;
        if (hasNext()) {
            result = a.get(nextIndex++);
        } else {
            throw new NoSuchElementException();
        }
        skipNullValues();
        return result;
    }
}
import java.util.*;

public class MyArrayList<E> extends ArrayList<E> {

    @Override
    public Iterator<E> iterator() {
        return new NullSkippingArrayListIterator<>(this);
    }
}
public class Trieda {

    public static void main(String[] args) {
        MyArrayList<Integer> a = new MyArrayList<>();
        a.add(1);
        a.add(null);
        a.add(2);
        a.add(null);
        a.add(null);
        a.add(3);
        a.add(null);
        for (Integer x : a) {
            System.out.print(x + " ");
        }
        System.out.println(); 
        System.out.println(a); // Zmena sa prejavi aj tu, pretoze toString pre ArrayList vyuziva iterator.
    }
}

Zobrazenia

Rozhranie Map<K, V> reprezentuje zobrazenia, ktoré priraďujú nejakej množine kľúčov (angl. keys) typu K ich obrazy (resp. hodnoty, angl. values) typu V. Najdôležitejšou implementáciou tohto rozhrania je trieda HashMap založená na hešovaní. Kľúčom aj jeho obrazom tu môže byť aj null. Podobne ako HashSet využíva táto trieda metódy hashCode a equals pre jednotlivé kľúče. Niektoré poskytované metódy:

  • Metóda public V get​(Object key) vráti obraz kľúča key pri danom zobrazení, ak je definovaný; v opačnom prípade vráti null.
  • Metóda public V put​(K key, V value) nastaví obraz kľúča key na hodnotu value.
  • Metóda public boolean containsKey​(Object key) zistí, či je key kľúčom, t. j. či je preň definovaný obraz v danom zobrazení.
  • Metóda public Set<K> keySet() vráti množinu všetkých kľúčov.
  • Metóda public int size() vráti počet všetkých dvojíc kľúč/obraz v danom zobrazení.
  • ...

Príklad: nasledujúci program spočíta počty výskytov slov v nejakom vstupnom texte (a na konci ich vypíše v nejakom ľubovoľnom poradí).

import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String word = scanner.next();
            if (map.containsKey(word)) {
                map.put(word, map.get(word) + 1);
            } else {
                map.put(word, 1);
            }
        }
        System.out.println("Pocet roznych slov: " + map.size() + ".");
        for (String word : map.keySet()) {  
            System.out.println("Pocet vyskytov slova \"" + word + "\": " + map.get(word) + ".");
        }
    }
}

Usporiadané množiny a zobrazenia na nich

Java Collections obsahuje aj rozhrania SortedSet<E> resp. SortedMap<K, V> rozširujúce rozhrania Set<E> resp. Map<K, V>. Ide o reprezentácie úplne usporiadaných množín resp. zobrazení s úplným usporiadaním množiny kľúčov. Najdôležitejšími implementáciami týchto rozhraní sú triedy TreeSet<E> resp. TreeMap<K, V>.

  • V základnom variante – to jest pri použití konštruktora triedy TreeSet alebo TreeMap bez parametrov – sa predpokladá, že všetky dvojice prvkov, ktoré budú pridávané do množiny resp. budú zohrávať úlohu kľúčov pri zobrazení, sú navzájom porovnateľné pomocou metódy compareTo (použije sa teda tzv. prirodzené usporiadanie prvkov). Typicky sa teda TreeSet<E> a TreeMap<K, V> používajú v prípade, keď typ E resp. K implementuje rozhranie Comparable<E> resp. Comparable<K>. Nie je to ale striktne vyžadované: pomocou konštruktora bez parametrov technicky môžeme vytvoriť napríklad aj inštanciu typu TreeSet<Object>. Akonáhle však do takejto množiny pridáme dvojicu neporovnateľných objektov, dôjde k vyhodeniu výnimky typu ClassCastException.
  • Od prirodzeného usporiadania sa tu vyžaduje, aby bolo konzistentné s metódou equals. Pre všetky dvojice objektov e1, e2 daného typu by teda mal mať logický výraz e1.compareTo(e2) == 0 vždy tú istú booleovskú hodnotu ako e1.equals(e2). Štandardné triedy implementujúce rozhranie Comparable, ako napríklad Integer alebo String, túto požiadavku spĺňajú.
  • Pri iterovaní cez usporiadanú množinu, ako aj cez množinu kľúčov zobrazenia typu SortedMap, sa prvky prechádzajú vo vzostupnom poradí (čo môže byť užitočné napríklad aj v našom príklade s počítaním výskytov slov).

Komparátory

Alternatívne možno pri triedach TreeSet<E> a TreeMap<K, V> namiesto prirodzeného usporiadania prvkov použiť aj novodefinované usporiadanie (bez ohľadu na to, či na prvkoch daného typu existuje alebo neexistuje prirodzené usporiadanie). V takom prípade vytvoríme inštanciu TreeSet<E> resp. TreeMap<K, V> pomocou konštruktora, ktorý ako argument berie tzv. komparátor prvkov typu E resp. K (alebo ich nadtried).

Pod komparátorom prvkov typu T rozumieme ľubovoľnú inštanciu triedy implementujúcej rozhranie Comparator<T>.

  • Toto rozhranie vyžaduje povinnú implementáciu jedinej metódy – a to metódy int compare​(T o1, T o2) na porovnanie dvojice prvkov typu T.
  • Výstupom tejto metódy má byť záporné číslo, nula, resp. kladné číslo podľa toho, či je prvý argument menší, rovnako veľký, alebo väčší ako druhý argument.
  • Od programátora sa vyžaduje splnenie zvyčajných vlastností úplného usporiadania alebo aspoň úplného predusporiadania (detaily možno nájsť v dokumentácii). Pri použití v TreeSet a TreeMap sa rovnako ako pri prirodzenom usporiadaní vyžaduje konzistencia s metódou equals.

Príklad:

import java.util.*;

public class DualComparator implements Comparator<Integer> {

    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }
}
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>(new DualComparator());
        set.add(0);
        set.add(1);
        set.add(2);
        set.add(3);
        for (Integer x : set) {
            System.out.print(x + " ");  // Vypise 3 2 1 0
        }
    }
}

Trieda Collections

Trieda Collections obsahuje viacero užitočných statických metód na prácu s Java Collections (ide o akúsi obdobu triedy Arrays pre polia). Sú v nej definované napríklad:

  • Metódy sort na utriedenie zoznamu (v dvoch verziách: s použitím prirodzeného usporiadania v prípade, že typ E prvkov zoznamu implementuje Comparable<E>, ako aj s použitím komparátora).
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        List<Integer> a = new ArrayList<>();
        a.add(6);
        a.add(1);
        a.add(3);
        a.add(2);
        a.add(3);
        Collections.sort(a);
        System.out.println(a);
        Collections.sort(a, new DualComparator());
        System.out.println(a);
    }
}
  • Metódy shuffle na náhodné premiešanie zoznamu.
  • Metódu reverse na otočenie zoznamu.
  • ...

Vnorené, lokálne a anonymné triedy

Triedy pre iterátory a komparátory – napríklad tie, ktoré sme tvorili vyššie – sú dosť často triedami „na jedno použitie”. Takéto triedy, ktoré sú viac-menej limitované na použitie v jednej inej triede alebo metóde, môžu pomerne značne zneprehľadňovať štruktúru celého projektu. V podobných situáciách je preto lepšie využiť črtu jazyka Java, s ktorou sme sa doposiaľ nestretli: možnosť tvoriť triedy, ktoré sú súčasťou iných tried alebo dokonca metód. Obmedzíme sa tu len na nutné základy tejto problematiky – viac sa možno dočítať tu.

Statické vnorené triedy

Trieda definovaná vo vnútri inej triedy sa v Jave nazýva vnorenou triedou (angl. nested class). Takáto trieda môže alebo nemusí byť statická. V prípade, že statická je, správa sa táto trieda veľmi podobne ako ľubovoľná iná trieda – k statickej vnorenej triede StatickaVnorenaTrieda definovanej v triede VonkajsiaTrieda ale mimo triedy VonkajsiaTrieda pristupujeme cez VonkajsiaTrieda.StatickaVnorenaTrieda. Užitočnou novinkou je možnosť nastaviť vnoreným triedam prístupový modifikátor private, čím sa trieda stane viditeľnou iba z vonkajšej triedy, v ktorej je definovaná.

public class VonkajsiaTrieda {

    private static class StatickaVnorenaTrieda1 {
        // ...
    }

    public static class StatickaVnorenaTrieda2 {
        // ...
    }

    public void metoda() {
        StatickaVnorenaTrieda1 o1 = new StatickaVnorenaTrieda1();
        StatickaVnorenaTrieda2 o2 = new StatickaVnorenaTrieda2();
    }
}
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        VonkajsiaTrieda.StatickaVnorenaTrieda2 x = new VonkajsiaTrieda.StatickaVnorenaTrieda2();
    }
}

Statickosť vnorených tried znamená predovšetkým to, že k premenným a metódam inštancií triedy, v ktorej sú definované, pristupujú v podstate ako ktorákoľvek iná trieda (t. j. iba prostredníctvom tvorby inštancií vonkajšej triedy).

Vnútorné triedy

Nestatické vnorené triedy sa v Jave nazývajú aj vnútornými triedami (angl. inner classes). Tieto triedy patria jednotlivým inštanciám vonkajšej triedy a majú tak prístup k ich premenným a metódam.

Ukážka:

public class VonkajsiaTrieda {
    private int x, y;

    private class VnutornaTrieda1 {
        private int y;

        public void f() {
            y = x + VonkajsiaTrieda.this.y; // Premenna y vnutornej triedy skryva metodu vonkajsej triedy.
        }

        public int getY() {
            return y;
        }
    }

    public class VnutornaTrieda2 {

        public void vypis() {
            System.out.println("Som vnutorna trieda.");
        }
    }

    public void metoda() {
        VnutornaTrieda1 o1 = new VnutornaTrieda1();
        x = 2;
        y = 3;
        o1.f();
        System.out.println(o1.getY());
        VnutornaTrieda2 o2 = new VnutornaTrieda2();
    }
}
public class Trieda {

    public static void main(String[] args) {
        VonkajsiaTrieda o = new VonkajsiaTrieda();
        o.metoda();
        VonkajsiaTrieda.VnutornaTrieda2 o2 = o.new VnutornaTrieda2();
        o2.vypis();
    }
}

Lokálne triedy

Dôležitejšie, než vnútorné triedy pre nás budú tzv. lokálne triedy (angl. local classes). Tie sú podobným konceptom ako vnútorné triedy, avšak definujú sa vo vnútri metódy (resp. bloku) a pod svojím názvom sú prístupné iba tam.

  • Lokálne triedy sa definujú vo vnútri bloku medzi ostatnými príkazmi, ale inak je ich definícia veľmi podobná definícii ktorejkoľvek inej triedy. Keďže sú tieto triedy viditeľné iba v rámci daného bloku, nepíše sa pred kľúčové slovo class žiaden modifikátor prístupu.
  • Inštancie týchto tried sú použiteľné aj mimo bloku, v ktorom je lokálna trieda definovaná (za ich typ tam možno považovať napríklad nejakú nelokálnu nadtriedu lokálnej triedy alebo implementované rozhranie).
  • Z lokálnych tried možno pristupovať aj k lokálnym premenným metódy, v ktorej je táto trieda definovaná. Avšak to iba za predpokladu, že sú tieto premenné finálne alebo „v podstate finálne” (angl. effectively final), to jest po inicializácii sa už ďalej nemení ich hodnota. Pokiaľ je touto premennou objekt, znamená táto požiadavka nemenenie referencie; dáta, na ktoré referencia ukazuje, sa meniť môžu. Lokálne premenné, ku ktorým sa z lokálnej triedy pristupuje, samozrejme musia byť definované ešte pred definíciou danej lokálnej triedy.
  • Lokálne triedy sú tak vcelku užitočným nástrojom na tvorbu „jednorazových” tried pre iterátory alebo komparátory.

Príklad: Triedu NullSkippingArrayListIterator<E>, ktorú sme vracali ako iterátor v triede MyArrayList<E>, môžeme napísať ako lokálnu triedu v metóde iterator triedy MyArrayList<E>.

import java.util.*;

public class MyArrayList<E> extends ArrayList<E> {

    @Override
    public Iterator<E> iterator() {
        class NullSkippingArrayListIterator<E> implements Iterator<E> {
            private int nextIndex;

            public NullSkippingArrayListIterator() {
                skipNullValues();
            }

            private void skipNullValues() {
                while (nextIndex <= MyArrayList.this.size() - 1 && MyArrayList.this.get(nextIndex) == null) {
                    nextIndex++;
                }
            }

            @Override
            public boolean hasNext() {
                return nextIndex <= MyArrayList.this.size() - 1;
            }

            @Override
            public E next() {
                E result = null;
                if (hasNext()) {
                    result = (E) MyArrayList.this.get(nextIndex++);
                } else {
                    throw new NoSuchElementException();
                }
                skipNullValues();
                return result;
            }
        }

        return new NullSkippingArrayListIterator<>();
    }
}

Príklad: Komparátor DualComparator ako lokálna trieda.

import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        class DualComparator implements Comparator<Integer> {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        }
        List<Integer> a = new ArrayList<>();
        a.add(6);
        a.add(1);
        a.add(3);
        a.add(2);
        a.add(3);
        Collections.sort(a, new DualComparator());
        System.out.println(a);
    }
}

Anonymné triedy

Ak sa aj v rámci metódy inštancia lokálnej triedy vytvára iba raz, je často užitočné spojiť definíciu lokálnej triedy a vytvorenie jej inštancie do jedného príkazu. To umožňuje mechanizmus tzv. anonymných tried (angl. anonymous classes). Takáto lokálna trieda ani nemá svoj názov, vytvorí sa iba jej inštancia. Syntax vytvorenia inštancie anonymnej triedy je nasledujúca:

  • Za kľúčovým slovom new nasleduje volanie konštruktora nadtriedy, prípadne (častejšie) názov implementovaného rozhrania nasledovaný prázdnymi zátvorkami (akoby „konštruktor bez parametrov”, akurát v tomto prípade ide o rozhranie).
  • Následne sa uvedie samotná definícia triedy.
  • Za uzatváracou zloženou zátvorkou } definície triedy sa uvedie bodkočiarka, ktorou sa ukončí príkaz na vytvorenie nového objektu (prípadne príkaz obsahujúci new pokračuje iným spôsobom).

Anonymné triedy sú oproti lokálnym triedam obmedzené v tom, že pre ne nemožno písať konštruktory. Možno ale inicializovať ich premenné na dané hodnoty.

Príklad: Iterátor pre MyArrayList ako anonymná trieda.

import java.util.*;

public class MyArrayList<E> extends ArrayList<E> {

    @Override
    public Iterator<E> iterator() {
        return new Iterator<>() {
            private int nextIndex;

            private void skipNullValues() {
                while (nextIndex <= MyArrayList.this.size() - 1 && MyArrayList.this.get(nextIndex) == null) {
                    nextIndex++;
                }
            }

            @Override
            public boolean hasNext() {
                skipNullValues();
                return nextIndex <= MyArrayList.this.size() - 1;
            }

            @Override
            public E next() {
                skipNullValues();
                E result = null;
                if (hasNext()) {
                    result = (E) MyArrayList.this.get(nextIndex++);
                } else {
                    throw new NoSuchElementException();
                }
                skipNullValues();
                return result;
            }
        }; 
    }
}

Alternatívne by sme mohli metódu skipNullValues prepísať tak, aby vracala int a jej výstup použiť na inicializáciu premennej nextIndex; v takom prípade by už volania metódy skipNullValues na začiatku metód next a hasNext neboli nutné.

Príklad: Komparátor ako anonymná trieda.

import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        List<Integer> a = new ArrayList<>();
        a.add(6);
        a.add(1);
        a.add(3);
        a.add(2);
        a.add(3);
        Collections.sort(a, new Comparator<>(){
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        System.out.println(a);
    }
}

V tomto prípade dokonca existuje ešte podstatne kratší zápis pomocou tzv. lambda výrazov – nimi sa budeme zaoberať neskôr v priebehu tohto semestra.

Letný semester, prednáška č. 6

Oznamy

  • Počas najbližších cvičení, čiže v stredu 24. marca od 11:30 do 13:00 bude prebiehať druhý test. Bude pozostávať z troch úloh zameraných na látku z prvých piatich týždňov, s dôrazom na látku zo štvrtého a piateho týždňa. Po dobu riešenia testu je potrebná účasť na stretnutí „Cvičenia” v MS Teams.
  • Druhú bonusovú úlohu je potrebné odovzdať do stredy 24. marca, 11:30 (čiže do začiatku stredajších cvičení).
  • Druhú domácu úlohu je potrebné odovzdať do pondelka 29. marca, 9:00 (čiže do začiatku siedmej prednášky).

Tvorba dokumentácie: Javadoc

Systém Javadoc umožňuje automatické generovanie dokumentácie k balíku, triede a pod. v štandardnom formáte, aký používa aj dokumentácia k Java API. Program javadoc je štandardnou súčasťou Javy a možno ho nájsť v rovnakom priečinku ako kompilátor javac a interpreter java. Javadoc dokumentáciu generuje na základe komentárov pri jednotlivých triedach, metódach, premenných, atď. Tieto komentáre musia byť v nasledujúcom špeciálnom formáte:

  • Komentár musí byť ohraničený značkami /** a */ (na začiatku sú teda dve hviezdičky namiesto bežnej jednej) a každý riadok komentára sa tiež musí začínať hviezdičkou.
  • Musí byť umiestnený bezprostredne pred triedou, metódou a pod., ku ktorej sa vzťahuje.
  • Časť textu komentára končiaca prvou bodkou je stručné zhrnutie, ktoré sa uvádza aj v prehľadoch metód, tried, atď.; v samotnej dokumentácii danej triedy resp. metódy sa potom uvádza kompletný text.
  • Ako súčasť Javadoc komentára možno (nepovinne) použiť niekoľko špecializovaných značiek – napríklad @param pre opis parametra metódy, @return pre opis výstupu metódy, @throws pre opis vyhadzovaných výnimiek, atď.

Príklad:

/**
 * Trieda obsahujuca velmi dolezite a zmysluplne metody na pracu s celymi cislami. Napriklad druhu mocninu a v buducich
 * verziach mozno aj scitanie.
 */
public class Trieda {

    /**
     * Metoda pocitacuja druhu mocninu celociselneho vstupneho parametra. Vypocet je implementovany prenasobenim
     * vstupneho cisla so sebou samym.
     *
     * @param n Celociselny vstup.
     * @return  Druha mocnina cisla n.
     */
    public int sqr(int n) {
        return n * n;
    }

    /**
     * Metoda na scitanie dvojice celociselnych vstupov. Je implementovana ako vyhodenie vynimky typu Exception.
     *
     * @param n Prvy vstupny parameter.
     * @param m Druhy vstupny parameter.
     * @return  Nepodstatne...
     * @throws Exception V pripade nespravneho pouzitia vyhodi vynimku typu Exception.
     */
    public int add(int n, int m) throws Exception {
        throw new Exception();
    }

}

Pre triedu alebo balík tried s komentármi vo formáte Javadoc možno pomocou programu javadoc vygenerovať samotnú dokumentáciu vo formáte HTML.

  • Z príkazového riadku sa tak dá urobiť volaním programu javadoc s vhodnými parametrami (treba sa nastaviť do adresára obsahujúceho priečinok s daným balíkom resp. danú triedu):
javadoc balik
javadoc Trieda.java
Toto volanie programu javadoc často vygeneruje pomerne veľké množstvo súborov. Je preto užitočné nastaviť priečinok, do ktorého sa má dokumentácia vygenerovať, pomocou možnosti -d.
javadoc -d doc balik
javadoc -d doc Trieda.java
  • Z IntelliJ cez Tools --> Generate JavaDoc...

Pri východzích nastaveniach sa dokumentácia vytvára iba pre položky s modifikátorom prístupu public alebo protected, pretože predovšetkým tieto tvoria API pre ostatné triedy. V prípade potreby možno toto správanie zmeniť:

  • Z príkazového riadku použitím jedného z prepínačov -private, -package, -protected (ekvivalentné nepoužitiu žiadneho prepínača), alebo -public. V takom prípade sa dokumentácia vygeneruje ku všetkým položkám s príslušným alebo „verejnejším” modifikátorom prístupu.
  • V IntelliJ možno tieto nastavenia meniť v dialógovom okne, ktoré sa zobrazí pred vygenerovaním dokumentácie.

Testovanie programov

Pod testovaním sa rozumie systematický spôsob hľadania chýb v programe spočívajúci vo vytvorení niekoľkých testov pozostávajúcich zo vstupov a očakávaných výstupov na týchto vstupoch; program je následne (podobne ako na testovači) spustený na každom zo vstupov a jeho výstupy sú konfrontované s očakávanými.

  • Cieľom testovania je teda preukázať, že program nepracuje podľa špecifikácie (aby sme potom vedeli chybu nájsť a opraviť).
  • Ak program prejde všetkými testmi, nejde v žiadnom prípade o dôkaz toho, že program pracuje správne. Dokázať správnosť programu možno iba pomocou metód formálnej verifikácie, ktoré značne presahujú rámec tohto predmetu (a pre ich výpočtovú náročnosť sa v súčasnosti uplatňujú zvyčajne iba pri tvorbe kritických aplikácií).
  • Už aj dobre navrhnutými testmi ale možno početnosť chýb podstatne znížiť.

Typický proces testovania programu alebo jeho časti možno zhrnúť nasledovne:

  • Vytvorí sa niekoľko testov pozostávajúcich zo vstupu, očakávaného správneho výstupu a obyčajne aj opisu daného testu (aby bolo jasné, čo sa chce daným testom preveriť).
  • Program (resp. napríklad testovaná metóda) sa pre každý z testov spustí na príslušnom vstupe a takto získaný výstup sa porovná s očakávanou 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 a až následne sa programuje kód, ktorý ich dokáže splniť.

„White-box” testovanie

Pod „white-box” testovaním sa rozumie prístup, pri ktorom sa testy vytvárajú na základe kódu; cieľom je pritom preveriť všetky významné vetvy výpočtu.

  • Pri cykloch napríklad možno preveriť prípady, keď sa vykoná 0 iterácií, 1 iterácia, 2 iterácie, nejaký väčší pevný počet iterácií a prípadne maximálny počet iterácií (ak niečo také dáva zmysel).
  • Pri podmienkach sa preverí ako prípad, keď je táto podmienka splnená, tak aj prípad, keď je nesplnená.
  • ...

Nevýhodou tohto prístupu 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úca metóda úplne nespĺňa svoju špecifikáciu:

    /**
     * Metoda pocitajuca pocet retazcov vo vstupnom zozname retazcov, ktore obsahuju dany podretazec. Hladany
     * podretazec musi byt rozny od null. Vstupny zoznam ostane po vykonani metody nezmeneny.
     *
     * @param list      Vstupny zoznam retazcov, ktorym moze byt lubovolna instancia typu List&lt;String&gt;.
     * @param substring Podretazec, ktoreho vyskyty v retazcoch sa pocitaju.
     * @return          Pocet retazcov zo zoznamu list, ktore obsahuju podretazec substring.
     * @throws IllegalArgumentException V pripade, ze je niektory z argumentov rovny null, vznikne vynimka typu
     * IllegalArgumentException.
     */
    public static int numberOfSubstringContainingStrings(List<String> list, String substring) {
        if (list == null || substring == null) {
            throw new IllegalArgumentException();
        }
        int count = 0;
        for (String s : list) {
            if (s.contains(substring)) {
                count++;
            }
        }
        return count;
    }

„Black-box” testovanie

Pod „black-box” testovaním sa naopak 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 neformálnu špecifikáciu metódy remove rovnakú ako vyššie:

    /**
     * Metoda pocitajuca pocet retazcov vo vstupnom zozname retazcov, ktore obsahuju dany podretazec. Hladany
     * podretazec musi byt rozny od null. Vstupny zoznam ostane po vykonani metody nezmeneny.
     * 
     * @param list      Vstupny zoznam retazcov, ktorym moze byt lubovolna instancia typu List&lt;String&gt;.
     * @param substring Podretazec, ktoreho vyskyty v retazcoch sa pocitaju.
     * @return          Pocet retazcov zo zoznamu list, ktore obsahuju podretazec substring.
     * @throws IllegalArgumentException V pripade, ze je niektory z argumentov rovny null, vznikne vynimka typu
     * IllegalArgumentException.
     */
    public static int numberOfSubstringContainingStrings(List<String> list, String substring) {
        // ...
    }

K nej môžeme zhotoviť napríklad nasledujúcu sadu testovacích vstupov:

  1. Prázdny zoznam list, ľubovoľný reťazec substring.
  2. Jednoprvkový zoznam list obsahujúci iba reťazec rovný reťazcu substring.
  3. Jednoprvkový zoznam list, ktorého jediný reťazec neobsahuje podreťazec substring.
  4. Dlhší zoznam obsahujúci niekoľko reťazcov s výskytmi podreťazca substring a niekoľko reťazcov bez výskytu tohto podreťazca. Medzi reťazcami zoznamu obsahujúcimi podreťazec substring by mali byť také, ktoré tento podreťazec obsahujú na začiatku, v strede, na konci a viackrát.
  5. Reťazec substring prázdny, zoznam list ľubovoľný.
  6. Reťazec substring rovný null, zoznam list ľubovoľný.
  7. Zoznam list rovný null, reťazec substring ľubovoľný.
  8. Zoznam obsahujúci prázdne reťazce, reťazec substring ľubovoľný.
  9. Zoznam obsahujúci výskyty null, reťazec substring ľubovoľný.
  10. Veľmi dlhý zoznam náhodne vygenerovaných reťazcov, reťazec substring ľubovoľný.
  11. Zoznam veľmi dlhých náhodne vygenerovaných reťazcov, dlhý náhodne vygenerovaný reťazec substring.
  12. ...

Popri tom je žiadúce v rôznych testoch použiť zoznamy rôznych typov, napr. aspoň ArrayList a LinkedList.

JUnit

Systém JUnit umožňuje vytvárať špeciálne triedy obsahujúce testy iných tried.

  • Sadu testov možno ľahko automaticky spustiť a vyhodnotiť ich výsledky.
  • Podporované väčšinou IDE pre Javu.

JUnit nie je priamo súčasťou Javy, ale jeho verziu JUnit 4 možno nájsť napríklad aj v typickej inštalácii IntelliJ. Prostredie IntelliJ má dobrú podporu aj pre najnovšiu verziu JUnit 5, tú je ale potrebné stiahnuť pomocou nástroja Maven. V nasledujúcom používame (stále hojne rozšírenú) verziu JUnit 4. O použití JUnit z príkazového riadku sa možno dočítať napríklad tu.

V prostredí IntelliJ je pred prácou s JUnit potrebné vykonať nasledujúce úkony:

  • Cez File --> Project Structure... -> Libraries -> + -> Java pridať do projektu ako knižnice balíky hamcrest-core-1.3.jar a junit-4.12.jar, ktoré by mali byť umiestnené v podpriečinku lib koreňového priečinka inštalácie IntelliJ (čísla verzií sa v závislosti od verzie IntelliJ môžu trochu líšiť, ale podstatné je, aby verzia JUnit začínala číslom 4).
  • Vytvoriť nový priečinok (napríklad tests) na rovnakej úrovni ako src. Tento adresár je následne potrebné označiť ako koreňový pre testy pomocou možnosti Mark Directory as --> Test Sources Root z kontextovej ponuky, ktorá sa zjaví po kliknutí na adresár pravou myšou.

Samotný test pre triedu Trieda potom vytvoríme takto:

  • V zdrojovom kóde klikneme na názov triedy Trieda a použijeme klávesovú skratku Alt+Enter.
  • Zvolíme možnosť Create Test.
  • V dialógovom okne vyberieme verziu JUnit 4 a ako názov triedy zvolíme napríklad TriedaTest.
  • Po potvrdení vznikne nová trieda TriedaTest.java, ktorá bude namiesto pod priečinkom src umiestnená pod priečinkom tests.
  • (Alternatívne možno namiesto predchádzajúcich štyroch krokov jednoducho vytvoriť novú triedu pod priečinkom tests.)
  • Vo vytvorenej triede môžeme napísať niekoľko testov podobných ako v ukážke nižšie.
  • Po spustení triedy TriedaTest sa spustia všetky testy a v okne Run sa zobrazia ich výsledky.

Príklad niekoľkých testov pre metódu numberOfSubstringContainingStrings opísanú vyššie (statický import statických metód z triedy – pri použití * ide o všetky takéto metódy – znamená, že sa tieto metódy budú dať volať iba ich názvom, bez potreby uvedenia názvu triedy):

import org.junit.*;
import static org.junit.Assert.*;
import java.util.*;

public class TriedaTest {

    @Test
    public void testEmpty() {
        List<String> list = new ArrayList<>();
        String substring = "retazec";

        int expectedResult = 0;
        List<String> expectedList = new ArrayList<>();

        int result = Trieda.numberOfSubstringContainingStrings(list, substring);

        assertEquals(result, expectedResult);
        assertTrue(expectedList.equals(list));  // To iste sa da napisat aj cez assertEquals.
    }

    @Test
    public void testSubstringOnly() {
        List<String> list = new LinkedList<>();
        list.add("retazec");
        String substring = "retazec";

        int expectedResult = 1;
        List<String> expectedList = new LinkedList<>(list);

        int result = Trieda.numberOfSubstringContainingStrings(list, substring);

        assertEquals(result, expectedResult);
        assertTrue(list.equals(expectedList));
    }

    @Test(expected = IllegalArgumentException.class)
    public void testSubstringNull() {
        List<String> list = new ArrayList<>();
        list.add("retazec1");
        list.add("retazec2");
        String substring = null;

        List<String> expectedList = new ArrayList<>(list);

        Trieda.numberOfSubstringContainingStrings(list, substring);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testListNull() {
        List<String> list = null;
        String substring = "retazec";

        Trieda.numberOfSubstringContainingStrings(list, substring);
    }

    @Test
    public void testListContainsNull() {
        List<String> list = new LinkedList<>();
        list.add("retazec1");
        list.add(null);
        list.add("retazec2");
        String substring = "retazec";

        int expectedResult = 2;
        List<String> expectedList = new LinkedList<>(list);

        int result = Trieda.numberOfSubstringContainingStrings(list, substring);

        assertEquals(result, expectedResult);
        assertTrue(list.equals(expectedList));
    }

}

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 v triede ProgTest definovať premenné, konštruktor, ako aj špeciálne metódy, ktoré sa vykonajú pred každým testom, prípadne po každom teste.
  • ...

Grafy: úvod

Počas nasledujúcich niekoľkých týždňov sa budeme venovať práci s grafmi a implementácii jednoduchých grafových algoritmov.

  • Na tomto predmete nebudeme grafy definovať matematicky – to je náplň predmetu „Úvod do kombinatoriky a teórie grafov”. Namiesto toho si vystačíme s intuitívnym chápaním vysvetleným nižšie.
  • Viac sa o grafových algoritmoch možno dočítať napríklad v nasledujúcej literatúre, ktorá svojím záberom prudko presahuje rámec tohto predmetu:
    • R. Sedgewick, K. Wayne: Algorithms, 4th ed. Upper Saddle River : Addison-Wesley, 2011. (Grafmi sa zaoberá štvrtá kapitola, používa sa Java.)
    • J. Demel: Grafy a jejich aplikace. Praha : Academia, 2002. (Algoritmy opisované pomocou prirodzeného/matematického jazyka.)
    • T. H. Cormen et al. Introduction to Algorithms, 3rd ed. Cambridge, Massachusetts : MIT Press, 2009. (Algoritmy opisované pomocou pseudokódu.)
  • Ďalšie predmety, na ktorých sa robí s grafmi:

Orientované a neorientované grafy

  • Pod orientovaným grafom (angl. directed graph) 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. Príklad diagramu orientovaného grafu je na obrázku nižšie.
Graf1.png
  • V neorientovanom grafe (angl. undirected graph) nerozlišujeme orientáciu hrán; hrany tak namiesto šípok kreslíme „obyčajnými čiarami”. Medzi každou (neusporiadanou) dvojicou vrcholov pritom môže viesť najviac jedna hrana a v každom vrchole môže graf obsahovať najviac jednu slučku. Neorientovaný graf budeme stotožňovať s orientovaným grafom, v ktorom existencia hrany z vrcholu u do vrcholu v (rôzneho od u) implikuje existenciu hrany z v do u. Príklad diagramu neorientovaného grafu je na obrázku nižšie.
Graf2.png

Pod grafom (bez ďalšieho prívlastku) budeme mať – na rozdiel od väčšiny odborníkov na teóriu grafov – vždy na mysli orientovaný graf.

Dôležité pojmy:

  • Následník vrcholu u v grafe G je ľubovoľný vrchol v taký, že v G vedie hrana z vrcholu u do vrcholu v.
  • Predchodca vrcholu u v grafe G je ľubovoľný vrchol v taký, že u je v grafe G následníkom v.
  • Sused vrcholu u je ľubovoľný vrchol v, ktorý je následníkom alebo predchodcom vrcholu u.
  • V neorientovaných grafoch sú množiny následníkov, predchodcov a susedov každého vrcholu totožné.

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 × n. Pritom A[i][j] == true práve vtedy, keď v grafe vedie hrana z vrcholu i do vrcholu 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)} dostávame nasledujúcu štvorcovú maticu rádu 5 (kde T je skratkou true a F pre false):
  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 následníkov (angl. successor lists)

  • Pre každý vrchol u si pamätáme zoznam (ArrayList, LinkedList, prípadne aj obyčajné pole) následníkov – čiže vrcholov, do ktorých vedie z vrcholu u hrana. Tieto vrcholy si môžeme pamätať v ľubovoľnom poradí, napríklad od najmenšieho po najväčší.
  • 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
1: 2, 3
2: 3
3: 0, 3
4:
  • Pre neorientované grafy obsahuje každý zo zoznamov práve všetkých susedov daného vrcholu. Ide tak o tzv. zoznamy susedov (angl. adjacency lists).

Graf ako abstraktný dátový typ: rozhranie Graph

  • Skôr, než si ukážeme konkrétne implementácie grafov pomocou matíc susednosti aj zoznamov následníkov, potrebujeme vedieť, aké operácie by mal graf poskytovať.
  • Napíšeme preto jednoduché rozhranie pre (orientovaný alebo neorientovaný) graf deklarujúce metódy, ktoré by mala poskytovať každá implementácia grafov.
  • Budeme prevažne pracovať s nemodifikovateľnými grafmi – nasledujúce rozhranie preto nebude deklarovať metódy meniace počet vrcholov alebo množinu hrán.
package graphs;

/**
 *  Rozhranie pre reprezentacie grafov o vrcholoch 0, 1, ..., n-1 pre nejake
 *  prirodzene cislo n.
 */
public interface Graph {
    /**
     * Metoda, ktora vrati pocet vrcholov reprezentovaneho grafu.
     *
     * @return Pocet vrcholov grafu.
     */
    int getNumberOfVertices();

    /**
     * Metoda, ktora vrati pocet hran reprezentovaneho grafu.
     *
     * @return Pocet hran grafu.
     */
    int getNumberOfEdges();

    /**
     * Metoda, ktora zisti, ci v grafe existuje hrana medzi danou dvojicou vrcholov.
     *
     * @param from Pociatocny vrchol.
     * @param to   Koncovy vrchol.
     * @return     Vrati true prave vtedy, ked v grafe existuje hrana z vrcholu from do vrcholu to.
     */
    boolean existsEdge(int from, int to);

    /**
     * Metoda, ktora vrati vsetkych naslednikov daneho vrcholu -- cize vsetky vrcholy, do ktorych vedie z daneho vrcholu
     * orientovana hrana. Pre neorientovane grafy tak tato metoda vzdy vrati vsetkych susedov daneho vrcholu.
     * 
     * @param vertex Lubovolny vrchol grafu.
     * @return       Naslednici vrcholu vertex ako instancia typu Iterable&lt;Integer&gt;.
     */
    Iterable<Integer> outgoingEdgesDestinations(int vertex);
}

Výstupom metódy outgoingEdgesDestinations je inštancia triedy implementujúcej rozhranie Iterable<Integer>. Pripomeňme si, že je v tomto rozhraní predpísaná jediná metóda iterator(), ktorá vráti iterátor (v našom prípade cez prvky typu Integer) a že inštancie inst tried implementujúcich Iterable<Integer> sa dajú použiť v cykle for each. Napríklad:

package graphs;

import java.io.*;

public class Trieda {
    /**
     * Metoda vypise do daneho vystupneho prudu pocet vrcholov a hran grafu, ako aj vsetky dvojice vrcholov tvoriace
     * hrany grafu. 
     * 
     * @param g   Graf, pre ktory sa vypis realizuje.
     * @param out Vystupny prud, do ktoreho sa vypis realizuje.
     */
    public 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.outgoingEdgesDestinations(u)) {
                out.println(u + " " + v);
            }
        }
    }
}

Orientované grafy pomocou zoznamov následníkov: trieda SuccessorListsGraph

  • Pre každý vrchol u si budeme udržiavať ArrayList jeho následníkov.
  • V metóde outgoingEdgesDestinations jednoducho pre daný vrchol vrátime tento zoznam. Obalíme ho ale tak, aby sa nedal meniť (alternatívne by sme namiesto zoznamu samotného mohli vracať jeho kópiu, čo by ale bolo pri častom volaní tejto metódy o niečo menej efektívne).
  • Konštruktor dostane počet vrcholov grafu a všetky jeho hrany v nejakom zoskupení typu Collection<Edge>, kde Edge je pomocná trieda reprezentujúca hranu a slúžiaca hlavne na tento účel.
package graphs;

public class Edge {
    private int from, to;

    public Edge(int from, int to) {
        this.from = from;
        this.to = to;
    }

    public int getFrom() {
        return from;
    }

    public int getTo() {
        return to;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        return this.getClass() == o.getClass() && from == ((Edge) o).from && to == ((Edge) o).to;
    }

    @Override
    public int hashCode() {
        return Integer.valueOf(from).hashCode() + 31 * Integer.valueOf(to).hashCode();
    }
}
package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca orientovany graf pomocou zoznamov naslednikov jednotlivych jeho vrcholov.
 */
public class SuccessorListsGraph implements Graph {
    /**
     * Pre kazdy vrchol zoznam jeho naslednikov.
     */
    private ArrayList<ArrayList<Integer>> successorLists;

    /**
     * Pocet hran v grafe (velkost grafu).
     */
    private int edgeCount;

    /**
     * Konstruktor, ktory dostane ako argumenty pocet vrcholov grafu (t. j. jeho rad), ako aj vsetky hrany grafu.
     *
     * @param vertexCount Rad grafu, cize pocet jeho vrcholov.
     * @param edges       Zoskupenie pozostavajuce zo vsetkych hran grafu.
     */
    public SuccessorListsGraph(int vertexCount, Collection<Edge> edges) {
        successorLists = new ArrayList<>();
        for (int i = 0; i <= vertexCount - 1; i++) {
            successorLists.add(new ArrayList<>());
        }
        edgeCount = 0;
        for (Edge e : edges) {
            if (!existsEdge(e.getFrom(), e.getTo())) {
                successorLists.get(e.getFrom()).add(e.getTo());
                edgeCount++;
            }
        }
    }

    @Override
    public int getNumberOfVertices() {
        return successorLists.size();
    }

    @Override
    public int getNumberOfEdges() {
        return edgeCount;
    }

    @Override
    public boolean existsEdge(int from, int to) {
        return successorLists.get(from).contains(to);
    }

    @Override
    public Iterable<Integer> outgoingEdgesDestinations(int from) {
        return Collections.unmodifiableList(successorLists.get(from));  // Vratime nemodifikovatelny pohlad na zoznam successorLists.get(from).
    }
}

Orientované grafy pomocou matice susednosti: trieda AdjacencyMatrixGraph

package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca orientovany graf pomocou matice susednosti.
 */
public class AdjacencyMatrixGraph implements Graph {
    /**
     * Matica susednosti.
     */
    private boolean adjacencyMatrix[][];

    /**
     * Pocet hran v grafe (velkost grafu).
     */
    private int edgeCount;

    /**
     * Konstruktor, ktory dostane ako argumenty pocet vrcholov grafu (t. j. jeho rad), ako aj vsetky hrany grafu.
     *
     * @param vertexCount Rad grafu, cize pocet jeho vrcholov.
     * @param edges       Zoskupenie pozostavajuce zo vsetkych hran grafu.
     */
    public AdjacencyMatrixGraph(int vertexCount, Collection<Edge> edges) {
        adjacencyMatrix = new boolean[vertexCount][vertexCount];
        edgeCount = 0;
        for (Edge e : edges) {
            if (!existsEdge(e.getFrom(), e.getTo())) {
                adjacencyMatrix[e.getFrom()][e.getTo()] = true;
                edgeCount++;
            }
        }
    }

    @Override
    public int getNumberOfVertices() {
        return adjacencyMatrix.length;
    }

    @Override
    public int getNumberOfEdges() {
        return edgeCount;
    }

    @Override
    public boolean existsEdge(int from, int to) {
        return adjacencyMatrix[from][to];
    }

    @Override
    public Iterable<Integer> outgoingEdgesDestinations(int vertex) {
        List<Integer> a = new ArrayList<>();
        for (int i = 0; i <= getNumberOfVertices() - 1; i++) {
            if (adjacencyMatrix[vertex][i]) {
                a.add(i);
            }
        }
        return Collections.unmodifiableList(a);
    }
}

Neorientované grafy: triedy AdjacencyListsUndirectedGraph a AdjacencyMatrixUndirectedGraph

Pri implementácii neorientovaných grafov môžeme využiť dedenie od prislušných tried pre orientované grafy. Narážame tu len na dva rozdiely:

  • Konštruktor by mal pre každú požadovanú neorientovanú hranu pridať dvojicu protichodných orientovaných hrán (s výnimkou slučiek, kde sa pridáva jediná orientovaná hrana).
  • Metóda getNumberOfEdges by nemala vracať počet orientovaných hrán, ale počet neorientovaných hrán.
package graphs;

public interface UndirectedGraph extends Graph {

}
package graphs;

import java.util.*;

/**
 * Pomocne metody na pracu so zoskupeniami hran.
 */
public class Edges {

    public static Collection<Edge> symmetricClosure(Collection<Edge> edges) {
        List<Edge> result = new ArrayList<>();
        for (Edge e : edges) {
            result.add(e);
            if (e.getFrom() != e.getTo()) {
                result.add(new Edge(e.getTo(), e.getFrom()));
            }
        }
        return result;
    }

    public static int distinctUndirectedEdges(Collection<Edge> edges) {
        HashSet<Edge> set = new HashSet<>();
        for (Edge e : edges) {
            if (!set.contains(e) && !set.contains(new Edge(e.getTo(), e.getFrom()))) {
                set.add(e);
            }
        }
        return set.size();
    }
}
package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca neorientovany graf pomocou zoznamov susedov jednotlivych jeho vrcholov.
 */
public class AdjacencyListsUndirectedGraph extends SuccessorListsGraph implements UndirectedGraph {
    private int undirectedEdgeCount;

    public AdjacencyListsUndirectedGraph(int vertexCount, Collection<Edge> edges) {
        super(vertexCount, Edges.symmetricClosure(edges));
        undirectedEdgeCount = Edges.distinctUndirectedEdges(edges);
    }

    @Override
    public int getNumberOfEdges() {
        return undirectedEdgeCount;
    }
}
package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca neorientovany graf pomocou matice susednosti.
 */
public class AdjacencyMatrixUndirectedGraph extends AdjacencyMatrixGraph implements UndirectedGraph {
    private int undirectedEdgeCount;

    public AdjacencyMatrixUndirectedGraph(int vertexCount, Collection<Edge> edges) {
        super(vertexCount, Edges.symmetricClosure(edges));
        undirectedEdgeCount = Edges.distinctUndirectedEdges(edges);
    }

    @Override
    public int getNumberOfEdges() {
        return undirectedEdgeCount;
    }
}

Vytvorenie grafu

Metóda readGraph triedy Trieda uvedenej nižšie prečíta pomocou danej inštancie triedy Scanner reprezentáciu grafu a vytvorí z nej graf typu určeného nasledujúcim parametrom. Argument pre typ grafu je pritom vymenovaného typu GraphType (o vymenovaných typoch sa možno dočítať viac tu).

package graphs;

public enum GraphType {
    DIRECTED_SUCCESSOR_LISTS, DIRECTED_ADJACENCY_MATRIX, UNDIRECTED_ADJACENCY_LISTS, UNDIRECTED_ADJACENCY_MATRIX
}
package graphs;

import java.util.*;

public class Trieda {
    /**
     * Metoda, ktora precita textovu reprezentaciu grafu pozostavajucu z poctu vrcholov n, poctu hran m a z m dvojic
     * vrcholov udavajucich jednotlive hrany a vytvori z nej graf urceneho typu.
     *
     * @param scanner   Scanner, z ktoreho sa reprezentacia grafu cita.
     * @param graphType Typ vytvaraneho grafu.
     * @return          Vytvoreny graf.  
     */
    public static Graph readGraph(Scanner scanner, GraphType graphType) {
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        List<Edge> edges = new ArrayList<>();
        for (int i = 1; i <= m; i++) {
            edges.add(new Edge(scanner.nextInt(), scanner.nextInt()));
        }
        Graph g = null;
        switch (graphType) {
            case DIRECTED_SUCCESSOR_LISTS:
                g = new SuccessorListsGraph(n, edges);
                break;
            case DIRECTED_ADJACENCY_MATRIX:
                g = new AdjacencyMatrixGraph(n, edges);
                break;
            case UNDIRECTED_ADJACENCY_LISTS:
                g = new AdjacencyListsUndirectedGraph(n, edges);
                break;
            case UNDIRECTED_ADJACENCY_MATRIX:
                g = new AdjacencyMatrixUndirectedGraph(n, edges);
                break;
        }
        return g;
    }
}

Volanie metódy readGraph potom môže vyzerať napríklad nasledovne:

Graph g = readGraph(scanner, GraphType.DIRECTED_SUCCESSOR_LISTS);

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. V závislosti od použitej reprezentácie grafu sa líši ako časová zložitosť jednotlivých operácií na grafoch, tak aj pamäťová zložitosť samotnej tejto reprezentácie. Napríklad:

  • Pamäť potrebná na uloženie matice susednosti grafu je vždy rádovo veľkosti n2. Pri reprezentácii pomocou zoznamov následníkov resp. susedov je veľkosť reprezentácie grafu rádovo n+m. Hlavne pre riedke grafy (s menším počtom hrán) je teda reprezentácia pomocou zoznamov pamäťovo efektívnejšia.
  • Operácia existsEdge sa pre grafy reprezentované maticou susednosti vykoná v konštantnom čase. Pre grafy reprezentované zoznamami následníkov resp. susedov môže byť zložitosť tejto operácie až lineárna v závislosti od počtu vrcholov grafu (je potrebné prejsť celý zoznam susedov jedného vrchola, ktorý môže obsahovať až n rôznych vrcholov).
  • Naopak vytvorenie zoznamu následníkov resp. susedov grafu v metóde outgoingEdgesDestinations je efektívnejšie pri reprezentácii pomocou zoznamov.

Ď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 pridaním informácie o multiplicite do zoznamov následníkov resp. susedov.
  • 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, prípadne ich možno využiť aj na reprezentáciu multigrafov). Možno ich reprezentovať nahradením booleovskej matice maticou ohodnotení, prípadne zakomponovaním informácie o ohodnotení hrán do zoznamov následníkov resp. susedov. S ohodnotenými grafmi sa okrajovo stretneme aj tento semester.
  • Dynamické grafy podporujú aj pridávanie a mazanie vrcholov a/alebo hrán.

Riešenia testu č. 2

Úloha č. 1

import java.util.*;

public class Multiset<E> {
    private HashMap<E, Integer> map;

    public Multiset() {
        map = new HashMap<>();
    }

    public void add(E e) {
        if (map.containsKey(e)) {
            map.put(e, map.get(e) + 1);
        } else {
            map.put(e, 1);
        }
    }

    public void remove(E e) {
        if (map.containsKey(e) && map.get(e) >= 2) {
            map.put(e, map.get(e) - 1);
        } else {
            map.remove(e);
        }
    }

    public boolean contains(E e) {
        return map.containsKey(e);
    }

    public int getMultiplicity(E e) {
        if (map.containsKey(e)) {
            return map.get(e);
        } else {
            return 0;
        }
    }
}

Úloha č. 2

// ...

Collections.sort(polynomials, new Comparator<Polynomial>() {
    @Override
    public int compare(Polynomial p1, Polynomial p2) {
        if (p1.getDegree() < p2.getDegree()) {
            return -1;
        }
        if (p1.getDegree() > p2.getDegree()) {
            return 1;
        }
        for (int n = p1.getDegree(); n >= 0; n--) {
            if (p1.getCoefficient(n) < p2.getCoefficient(n)) {
                return -1;
            }
            if (p1.getCoefficient(n) > p2.getCoefficient(n)) {
                return 1;
            }
        }
        return 0;
    }
});

// ...

Úloha č. 3

import java.util.*;

public class PalilalicIterator<E> implements Iterator<E> {
    private Iterator<E> it;
    private E next;
    private boolean newValueFollows = true;

    public PalilalicIterator(Iterable<E> iterable) {
        this.it = iterable.iterator();
        findNext();
    }

    private void findNext() {
        if (newValueFollows) {
            if (it.hasNext()) {
                next = it.next();
            } else {
                next = null;
            }
        }
        newValueFollows = !newValueFollows;
    }

    @Override
    public boolean hasNext() {
        return next != null;
    }

    @Override
    public E next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        E result = next;
        findNext();
        return result;
    }
}

Letný semester, prednáška č. 7

Oznamy

  • Dnes po prednáške bude zverejnené zadanie tretej domácej úlohy, ktorú bude potrebné odovzdať do pondelka 12. apríla, 9:00 (čiže do začiatku budúcej prednášky).
  • V stredu na cvičeniach bude zverejnených niekoľko nebodovaných úloh zameraných na grafy a grafové algoritmy.
  • Počas cvičení v stredu 7. apríla bude prebiehať tretí test zameraný na látku z prvých siedmich prednášok (časť úloh bude zameraná na grafy, časť ešte na skoršiu látku).

Triedy pre grafy z minulej prednášky

package graphs;

/**
 *  Rozhranie pre reprezentacie grafov o vrcholoch 0, 1, ..., n-1 pre nejake
 *  prirodzene cislo n.
 */
public interface Graph {
    /**
     * Metoda, ktora vrati pocet vrcholov reprezentovaneho grafu.
     *
     * @return Pocet vrcholov grafu.
     */
    int getNumberOfVertices();

    /**
     * Metoda, ktora vrati pocet hran reprezentovaneho grafu.
     *
     * @return Pocet hran grafu.
     */
    int getNumberOfEdges();

    /**
     * Metoda, ktora zisti, ci v grafe existuje hrana medzi danou dvojicou vrcholov.
     *
     * @param from Pociatocny vrchol.
     * @param to   Koncovy vrchol.
     * @return     Vrati true prave vtedy, ked v grafe existuje hrana z vrcholu from do vrcholu to.
     */
    boolean existsEdge(int from, int to);

    /**
     * Metoda, ktora vrati vsetkych naslednikov daneho vrcholu -- cize vsetky vrcholy, do ktorych vedie z daneho vrcholu
     * orientovana hrana. Pre neorientovane grafy tak tato metoda vzdy vrati vsetkych susedov daneho vrcholu.
     * 
     * @param vertex Lubovolny vrchol grafu.
     * @return       Naslednici vrcholu vertex ako instancia typu Iterable&lt;Integer&gt;.
     */
    Iterable<Integer> outgoingEdgesDestinations(int vertex);
}
package graphs;

public class Edge {
    private int from, to;

    public Edge(int from, int to) {
        this.from = from;
        this.to = to;
    }

    public int getFrom() {
        return from;
    }

    public int getTo() {
        return to;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        return this.getClass() == o.getClass() && from == ((Edge) o).from && to == ((Edge) o).to;
    }

    @Override
    public int hashCode() {
        return Integer.valueOf(from).hashCode() + 31 * Integer.valueOf(to).hashCode();
    }
}
package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca orientovany graf pomocou zoznamov naslednikov jednotlivych jeho vrcholov.
 */
public class SuccessorListsGraph implements Graph {
    /**
     * Pre kazdy vrchol zoznam jeho naslednikov.
     */
    private ArrayList<ArrayList<Integer>> successorLists;

    /**
     * Pocet hran v grafe (velkost grafu).
     */
    private int edgeCount;

    /**
     * Konstruktor, ktory dostane ako argumenty pocet vrcholov grafu (t. j. jeho rad), ako aj vsetky hrany grafu.
     *
     * @param vertexCount Rad grafu, cize pocet jeho vrcholov.
     * @param edges       Zoskupenie pozostavajuce zo vsetkych hran grafu.
     */
    public SuccessorListsGraph(int vertexCount, Collection<Edge> edges) {
        successorLists = new ArrayList<>();
        for (int i = 0; i <= vertexCount - 1; i++) {
            successorLists.add(new ArrayList<>());
        }
        edgeCount = 0;
        for (Edge e : edges) {
            if (!existsEdge(e.getFrom(), e.getTo())) {
                successorLists.get(e.getFrom()).add(e.getTo());
                edgeCount++;
            }
        }
    }

    @Override
    public int getNumberOfVertices() {
        return successorLists.size();
    }

    @Override
    public int getNumberOfEdges() {
        return edgeCount;
    }

    @Override
    public boolean existsEdge(int from, int to) {
        return successorLists.get(from).contains(to);
    }

    @Override
    public Iterable<Integer> outgoingEdgesDestinations(int from) {
        return Collections.unmodifiableList(successorLists.get(from));  // Vratime nemodifikovatelny pohlad na zoznam successorLists.get(from).
    }
}
package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca orientovany graf pomocou matice susednosti.
 */
public class AdjacencyMatrixGraph implements Graph {
    /**
     * Matica susednosti.
     */
    private boolean adjacencyMatrix[][];

    /**
     * Pocet hran v grafe (velkost grafu).
     */
    private int edgeCount;

    /**
     * Konstruktor, ktory dostane ako argumenty pocet vrcholov grafu (t. j. jeho rad), ako aj vsetky hrany grafu.
     *
     * @param vertexCount Rad grafu, cize pocet jeho vrcholov.
     * @param edges       Zoskupenie pozostavajuce zo vsetkych hran grafu.
     */
    public AdjacencyMatrixGraph(int vertexCount, Collection<Edge> edges) {
        adjacencyMatrix = new boolean[vertexCount][vertexCount];
        edgeCount = 0;
        for (Edge e : edges) {
            if (!existsEdge(e.getFrom(), e.getTo())) {
                adjacencyMatrix[e.getFrom()][e.getTo()] = true;
                edgeCount++;
            }
        }
    }

    @Override
    public int getNumberOfVertices() {
        return adjacencyMatrix.length;
    }

    @Override
    public int getNumberOfEdges() {
        return edgeCount;
    }

    @Override
    public boolean existsEdge(int from, int to) {
        return adjacencyMatrix[from][to];
    }

    @Override
    public Iterable<Integer> outgoingEdgesDestinations(int vertex) {
        List<Integer> a = new ArrayList<>();
        for (int i = 0; i <= getNumberOfVertices() - 1; i++) {
            if (adjacencyMatrix[vertex][i]) {
                a.add(i);
            }
        }
        return Collections.unmodifiableList(a);
    }
}
package graphs;

public interface UndirectedGraph extends Graph {

}
package graphs;

import java.util.*;

/**
 * Pomocne metody na pracu so zoskupeniami hran.
 */
public class Edges {

    public static Collection<Edge> symmetricClosure(Collection<Edge> edges) {
        List<Edge> result = new ArrayList<>();
        for (Edge e : edges) {
            result.add(e);
            if (e.getFrom() != e.getTo()) {
                result.add(new Edge(e.getTo(), e.getFrom()));
            }
        }
        return result;
    }

    public static int distinctUndirectedEdges(Collection<Edge> edges) {
        HashSet<Edge> set = new HashSet<>();
        for (Edge e : edges) {
            if (!set.contains(e) && !set.contains(new Edge(e.getTo(), e.getFrom()))) {
                set.add(e);
            }
        }
        return set.size();
    }
}
package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca neorientovany graf pomocou zoznamov susedov jednotlivych jeho vrcholov.
 */
public class AdjacencyListsUndirectedGraph extends SuccessorListsGraph implements UndirectedGraph {
    private int undirectedEdgeCount;

    public AdjacencyListsUndirectedGraph(int vertexCount, Collection<Edge> edges) {
        super(vertexCount, Edges.symmetricClosure(edges));
        undirectedEdgeCount = Edges.distinctUndirectedEdges(edges);
    }

    @Override
    public int getNumberOfEdges() {
        return undirectedEdgeCount;
    }
}
package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca neorientovany graf pomocou matice susednosti.
 */
public class AdjacencyMatrixUndirectedGraph extends AdjacencyMatrixGraph implements UndirectedGraph {
    private int undirectedEdgeCount;

    public AdjacencyMatrixUndirectedGraph(int vertexCount, Collection<Edge> edges) {
        super(vertexCount, Edges.symmetricClosure(edges));
        undirectedEdgeCount = Edges.distinctUndirectedEdges(edges);
    }

    @Override
    public int getNumberOfEdges() {
        return undirectedEdgeCount;
    }
}

Pokračovanie úvodu do grafov

Vytvorenie grafu

Metóda readGraph triedy Trieda uvedenej nižšie prečíta pomocou danej inštancie triedy Scanner reprezentáciu grafu a vytvorí z nej graf typu určeného jej druhým parametrom. Argument pre typ grafu je pritom vymenovaného typu GraphType (o vymenovaných typoch sa možno dočítať viac tu).

package graphs;

public enum GraphType {
    DIRECTED_SUCCESSOR_LISTS, DIRECTED_ADJACENCY_MATRIX, UNDIRECTED_ADJACENCY_LISTS, UNDIRECTED_ADJACENCY_MATRIX
}
package graphs;

import java.util.*;

public class Trieda {
    /**
     * Metoda, ktora precita textovu reprezentaciu grafu pozostavajucu z poctu vrcholov n, poctu hran m a z m dvojic
     * vrcholov udavajucich jednotlive hrany a vytvori z nej graf urceneho typu.
     *
     * @param scanner   Scanner, z ktoreho sa reprezentacia grafu cita.
     * @param graphType Typ vytvaraneho grafu.
     * @return          Vytvoreny graf.  
     */
    public static Graph readGraph(Scanner scanner, GraphType graphType) {
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        List<Edge> edges = new ArrayList<>();
        for (int i = 1; i <= m; i++) {
            edges.add(new Edge(scanner.nextInt(), scanner.nextInt()));
        }
        Graph g = null;
        switch (graphType) {
            case DIRECTED_SUCCESSOR_LISTS:
                g = new SuccessorListsGraph(n, edges);
                break;
            case DIRECTED_ADJACENCY_MATRIX:
                g = new AdjacencyMatrixGraph(n, edges);
                break;
            case UNDIRECTED_ADJACENCY_LISTS:
                g = new AdjacencyListsUndirectedGraph(n, edges);
                break;
            case UNDIRECTED_ADJACENCY_MATRIX:
                g = new AdjacencyMatrixUndirectedGraph(n, edges);
                break;
        }
        return g;
    }
}

Volanie metódy readGraph potom môže vyzerať napríklad nasledovne:

Graph g = readGraph(scanner, GraphType.DIRECTED_SUCCESSOR_LISTS);

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. V závislosti od použitej reprezentácie grafu sa líši ako časová zložitosť jednotlivých operácií na grafoch, tak aj pamäťová zložitosť samotnej tejto reprezentácie. Napríklad:

  • Pamäť potrebná na uloženie matice susednosti grafu je vždy rádovo veľkosti n2. Pri reprezentácii pomocou zoznamov následníkov resp. susedov je veľkosť reprezentácie grafu rádovo n+m. Hlavne pre riedke grafy (s menším počtom hrán) je teda reprezentácia pomocou zoznamov pamäťovo efektívnejšia.
  • Operácia existsEdge sa pre grafy reprezentované maticou susednosti vykoná v konštantnom čase. Pre grafy reprezentované zoznamami následníkov resp. susedov môže byť zložitosť tejto operácie až lineárna v závislosti od počtu vrcholov grafu (je potrebné prejsť celý zoznam susedov jedného vrchola, ktorý môže obsahovať až n rôznych vrcholov).
  • Naopak vrátenie zoznamu následníkov resp. susedov grafu v metóde outgoingEdgesDestinations je efektívnejšie pri reprezentácii pomocou zoznamov.

Ď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 pridaním informácie o multiplicite do zoznamov následníkov resp. susedov.
  • 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, prípadne ich možno využiť aj na reprezentáciu multigrafov). Možno ich reprezentovať nahradením booleovskej matice maticou ohodnotení, prípadne zakomponovaním informácie o ohodnotení hrán do zoznamov následníkov resp. susedov. S ohodnotenými grafmi sa okrajovo stretneme aj tento semester.
  • Dynamické grafy podporujú aj pridávanie a mazanie vrcholov a/alebo hrán.

Prehľadávanie (orientovaného alebo neorientovaného) grafu do hĺbky

Existencia cesty medzi dvojicou vrcholov

Riešme teraz nasledujúci problém: pre danú dvojicu vrcholov u a v nejakého (orientovaného alebo neorientovaného) grafu potrebujeme zistiť, či sú spojené sledom – t. j. či sa dá medzi nimi prejsť pomocou postupne na seba nadväzujúcich hrán (počet týchto hrán môže byť aj nulový, takže z každého vrcholu triviálne vedie sled do seba samého). Existencia sledu medzi dvoma vrcholmi je očividne ekvivalentná existencii cesty (t. j. sledu, v ktorom sa žiaden vrchol nezopakuje).

Sled cesta.png

V nasledujúcom preto budeme hovoriť o existencii ciest.

Pojmy sledu a cesty o niečo presnejšie:

  • Sledom v grafe rozumieme postupnosť vrcholov v0, v1, ..., vn takú, že pre i = 1,...,n existuje v danom grafe hrana z vi-1 do vi.
  • Cestou rozumieme sled v0, v1, ..., vn taký, že vrcholy v0, v1, ..., vn sú po dvoch rôzne.
  • Dĺžkou sledu (alebo cesty) v0, v1, ..., vn nazveme číslo n, čiže počet hrán tento sled tvoriacich.

Pre neorientované grafy možno problém existencie cesty medzi dvoma vrcholmi chápať aj ako úlohu zistiť, či sú tieto dva vrcholy v rovnakom komponente súvislosti grafu. Komponent súvislosti neorientovaného grafu je každý jeho (vzhľadom na inklúziu) maximálny podgraf, ktorý je súvislý – komponent súvislosti grafu teda pozostáva z nejakej podmnožiny jeho vrcholov a všetkých hrán pôvodného grafu spájajúcich vrcholy z tejto podmnožiny, pričom ľubovoľné dva vrcholy komponentu sú spojené cestou a pridaním ľubovoľného ďalšieho vrcholu grafu sa táto vlastnosť poruší. (Pri tejto „definícii” využívame fakt, že existencia cesty v neorientovanom grafe je zjavne reláciou ekvivalencie na množine jeho vrcholov.) Napríklad neorientovaný graf na nasledujúcom obrázku pozostáva z troch komponentov súvislosti.

Graf3.png

Na riešenie problému existencie cesty použijeme prehľadávanie do hĺbky (angl. depth-first search) – 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ť neorientovaným 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 neorientovaného grafu.

Na riešenie uvedeného problému napíšeme rekurzívnu metódu search, ktorá bude prehľadávať všetkých ešte nenavštívených následníkov daného vrcholu. 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, ktora rekurzivne prehlada vsetky doposial nenavstivene vrcholy
* dosiahnutelne z daneho vrcholu.
* @param g       Orientovany alebo neorientovany graf, v ktorom sa prehladavanie realizuje.
* @param vertex  Vrchol grafu g, v ktorom sa prehladavanie zacina.
* @param visited Zoznam obsahujuci informacie o navstiveni jednotlivych vrcholov grafu. Pri volani metody by malo
*                platit visited.get(vertex) == false.
*/
private static void search(Graph g, int vertex, List<Boolean> visited) {
    visited.set(vertex, true);
    for (int successor : g.outgoingEdgesDestinations(vertex)) {
        if (!visited.get(successor)) {
            search(g, successor, visited);
        }
    }
}

/**
 * Metoda, ktora zisti, ci je dvojica vrcholov grafu spojena cestou.
 * @param g    Graf, v ktorom sa uloha realizuje.
 * @param from Pociatocny vrchol.
 * @param to   Koncovy vrchol.
 * @return     Vystup je true prave vtedy, ked v grafe g existuje cesta z vrcholu from do vrcholu to.
 */
public static boolean existsPath(Graph 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);
}

Cvičenie. Vytvorte abstraktnú triedu AbstractGraph implementujúcu rozhranie Graph a upravte triedy SuccessorListsGraph a AdjacencyMatrixGraph tak, aby dedili od triedy AbstractGraph. Prepíšte metódy existsPath a search uvedené vyššie ako metódy inštancie triedy AbstractGraph. Aký bude mať táto zmena vplyv na argumenty týchto metód?

Hľadanie komponentov súvislosti neorientovaného grafu

V prípade, že pracujeme s neorientovaným grafom a existenciu cesty medzi dvojicami vrcholov by sme 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:

package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca rozdelenie neorientovaneho grafu na komponenty suvislosti.
 */
public class Components {
    /**
     * Neorientovany graf, ktoreho komponenty suvislosti su instanciou tejto triedy reprezentovane.
     */
    private UndirectedGraph g;

    /**
     * Zoznam, v ktorom si pre kazdy vrchol grafu g budeme pamatat cislo jeho komponentu.
     */
    private ArrayList<Integer> componentId;

    /**
     * Celkovy pocet komponentov suvislosti grafu g.
     */
    private int componentCount;

    /**
     * Konstruktor, ktory dostane ako argument neorientovany graf, najde komponenty suvislosti tohto grafu a informacie
     * o nich ulozi do premennych instancie.
     * @param g Neorientovany graf, ktoreho komponenty su reprezentovane instanciou tejto triedy.
     */
    public Components(UndirectedGraph g) {
        this.g = g;
        componentCount = 0;
        int n = g.getNumberOfVertices();

        componentId = new ArrayList<>();
        for (int i = 0; i <= n - 1; i++) {
            componentId.add(-1);
        }

        for (int i = 0; i <= n - 1; i++) {
            if (componentId.get(i) == -1) {
                search(i, componentCount);
                componentCount++;
            }
        }
    }

    /**
     * Pomocna metoda pre konstruktor, ktora oznaci vrcholy jedneho komponentom suvislosti identifikatorom tohto
     * komponentu. Pracuje na baze prehladavania do hlbky.
     * @param vertex Vrchol, z ktoreho sa zacina prehladavanie vrcholov komponentu.
     * @param id     Identifikator komponentu suvislosti.
     */
    private void search(int vertex, int id) {
        componentId.set(vertex, id);
        for (int neighbour : g.outgoingEdgesDestinations(vertex)) {
            if (componentId.get(neighbour) == -1) {
                search(neighbour, id);
            }
        }
    }

    /**
     * Metoda, ktora zisti, ci v grafe, ktoreho komponenty reprezentuje instancia tejto triedy, existuje cesta spajajuca
     * danu dvojicu vrcholov.
     * @param from Pociatocny vrchol.
     * @param to   Koncovy vrchol.
     * @return     Metoda vrati true prave vtedy, ked v grafe existuje cesta z vrcholu from do vrcholu to.
     */
    public boolean existsPath(int from, int to) {
        return componentId.get(from).equals(componentId.get(to));
    }

    /**
     * Metoda, ktora vrati celkovy pocet komponentov grafu.
     * @return Pocet komponentov.
     */
    public int getComponentCount() {
        return componentCount;
    }
}

Nasledujúci kód načíta neorientovaný graf a dvojicu jeho vrcholov. Na konzolu následne vypíše, či sú tieto dva vrcholy v danom grafe spojené cestou.

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Zadaj neorientovany graf:");
    UndirectedGraph g = (UndirectedGraph) readGraph(scanner, GraphType.UNDIRECTED_ADJACENCY_LISTS);
    System.out.println("Zadaj dvojicu vrcholov grafu:");
    int from = scanner.nextInt();
    int to = scanner.nextInt();

    Components components = new Components(g);
    if (components.existsPath(from, to)) {
        System.out.println("Vrcholy su spojene cestou.");
    } else {
        System.out.println("Vrcholy nie su spojene cestou.");
    }
}

Prehľadávanie (orientovaného alebo neorientovaného) grafu do šírky

Podobne ako prehľadávanie do hĺbky možno vo všeobecnejšom kontexte grafov aplikovať aj prehľadávanie do šírky (angl. breadth-first search), ktorého variant pre obdĺžnikovú mriežku poznáme z minulého semestra. Prehľadávanie grafu do šírky bude možné použiť na hľadanie najkratších ciest medzi dvojicami vrcholov orientovaného (a teda aj neorientovaného) grafu, kde dĺžka cesty je meraná počtom hrán.

Hľadanie najkratšej cesty

Hľadanie najkratších ciest v grafe – či už orientovanom alebo neorientovanom – možno realizovať napríklad nasledujúcou triedou 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 zoznamu dist uloží jeho vzdialenosť od vrcholu start a do zoznamu predecessors sa uloží vrchol u, z ktorého bol vrchol v objavený – musí pritom vždy ísť o predposledný vrchol na jednej z najkratších ciest 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 zozname 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 zozname predecessors.
package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca najkratsie cesty z pevne daneho pociatocneho vrcholu do vsetkych ostatnych vrcholov grafu.
 */
public class ShortestPathsFromVertex {
    /**
     * Graf, v ktorom sa hladanie najkratsich ciest realizuje.
     */
    private Graph g;

    /**
     * "Startovaci" vrchol. Instancia triedy bude reprezentovat najkratsie cesty z tohto vrcholu do vsetkych ostatnych
     * vrcholov grafu g.
     */
    private int start;

    /**
     * Zoznam obsahujuci pre kazdy vrchol grafu jeho vzdialenost zo startovacieho vrcholu start. Ak zo start do nejakeho
     * vrcholu nevedie ziadna cesta, bude namiesto jeho vzdialenosti v zozname ulozena hodnota -1.
     */
    private List<Integer> distances;

    /**
     * Zoznam obsahujuci pre kazdy vrchol v grafu g jeho predchodcu na najkratsej ceste z vrcholu start do vrcholu v.
     * Pre vrchol start samotny a vrcholy, do ktorych zo start nevedie ziadna cesta, bude v zozname ulozena hodnota -1.
     */
    private List<Integer> predecessors;

    /**
     * Konstruktor, ktory pre dany graf a "startovaci" vrchol rovno aj najde najkratsie cesty zo startovacieho vrcholu
     * do vsetkych ostatnych vrcholov grafu.
     * @param g     Graf, v ktorom sa hladanie ciest realizuje.
     * @param start "Startovaci" vrchol.
     */
    public ShortestPathsFromVertex(Graph g, int start) {
        this.g = g;
        this.start = start;

        /* Inicializacia zoznamov dist a predecessors: */
        distances = new ArrayList<>();
        predecessors = new ArrayList<>();
        for (int i = 0; i <= g.getNumberOfVertices() - 1; i++) {
            distances.add(-1);
            predecessors.add(-1);
        }

        /* Samotne prehladavanie do sirky: */
        Queue<Integer> queue = new LinkedList<>();
        distances.set(start, 0);
        queue.add(start);
        while (!queue.isEmpty()) {
            // Vyberieme vrchol z radu, prejdeme vsetkych jeho naslednikov, nenavstivenych spracujeme a vlozime do radu:
            int vertex = queue.remove();
            for (int successor : g.outgoingEdgesDestinations(vertex)) {
                if (distances.get(successor) == -1) {
                    distances.set(successor, distances.get(vertex) + 1);
                    predecessors.set(successor, vertex);
                    queue.add(successor);
                }
            }
        }
    }

    /**
     * Metoda, ktora vrati dlzku najkratsej cesty z vrcholu start do daneho vrcholu.
     * @param vertex Vrchol, vzdialenost ktoreho z vrcholu start sa pocita.
     * @return       Dlzka najkratsej cesty z vrcholu start do vrcholu vertex. Ak ziadna neexistuje, vrati sa -1.
     */
    public int distanceFromStart(int vertex) {
        return distances.get(vertex);
    }

    /**
     * Metoda, ktora vrati najkratsiu cestu z vrcholu start do daneho vrcholu, reprezentovanu ako zoznam vrcholov.
     * @param vertex Vrchol, najkratsia cesta do ktoreho z vrcholu start sa pocita.
     * @return       Nemodifikovatelny zoznam obsahujuci postupne vsetky vrcholy najkratsej cesty zo start do vertex.
     *               Ak ziadna cesta zo start do vertex neexistuje, vrati metoda referenciu null.
     */
    public List<Integer> shortestPathFromStart(int vertex) {
        if (distances.get(vertex) == -1) {
            return null;
        }
        LinkedList<Integer> path = new LinkedList<>();
        int v = vertex;
        while (v != -1) {
            path.addFirst(v);
            v = predecessors.get(v);
        }
        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, GraphType.DIRECTED_SUCCESSOR_LISTS);
    System.out.println("Zadaj pociatocny a koncovy vrchol:");
    int from = scanner.nextInt();
    int to = scanner.nextInt();

    ShortestPathsFromVertex shortestPathsFromVertex = new ShortestPathsFromVertex(g, from);
    System.out.println("Najkratsia cesta ma dlzku " + shortestPathsFromVertex.distanceFromStart(to) + ".");
    List<Integer> shortestPath = shortestPathsFromVertex.shortestPathFromStart(to);
    if (shortestPath != null) {
        System.out.println(shortestPath);
    }
}

Stromy prehľadávania do hĺbky a do šírky

Označme pri prehľadávaní do hĺbky aj do šírky tie hrany, ktoré boli použité pri objavovaní nových vrcholov – to znamená: pri prehľadávaní do hĺbky hrany (u,v) také, že vo volaní metódy search pre vrchol u sa táto metóda zavolala rekurzívne aj pre vrchol v a pri prehľadávaní do šírky hrany (u,v) také, že sa v rámci prehľadávania následníkov vrcholu u do radu pridal vrchol v.

  • V oboch prípadoch potom takéto hrany tvoria strom zakorenený vo vrchole, v ktorom prehľadávanie začalo (pri orientovaných grafoch sú všetky hrany orientované smerom od koreňa k listom).
  • Hovoríme teda o stromoch prehľadávania do hĺbky resp. do šírky (angl. DFS Tree resp. BFS Tree).
  • V prípade, že je graf neorientovaný a súvislý, ide v obidvoch prípadoch o jeho kostru (t. j. strom zložený zo všetkých vrcholov grafu a jeho vybraných hrán).
  • U stromov prehľadávania do šírky reprezentujú cesty od koreňa k listom vždy niektorú z najkratších ciest medzi príslušnými vrcholmi grafu.

Príklad. Nižšie je znázornený diagram orientovaného grafu a jeho stromy prehľadávania do hĺbky a do šírky v prípade, že prehľadávanie začne vo vrchole 0 a pri spracúvaní následníkov sa postupuje vzostupne podľa čísel vrcholov (čo nemusí byť vždy tak).

Strom prehľadávania do hĺbky:

Graf4.png                            Graf4DFS.png                             DFSTree.png

Strom prehľadávania do šírky:

Graf4.png                            Graf4BFS.png                             BFSTree.png

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ú – žiadne efektívne algoritmy. Prehľadávaním s návratom 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 nájde všetky cesty dĺžky presne length vedúce v g z vrcholu from do vrcholu to. Prehľadávanie s návratom sa spustí hneď v konštruktore a nájdené cesty sa uložia do zoznamu paths.

Pri prehľadávaní sa v spájanom zozname path budú postupne generovať všetky cesty hľadaného typu. 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, uložíme ju do zoznamu paths.

package graphs;

import java.util.*;

/**
 * Trieda realizujuca najdenie vsetkych ciest danej fixnej dlzky medzi danou dvojicou vrcholov daneho grafu.
 */
public class FixedLengthPaths {
    /**
     * Graf, v ktorom sa hladanie ciest realizuje.
     */
    private Graph g;

    /**
     * Pociatocny vrchol hladanych ciest.
     */
    private int from;

    /**
     * Koncovy vrchol hladanych ciest.
     */
    private int to;

    /**
     * Pozadovana dlzka hladanych ciest.
     */
    private int length;

    /**
     * Pomocny zoznam, v ktorom sa budu pomocou prehladavania s navratom postupne generovat jednotlive cesty.
     */
    private LinkedList<Integer> path;

    /**
     * Pomocny zoznam, v ktorom si budeme pocas generovania ciest pre kazdy vrchol pamatat, ci sa nachadza alebo
     * nenachadza v doposial vygenerovanej casti cesty.
     */
    private ArrayList<Boolean> visited;

    /**
     * Zoznam, v ktorom budu ulozene vsetky vygenerovane cesty.
     */
    private List<List<Integer>> paths;

    /**
     * Konstruktor, ktory rovno aj spusti prehladavanie s navratom a do zoznamu paths postupne ulozi vsetky cesty danej
     * dlzky medzi danou dvojicou vrcholov daneho grafu.
     * @param g      Graf, v ktorom sa hladanie ciest realizuje.
     * @param from   Pociatocny vrchol hladanych ciest.
     * @param to     Koncovy vrchol hladanych ciest.
     * @param length Pozadovana dlzka hladanych ciest.
     */
    public FixedLengthPaths(Graph g, int from, int to, int length) {
        this.g = g;
        this.from = from;
        this.to = to;
        this.length = length;
        paths = new LinkedList<>();

        visited = new ArrayList<>();
        for (int i = 0; i <= g.getNumberOfVertices() - 1; i++) {
            visited.add(false);
        }
        path = new LinkedList<>();
        path.add(from);
        visited.set(from, true);
        search();
    }

    /**
     * Metoda realizujuca samotne rekurzivne prehladavanie s navratom. Ak je dosial vygenerovana cast cesty kratsia nez
     * length, postupne sa vyskusaju vsetky moznosti jej predlzenia. V pripade, ze uz ide o cestu dlzky length, overi
     * sa, ci cesta konci vo vrchole to a ak ano, prida sa kopia tejto cesty do zoznamu paths.
     */
    private void search() {
        if (path.size() == length + 1) {   // Dlzka zoznamu je vzdy o jedna vacsia, nez dlzka nim reprezentovanej cesty.
            if (path.getLast() == to) {
                paths.add(Collections.unmodifiableList(new LinkedList<>(path)));
            }
        } else {
            for (int successor : g.outgoingEdgesDestinations(path.getLast())) {
                if (!visited.get(successor)) {
                    visited.set(successor, true);
                    path.add(successor);
                    search();
                    path.removeLast();
                    visited.set(successor, false);
                }
            }
        }
    }

    /**
     * Metoda, ktora vrati nemodifikovatelny pohlad na zoznam vsetkych vygenerovanych ciest.
     * @return Nemodifikovatelny zoznam vsetkych ciest dlzky length veducich v grafe g z vrcholu from do vrcholu to.
     */
    public List<List<Integer>> getPaths() {
        return Collections.unmodifiableList(paths);
    }
}

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, GraphType.DIRECTED_SUCCESSOR_LISTS);
    System.out.println("Zadaj pociatocny a koncovy vrchol:");
    int from = scanner.nextInt();
    int to = scanner.nextInt();
    System.out.println("Zadaj dlzku generovanych ciest:");
    int length = scanner.nextInt();
  
    FixedLengthPaths fixedLengthPaths = new FixedLengthPaths(g, from, to, length);
    System.out.println("Cesty dlzky " + length + ":");
    for (List<Integer> path : fixedLengthPaths.getPaths()) {
        System.out.println(path);
    }
}
Graf5.png

Príklad. Pre orientovaný graf s vrcholmi {0,...,4} a hranami {(0,1),(0,2),(0,3),(1,2),(2,3),(2,4),(4,3)}, 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 hľadania a ukladania všetkých ciest danej dĺžky 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, ktorá sa oproti triede FixedLengthPaths bude líšiť len málo.

  • Počas prehľadávania si budeme pamätať najdlhšiu doposiaľ nájdenú cestu.
  • Vždy, keď prídeme do cieľového vrcholu, porovnáme dĺžku práve nájdenej cesty s najdlhšou doposiaľ nájdenou cestou.
package graphs;

import java.util.*;

/**
 * Trieda realizujuca najdenie najdlhsej cesty medzi danou dvojicou vrcholov grafu.
 */
public class LongestPath {
    /**
     * Graf, v ktorom sa hladanie ciest realizuje.
     */
    private Graph g;

    /**
     * Pociatocny vrchol hladanych ciest.
     */
    private int from;

    /**
     * Koncovy vrchol hladanych ciest.
     */
    private int to;

    /**
     * Pomocny zoznam, v ktorom sa budu pomocou prehladavania s navratom postupne generovat jednotlive cesty.
     */
    private LinkedList<Integer> path;

    /**
     * Pomocny zoznam, v ktorom si budeme pocas generovania ciest pre kazdy vrchol pamatat, ci sa nachadza alebo
     * nenachadza v doposial vygenerovanej casti cesty.
     */
    private ArrayList<Boolean> visited;

    /**
     * Zoznam, v ktorom bude ulozena najdlhsia cesta medzi danou dvojicou vrcholov (pocas prehladavania pojde
     * o najdlhsiu doposial najdenu cestu).
     */
    private List<Integer> longestPath;

    /**
     * Konstruktor, ktory rovno aj spusti prehladavanie s navratom a do zoznamu longestPath ulozi niektoru spomedzi
     * najdlhsich ciest medzi danou dvojicou vrcholov grafu.
     * @param g      Graf, v ktorom sa hladanie ciest realizuje.
     * @param from   Pociatocny vrchol hladanych ciest.
     * @param to     Koncovy vrchol hladanych ciest.
     */
    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);
        }
        path = new LinkedList<>();
        path.add(from);
        visited.set(from, true);
        search();
    }

    /**
     * Metoda realizujuca samotne rekurzivne prehladavanie s navratom. V pripade, ze sa vygenerovala cesta konciaca
     * vo vrchole to, porovna sa jej dlzka s dlzkou doposial najdlhsej najdenej cesty a ak je dlhsia, ulozi sa ako nova
     * doposial najdlhsia cesta. V opacnom pripade sa vyskusaju vsetky moznosti predlzenia cesty.
     */
    private void search() {
        if (path.getLast() == to) {
            if (longestPath == null || path.size() > longestPath.size()) {
                longestPath = new LinkedList<>(path);
            }
        } else {
            for (int successor : g.outgoingEdgesDestinations(path.getLast())) {
                if (!visited.get(successor)) {
                    visited.set(successor, true);
                    path.add(successor);
                    search();
                    path.removeLast();
                    visited.set(successor, false);
                }
            }
        }
    }

    /**
     * Metoda, ktora vrati najdenu najdlhsiu cestu medzi danou dvojicou vrcholov.
     * @return Nemodifikovatelny pohlad na zoznam vrcholov na najdlhsej ceste z vrcholu from do vrcholu to.
     */
    public List<Integer> getLongestPath() {
        if (longestPath != null) {
            return Collections.unmodifiableList(longestPath);
        } else {
            return null;
        }
    }
}

Použitie triedy:

LongestPath longestPath = new LongestPath(g, from, to);
List<Integer> longest = longestPath.getLongestPath();
if (longest != null) {
    System.out.println("Najdlhsia cesta: " + longest);
}

Príklad výstupu na rovnakom grafe ako vyššie pre počiatočný vrchol 0 a koncový vrchol 3:

Najdlhsia cesta: [0, 1, 2, 4, 3]

Riešenia testu č. 3

Úloha č. 1

import java.util.*;

public class IncreasingIterator<E extends Comparable<E>> implements Iterator<E> {
    private Iterator<E> it;
    private E next;

    public IncreasingIterator(Iterable<E> iterable) {
        it = iterable.iterator();
        if (it.hasNext()) {
            next = it.next();
        }
    }

    @Override
    public boolean hasNext() {
        return next != null;
    }

    private void findNext() {
        E newNext = null;
        while (newNext == null && it.hasNext()) {
            E e = it.next();
            if (e.compareTo(next) > 0) {
                newNext = e;
            }
        }
        next = newNext;
    }

    @Override
    public E next() {
        E result = null;
        if (hasNext()) {
            result = next;
        } else {
            throw new NoSuchElementException();
        }
        findNext();
        return result;
    }

}

Úloha č. 2

private static void search(Graph g, int vertex, List<Boolean> visited) {
    visited.set(vertex, true);
    for (int successor : g.outgoingEdgesDestinations(vertex)) {
        if (!visited.get(successor)) {
            search(g, successor, visited);
        }
    }
}

private static boolean existsPath(Graph 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);
}

public static boolean areMutuallyReachable(Graph g, int u, int v) {
    return existsPath(g, u, v) && existsPath(g, v, u);
}

Úloha č. 3

package graphs;

import java.util.*;

public class Eccentricities {
    private int n;
    private List<Integer> eccentricities;
    private int radius = Integer.MAX_VALUE;

    public Eccentricities(UndirectedGraph g) {
        n = g.getNumberOfVertices();
        eccentricities = new ArrayList<>();

        for (int v = 0; v <= n - 1; v++) {
            int eccentricity = 0;
            List<Integer> distances = new ArrayList<>();
            for (int u = 0; u <= n - 1; u++) {
                distances.add(-1);
            }

            Queue<Integer> queue = new LinkedList<>();
            distances.set(v, 0);
            queue.add(v);
            while (!queue.isEmpty()) {
                int vertex = queue.remove();
                if (distances.get(vertex) > eccentricity) {
                    eccentricity = distances.get(vertex);
                }
                for (int successor : g.outgoingEdgesDestinations(vertex)) {
                    if (distances.get(successor) == -1) {
                        distances.set(successor, distances.get(vertex) + 1);
                        queue.add(successor);
                    }
                }
            }

            if (distances.contains(-1)) {
                eccentricity = Integer.MAX_VALUE;
            }

            eccentricities.add(eccentricity);
            if (eccentricity < radius) {
                radius = eccentricity;
            }
        }
    }

    public int eccentricity(int vertex) {
        return eccentricities.get(vertex);
    }

    public int radius() {
        return radius;
    }

    public boolean isCentral(int vertex) {
        return eccentricities.get(vertex) == radius;
    }
}

Letný semester, prednáška č. 9

Oznamy

  • Dnes po prednáške bude zverejnené zadanie štvrtej domácej úlohy, ktorú bude potrebné odovzdať do pondelka 26. apríla, 9:00 (čiže do začiatku jedenástej prednášky).
  • Počas stredajších cvičení bude prebiehať štvrtý test zameraný predovšetkým na grafy a grafové algoritmy.
  • V rámci stredajších cvičení tiež bude zverejnené zadanie tretej bonusovej úlohy s termínom odovzdania do stredy 28. apríla, 11:30.
  • Cvičenia v stredu 21. apríla budú aj napriek dekanskému voľnu (účasť je samozrejme dobrovoľná, rovnako ako aj na ostatných cvičeniach, na ktorých sa nepíše test).
  • Boli vypísané predbežné termíny skúšok:
    • Predtermín v piatok 14. mája, 11:30 (ústne skúšky po 15:30). Maximálne 12 prihlásených študentov.
    • Ďalšie termíny vo štvrtky 20. mája, 3. júna, 17. júna a 24. júna, vždy o 9:00 (ústne skúšky po 14:00). Maximálne 18 prihlásených študentov na každý z termínov.
    • Každý z termínov možno využiť ako riadny aj ako opravný; žiadne ďalšie termíny ale nebudú.
    • Prípadné námietky voči týmto termínom dajte vedieť najneskôr na prednáške 26. apríla. Po tejto prednáške bude otvorené prihlasovanie na skúšky v AIS.
    • Na každý z termínov bude potrebné prihlásiť sa cez AIS najneskôr 24 hodín vopred.

Prehľadávanie s návratom na grafoch: pokračovanie

Hľadanie najdlhšej cesty

Uvažujme problém nájdenia niektorej z najdlhších ciest z vrcholu u do vrcholu v daného orientovaného grafu (ak existuje aspoň jedna). Je dokázané, že za predpokladu platnosti určitých hypotéz z teoretickej informatiky pre túto úlohu neexistuje žiaden efektívny algoritmus. Použijeme teda prehľadávanie s návratom. To bude realizovať trieda LongestPath, ktorú získame drobnou úpravou triedy FixedLengthPaths z minulej prednášky.

  • Počas prehľadávania si budeme pamätať najdlhšiu doposiaľ nájdenú cestu.
  • Vždy, keď prídeme do cieľového vrcholu, porovnáme dĺžku práve nájdenej cesty s najdlhšou doposiaľ nájdenou cestou.
package graphs;

import java.util.*;

/**
 * Trieda realizujuca najdenie najdlhsej cesty medzi danou dvojicou vrcholov grafu.
 */
public class LongestPath {
    /**
     * Graf, v ktorom sa hladanie ciest realizuje.
     */
    private Graph g;

    /**
     * Pociatocny vrchol hladanych ciest.
     */
    private int from;

    /**
     * Koncovy vrchol hladanych ciest.
     */
    private int to;

    /**
     * Pomocny zoznam, v ktorom sa budu pomocou prehladavania s navratom postupne generovat jednotlive cesty.
     */
    private LinkedList<Integer> path;

    /**
     * Pomocny zoznam, v ktorom si budeme pocas generovania ciest pre kazdy vrchol pamatat, ci sa nachadza alebo
     * nenachadza v doposial vygenerovanej casti cesty.
     */
    private ArrayList<Boolean> visited;

    /**
     * Zoznam, v ktorom bude ulozena najdlhsia cesta medzi danou dvojicou vrcholov (pocas prehladavania pojde
     * o najdlhsiu doposial najdenu cestu).
     */
    private List<Integer> longestPath;

    /**
     * Konstruktor, ktory rovno aj spusti prehladavanie s navratom a do zoznamu longestPath ulozi niektoru spomedzi
     * najdlhsich ciest medzi danou dvojicou vrcholov grafu.
     * @param g      Graf, v ktorom sa hladanie ciest realizuje.
     * @param from   Pociatocny vrchol hladanych ciest.
     * @param to     Koncovy vrchol hladanych ciest.
     */
    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);
        }
        path = new LinkedList<>();
        path.add(from);
        visited.set(from, true);
        search();
    }

    /**
     * Metoda realizujuca samotne rekurzivne prehladavanie s navratom. V pripade, ze sa vygenerovala cesta konciaca
     * vo vrchole to, porovna sa jej dlzka s dlzkou doposial najdlhsej najdenej cesty a ak je dlhsia, ulozi sa ako nova
     * doposial najdlhsia cesta. V opacnom pripade sa vyskusaju vsetky moznosti predlzenia cesty.
     */
    private void search() {
        if (path.getLast() == to) {
            if (longestPath == null || path.size() > longestPath.size()) {
                longestPath = new LinkedList<>(path);
            }
        } else {
            for (int successor : g.outgoingEdgesDestinations(path.getLast())) {
                if (!visited.get(successor)) {
                    visited.set(successor, true);
                    path.add(successor);
                    search();
                    path.removeLast();
                    visited.set(successor, false);
                }
            }
        }
    }

    /**
     * Metoda, ktora vrati najdenu najdlhsiu cestu medzi danou dvojicou vrcholov.
     * @return Nemodifikovatelny pohlad na zoznam vrcholov na najdlhsej ceste z vrcholu from do vrcholu to.
     */
    public List<Integer> getLongestPath() {
        if (longestPath != null) {
            return Collections.unmodifiableList(longestPath);
        } else {
            return null;
        }
    }
}

Použitie triedy:

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Zadaj graf:");
    Graph g = readGraph(scanner, GraphType.DIRECTED_SUCCESSOR_LISTS);
    System.out.println("Zadaj pociatocny a koncovy vrchol:");
    int from = scanner.nextInt();
    int to = scanner.nextInt();

    LongestPath longestPath = new LongestPath(g, from, to); 
    List<Integer> longest = longestPath.getLongestPath();
    if (longest != null) {
        System.out.println("Najdlhsia cesta: " + longest);
    } else {
        System.out.println("Ziadna cesta neexistuje.");
    }
}
Graf5.png

Príklad výstupu pre graf na obrázku vyššie, počiatočný vrchol 0 a koncový vrchol 3:

Najdlhsia cesta: [0, 1, 2, 4, 3]

Hľadanie najväčšej kliky v neorientovanom grafe

Uvažujme teraz neorientované grafy bez slučiek (slučky síce nebudeme zakazovať, ale v našich nasledujúcich úvahách ich budeme ignorovať; najvhodnejšia predstava teda je, že pracujeme s grafmi, ktoré žiadne slučky neobsahujú).

  • Klikou (angl. clique) v neorientovanom grafe rozumieme jeho úplný podgraf, čiže podmnožinu K množiny vrcholov grafu takú, že každé dva rôzne vrcholy z K sú navzájom spojené hranou. Veľkosťou kliky rozumieme počet vrcholov kliku tvoriacich. Špeciálne každý vrchol sám o sebe tvorí kliku veľkosti jedna a ľubovoľné dva vrcholy spojené hranou tvoria kliku veľkosti dva.
  • V grafoch na nasledujúcom obrázku sú vyznačené kliky veľkosti tri a štyri.
Kliky.png

Budeme sa teraz zaoberať problémom nájdenia najväčšej kliky (angl. maximum clique) v neorientovanom grafe.

  • Ide o nájdenie kliky s najväčšou veľkosťou spomedzi všetkých klík daného grafu.
  • Pozor na terminológiu: maximálna klika (angl. maximal clique) je ľubovoľná klika, ktorú nemožno pridaním ďalšieho vrcholu rozšíriť na väčšiu kliku. Nie každá maximálna klika ale musí byť aj najväčšia. My sa budeme zaoberať prakticky užitočnejším problémom hľadania najväčších klík.
  • Aj pre problém najväčšej kliky je dokázané, že v prípade platnosti určitých hypotéz z teoretickej informatiky preň neexistuje žiaden efektívny algoritmus. Použijeme teda prehľadávanie s návratom – nebudeme ním však teraz konštruovať cesty, ale množiny vrcholov grafu.
  • Postupne budeme generovať všetky množiny vrcholov daného grafu ako utriedené spájané zoznamy. Zakaždým sa pokúsime daný vrchol do množiny pridať a rekurzívne pokračovať na ďalší vrchol, následne sa ho pokúsime vynechať a tiež rekurzívne pokračovať na ďalší vrchol. Po vygenerovaní kompletnej množiny zistíme, či ide o kliku; ak áno, porovnáme jej veľkosť s najväčšou doposiaľ nájdenou klikou, ktorú v prípade potreby aktualizujeme.

Trieda MaximumClique realizujúca nájdenie najväčšej kliky v neorientovanom grafe:

package graphs;

import java.util.*;

/**
 * Trieda realizujuca najdenie niektorej spomedzi najvacsich klik v danom neorientovanom grafe.
 */
public class MaximumClique {
    /**
     * Neorientovany graf, v ktorom sa hladanie najvacsej kliky realizuje.
     */
    private UndirectedGraph g;

    /**
     * Zoznam vrcholov najvacsej kliky neorientovaneho grafu g, resp. pocas prehladavania s navratom doposial najvacsej
     * najdenej kliky.
     */
    private LinkedList<Integer> maximumClique;

    /**
     * Zoznam, v ktorom sa pocas prehladavania s navratom budu postupne generovat vsetky (utriedene) mnoziny vrcholov
     * grafu g.
     */
    private LinkedList<Integer> currentVertices;

    /**
     * Konstruktor, ktory pomocou prehladavania s navratom najde najvacsiu kliku v danom neorientovanom grafe.
     * @param g Neorientovany graf, v ktorom sa hladanie najvacsej kliky realizuje.
     */
    public MaximumClique(UndirectedGraph g) {
        this.g = g;
        maximumClique = new LinkedList<>();
        currentVertices = new LinkedList<>();
        search(0);
    }

    /**
     * Rekurzivna metoda realizujuca samotne prehladavnie s navratom a v zozname currentVertices postupne generujuca
     * vsetky (utriedene) mnoziny vrcholov grafu g. Predpoklada sa, ze pred volanim metody reprezentuje zoznam
     * currentVertices nejaku utriedenu podmnozinu mnoziny vrcholov &lbrace;0,...,vertex-1&rbrace;. Metoda search
     * sa najprv pokusi pridat do zoznamu vrchol vertex a vygenerovat vsetky mnoziny obsahujuce vrcholy z tohto zoznamu;
     * nasledne sa pokusi vrchol vertex do zoznamu nepridat a taktiez vygeneruje vsetky taketo mnoziny.
     * Volanie metody pre vertex == 0 teda postupne vygeneruje vsetky utriedene mnoziny vrcholov grafu g. V pripade, ze
     * vertex == g.getNumberOfVertices(), povazuje sa mnozina vrcholov za vygenerovanu a zisti sa, ci ide o najvacsiu
     * doposial najdenu kliku v grafe g.
     * @param vertex Vrchol neorientovaneho grafu g alebo hodnota g.getNumberOfVertices().
     */
    private void search(int vertex) {
        if (vertex == g.getNumberOfVertices()) {
            if (isClique(currentVertices) && currentVertices.size() > maximumClique.size()) {
                maximumClique = new LinkedList<>(currentVertices);
            }
        } else {
            currentVertices.addLast(vertex);
            search(vertex + 1);
            currentVertices.removeLast();
            search(vertex + 1);
        }
    }

    /**
     * Pomocna metoda, ktora zisti, ci dane zoskupenie vrcholov zodpoveda klike v neorientovanom grafe g.
     * @param vertices Zoskupenie vrcholov grafu g.
     * @return         Metoda vrati true prave vtedy, ked vrcholy zoskupenia vertices tvoria kliku v grafe g.
     */
    private boolean isClique(Collection<Integer> vertices) {
        for (int u : vertices) {
            for (int v : vertices) {
                if (u != v && !g.existsEdge(u, v)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Metoda, ktora vrati najdenu najvacsiu kliku v neorientovanom grafe g ako utriedeny zoznam jej vrcholov.
     * @return Nemodifikovatelny zoznam obsahujuci vo vzostupnom poradi vsetky vrcholy najdenej najvacsej kliky.
     */
    public List<Integer> getMaximumClique() {
        return Collections.unmodifiableList(maximumClique);
    }
}

Použitie triedy MaximumClique:

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    System.out.println("Zadaj graf:");
    UndirectedGraph g = (UndirectedGraph) readGraph(scanner, GraphType.UNDIRECTED_ADJACENCY_LISTS);
  
    MaximumClique maximumClique = new MaximumClique(g);
    System.out.println("Najvacsia klika v zadanom grafe: " + maximumClique.getMaximumClique());
}

Uvažujme napríklad graf na nasledujúcom obrázku.

Graf6.png

Vyššie uvedený program preň vypíše nasledujúci výstup:

Najvacsia klika v zadanom grafe: [0, 1, 2, 4]

Mierne zrýchlenie hľadania najväčšej kliky

Pri hľadaní najväčšej kliky v neorientovanom grafe sme zakaždým prešli cez všetkých 2n podmnožín n-prvkovej množiny vrcholov grafu. Často je ale možné už behom prehľadávania rozoznať, že rozpracovanú množinu vrcholov nebude možné rozšíriť na najväčšiu kliku. Zapracujeme teda do prehľadávania s návratom v triede MaximumClique dve vylepšenia, ktoré síce nijak nezmenia časovú zložitosť algoritmu v najhoršom prípade, ale pre veľké množstvo grafov jeho vykonávanie podstatne urýchlia:

  • Vrchol budeme pridávať do rozpracovanej množiny iba v prípade, že je spojený hranou so všetkými vrcholmi v tejto množine – v opačnom prípade už totiž z tejto množiny, ani po prípadnom pridaní ďalších vrcholov, kliku nikdy nevytvoríme. V takom prípade bude po vygenerovaní kompletnej množiny garantované, že ide o kliku a kontrolu metódou isClique môžeme vypustiť.
  • Vrchol tiež budeme pridávať iba v prípade, že je počet jeho susedov – ktorý pre neorientované grafy bez slučiek možno nazvať aj stupňom vrcholu – väčší alebo rovný počtu vrcholov v doposiaľ najväčšej objavenej klike. Ak má totiž najväčšia doposiaľ objavená klika veľkosť k, má pridanie vrcholu do rozpracovanej množiny zmysel iba v prípade, že bude môcť byť súčasťou kliky veľkosti aspoň k+1; každý vrchol, ktorý je súčasťou takejto kliky, má evidentne aspoň k susedov.

Upravená trieda MaximumClique:

package graphs;

import java.util.*;

public class MaximumClique {
    private UndirectedGraph g;
    private LinkedList<Integer> maximumClique;
    private LinkedList<Integer> currentVertices;

    public MaximumClique(UndirectedGraph g) {
        this.g = g;
        maximumClique = new LinkedList<>();
        currentVertices = new LinkedList<>();
        search(0);
    }

    private void search(int vertex) {
        if (vertex == g.getNumberOfVertices()) {
            if (currentVertices.size() > maximumClique.size()) {
                maximumClique = new LinkedList<>(currentVertices);
            }
        } else {
            if (hasEdgeToEach(vertex, currentVertices) && neighbourCount(vertex) >= maximumClique.size()) {
                currentVertices.addLast(vertex);
                search(vertex + 1);
                currentVertices.removeLast();
            }
            search(vertex + 1);
        }
    }

    private boolean hasEdgeToEach(int vertex, Collection<Integer> vertices) {
        for (int v : vertices) {
            if (!g.existsEdge(vertex, v)) {
                return false;
            }
        }
        return true;
    }

    private int neighbourCount(int vertex) {
        int result = 0;
        for (int v : g.outgoingEdgesDestinations(vertex)) {
            result++;
        }
        return result;
    }

    public List<Integer> getMaximumClique() {
        return Collections.unmodifiableList(maximumClique);
    }
}

Cvičenia:

  • Opíšte beh uvedeného programu na grafe obsahujúcom niekoľko vrcholov a žiadnu hranu.
  • Urýchlite uvedený algoritmus vhodnou implementáciou metódy neighbourCount už v triedach pre grafy (keďže bude potrebná implementácia aj pre orientované grafy, bude asi vhodnejšie premenovať metódu na successorCount a implementovať ju tak, aby pre každý vrchol vrátila počet jeho následníkov v orientovanom grafe).

Orientované acyklické grafy a topologické usporiadanie

Budeme teraz pracovať s orientovanými acyklickými grafmi – čiže orientovanými grafmi neobsahujúcimi žiaden cyklus. Takéto grafy sa zídu pri rôznych praktických úlohách, kde môžu reprezentovať napríklad časové závislosti – alebo prerekvizity – medzi vykonávanými činnosťami (celý proces je očividne vykonateľný práve vtedy, keď je tento orientovaný graf acyklický).

Príklad orientovaného acyklického grafu:

Graf7.png

Pod topologickým usporiadaním orientovaného acyklického grafu o vrcholoch 0,1,...,n-1 budeme rozumieť permutáciu π: {0,1,...,n-1} → {0,1,...,n-1} takú, že pre všetky hrany (u,v) grafu platí π(u) ≤ π(v). V princípe teda ide o úplné usporiadanie na množine vrcholov grafu (vrchol v vždy ide ako π(v)-ty v poradí) také, že všetky hrany v grafe idú „v smere tohto usporiadania”. Topologické usporiadanie teda napríklad môže určovať poradie, v ktorom možno vykonať jednotlivé činnosti tak, aby boli splnené všetky ich prerekvizity. Väčšinou budeme pod topologickým usporiadaním chápať priamo postupnosť vrcholov v poradí určenom príslušnou permutáciou, t. j. π-1(0), π-1(1), ..., π-1(n - 1). Pod topologickým triedením budeme rozumieť proces hľadania topologického usporiadania.

Jeden orientovaný acyklický graf môže mať aj viacero topologických usporiadaní. Napríklad graf na obrázku vyššie má presne štyri topologické usporiadania:

[5, 6, 4, 3, 1, 2, 0]
[6, 5, 4, 3, 1, 2, 0]
[5, 6, 4, 3, 2, 1, 0]
[6, 5, 4, 3, 2, 1, 0]

Poznámka: matematicky o niečo výstižnejšia ekvivalentná definícia topologického usporiadania využíva fakt, že orientované acyklické grafy sú práve tie orientované grafy bez slučiek, ktorých reflexívno-tranzitívny uzáver možno chápať ako čiastočné usporiadanie na množine vrcholov (neexistencia cyklu zodpovedá antisymetrii). V tomto zmysle teda každý orientovaný acyklický graf jednoznačne určuje čiastočné usporiadanie ⪯ na množine vrcholov grafu. Topologické usporiadanie je potom ľubovoľné úplné usporiadanie ≤ na tej istej množine vrcholov také, že ⪯ ⊆ ≤.

Veta: Každý orientovaný acyklický graf má aspoň jedno topologické usporiadanie.

Dôkaz. Indukciou vzhľadom na počet vrcholov. Graf o jednom vrchole v má evidentne topologické usporiadanie v. Predpokladajme teraz, že tvrdenie platí pre všetky grafy veľkosti n a uvažujme orientovaný acyklický graf veľkosti n+1. Ten musí obsahovať najmenej jeden vrchol, z ktorého nevychádza žiadna hrana – v opačnom prípade by totiž každý sled bolo možné predĺžiť na dlhší, pričom sled dĺžky n+1 nutne musí obsahovať cyklus. Nech má túto vlastnosť vrchol u. Podľa indukčného predpokladu potom existuje topologické usporiadanie grafu, ktorý z uvažovaného grafu získame odstránením vrcholu u a všetkých do neho vedúcich hrán (ten má totiž n vrcholov). Ak je týmto usporiadaním v1,...,vn, je v1,...,vn,u topologickým usporiadaním uvažovaného grafu. □

Naopak je zrejmé, že orientovaný graf obsahujúci cyklus iný ako slučku nemôže mať žiadne topologické usporiadanie (napríklad žiaden vrchol cyklu nemôže byť spomedzi týchto vrcholov v usporiadaní prvý).

Topologické triedenie grafu

Napíšeme teraz statickú metódu topologicalSort, ktorá realizuje topologické triedenie daného orientovaného acyklického grafu.

  • Vstupom metódy je orientovaný graf.
  • V prípade, že je tento graf acyklický, vráti metóda na výstupe zoznam vrcholov zodpovedajúci nejakému jeho topologickému usporiadaniu.
  • V opačnom prípade metóda vráti na výstupe referenciu null.

Samotný algoritmus si bude v zozname edgesFromUnprocessedVertices pre každý vrchol pamätať počet vrcholov, z ktorých do daného vrcholu vedie hrana a ktoré ešte neboli pridané do topologického usporiadania. Ak je tento počet pre nejaký vrchol nulový, možno ho pridať ako ďalší vrchol do topologického usporiadania. Vrcholy s touto vlastnosťou sa budú udržiavať v rade ready (rovnako dobre ako rad by sme mohli použiť aj inú dátovú štruktúru).

public static List<Integer> topologicalSort(Graph g) {
    /* Inicializacia: */
    int n = g.getNumberOfVertices();
    List<Integer> edgesFromUnprocessedVertices = new ArrayList<>();
    for (int v = 0; v <= n - 1; v++) {
        edgesFromUnprocessedVertices.add(0);
    }
    for (int v = 0; v <= n - 1; v++) {
        for (int successor : g.outgoingEdgesDestinations(v)) {
            edgesFromUnprocessedVertices.set(successor, edgesFromUnprocessedVertices.get(successor) + 1);
        }
    }
    Queue<Integer> ready = new LinkedList<>();
    for (int v = 0; v <= n - 1; v++) {
        if (edgesFromUnprocessedVertices.get(v) == 0) {
            ready.add(v);
        }
    }
    List<Integer> result = new LinkedList<>();
 
    /* Samotne topologicke triedenie: */
    while (!ready.isEmpty()) {
        int vertex = ready.remove();
        result.add(vertex);
        for (int successor : g.outgoingEdgesDestinations(vertex)) {
            edgesFromUnprocessedVertices.set(successor, edgesFromUnprocessedVertices.get(successor) - 1);
            if (edgesFromUnprocessedVertices.get(successor) == 0) {
                ready.add(successor);
            }
        }
    }

    if (result.size() == n) {
        return result;
    } else {
        return null;
    }
}

Topologické triedenie na báze prehľadávania do hĺbky

Ukážme si ešte alternatívnu metódu topologického triedenia grafu, založenú na nasledujúcej úprave prehľadávania do hĺbky:

  • Okrem zoznamu visited si budeme udržiavať aj zoznam finished, v ktorom si pre každý vrchol grafu budeme pamätať, či prehľadávanie v ňom započaté už skončilo.
  • Pri objavení vrcholu teda nastavíme na true príslušnú položku zoznamu visited a po vykonaní všetkých rekurzívnych volaní metódy search pre susedov daného vrcholu nastavíme na true aj príslušnú položku zoznamu finished.

V každom momente vykonávania takto upraveného prehľadávania do hĺbky teda existujú tri množiny vrcholov:

  • Už spracované vrcholy, pre ktoré sú rovné true príslušné položky v zozname visited aj v zozname finished.
  • „Rozrobené” vrcholy, pre ktoré je príslušná položka v zozname visited rovná true, ale príslušná položka v zozname finished je rovná false.
  • Ešte nenavštívené vrcholy, pre ktoré sú príslušné položky v zozname visited aj v zozname finished rovné false.

Ľahko pritom vidieť, že graf obsahuje cyklus práve vtedy, keď počas prehľadávania grafu do hĺbky z niektorého vrcholu objavíme „rozrobený” vrchol.

Pre účely topologického triedenia je kľúčová nasledujúca vlastnosť:

  • Kedykoľvek v orientovanom acyklickom grafe skončí prehľadávanie do hĺbky z niektorého vrcholu v skôr, než prehľadávanie do hĺbky z vrcholu u, nemôže v tomto grafe viesť žiadna cesta z vrcholu v do vrcholu u. Ak totiž takáto cesta existuje, sú iba nasledujúce možnosti:
    • Vrchol u je na začiatku prehľadávania z vrcholu v nenavštívený. V takom prípade ho musíme, ako ešte nenavštívený vrchol dosiahnuteľný z v, navštíviť v rámci prehľadávania z v. Potom však prehľadávanie z vrcholu u skončí skôr, než prehľadávanie z vrcholu v: spor s predpokladom.
    • Vrchol u je na začiatku prehľadávania z vrcholu v „rozrobený”. To znamená, že sme vrchol v objavili v rámci prehľadávania spusteného vo vrchole u a z u do v teda vedie orientovaná cesta. Spojením tejto cesty s orientovanou cestou z v do u potom dostávame uzavretý sled: spor s predpokladom acyklickosti.
    • Vrchol u je na začiatku prehľadávania z vrcholu v už spracovaný: spor s predpokladom, že prehľadávanie z v skončí skôr, než prehľadávanie z u.
  • To znamená, že kedykoľvek prehľadávanie z nejakého vrcholu ukončíme, môžeme ho pridať na začiatok topologicky usporiadanej postupnosti vrcholov grafu.
package graphs;

import java.util.*;

/**
 * Trieda realizujuca topologicke triedenie pomocou prehladavania do hlbky.
 */
public class TopologicalSort {
    /**
     * Graf, v ktorom sa topologicke triedenie realizuje.
     */
    private Graph g;

    /**
     * Vysledne topologicke usporiadanie alebo jeho cast.
     */
    private LinkedList<Integer> topologicalOrder;

    /**
     * Pole, v ktorom si pre kazdy vrchol pamatame, ci bol prehladavanim do hlbky navstiveny.
     */
    private ArrayList<Boolean> visited;

    /**
     * Pole, v ktorom si pre kazdy vrchol pamatame, ci bolo prehladavanie z neho ukoncene.
     */
    private ArrayList<Boolean> finished;

    /**
     * Po vykonani konstruktora bude true prave vtedy, ked je graf g acyklicky.
     */
    private boolean acyclic = true;

    /**
     * Konstruktor, ktory pomocou prehladavania do hlbky najde niektore z topologickych usporiadani daneho grafu, resp.
     * v pripade grafov obsahujuci cyklus nastavi premennu acyclic na false.
     * @param g Graf, v ktorom sa topologicke triedenie realizuje.
     */
    public TopologicalSort(Graph g) {
        this.g = g;
        int n = g.getNumberOfVertices();
        topologicalOrder = new LinkedList<>();
        visited = new ArrayList<>();
        finished = new ArrayList<>();

        for (int v = 0; v <= n - 1; v++) {
            visited.add(false);
            finished.add(false);
        }

        for (int v = 0; v <= n - 1; v++) {
            if (!visited.get(v)) {
                search(v);
            }
        }
    }

    /**
     * Metoda realizujuca samotne prehladavanie do hlbky z vrcholu vertex.
     * @param vertex Vrchol, z ktoreho sa prehladavanie do hlbky spusta.
     */
    private void search(int vertex) {
        visited.set(vertex, true);
        for (int successor : g.outgoingEdgesDestinations(vertex)) {
            if (visited.get(successor) && !finished.get(successor)) {
                acyclic = false;
            }
            if (!visited.get(successor)) {
                search(successor);
            }
        }
        finished.set(vertex, true);
        topologicalOrder.addFirst(vertex);
    }

    /**
     * Metoda, ktora vrati najdene topologicke usporiadanie grafu.
     * @return Topologicke usporiadanie grafu ako nemodifikovatelny zoznam vrcholov. V pripade, ze graf nie je
     *         acyklicky, vrati sa na vystupe referencia null.
     */
    public List<Integer> getTopologicalOrder() {
        if (acyclic) {
            return Collections.unmodifiableList(topologicalOrder);
        } else {
            return null;
        }
    }

    /**
     * Metoda, ktora vrati informaciu o tom, ci je graf g acyklicky.
     * @return Vrati booleovsku hodnotu true prave vtedy, ked je graf g acyklicky.
     */
    public boolean isAcyclic() {
        return acyclic;
    }
}

Úlohy na ohodnotených grafoch

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 WeightedSuccessorListsGraph reprezentujúcu orientované ohodnotené grafy pomocou zoznamov následníkov (podobným spôsobom ako na minulých prednáškach by sme však mohli napísať aj triedy ako WeightedAdjacencyMatrixGraph, WeightedAdjacencyListsUndirectedGraph a podobne).

Rozhranie pre ohodnotené grafy (WeightedGraph)

package graphs;

/**
 * Trieda reprezentujuca ohodnotenu hranu.
 */
public class WeightedEdge extends Edge {
    private double weight;

    public WeightedEdge(int from, int to, double weight) {
        super(from, to);
        this.weight = weight;
    }

    public double getWeight() {
        return weight;
    }

    @Override
    public boolean equals(Object o) {
        return super.equals(o) && ((WeightedEdge) o).weight == weight;
    }

    @Override
    public int hashCode() {
        return Double.valueOf(weight).hashCode() + 31 * super.hashCode();
    }
}
package graphs;

import java.util.*;

public class WeightedEdges {

    public static Collection<Edge> unweightedEdgeCollection(Collection<WeightedEdge> weightedEdges) {
        ArrayList<Edge> result = new ArrayList<>();
        for (WeightedEdge we : weightedEdges) {
            result.add(new Edge(we.getFrom(), we.getTo()));
        }
        return result;
    }
}
package graphs;

/**
 * Rozhranie pre ohodnoteny (vo vseobecnosti orientovany) graf.
 */
public interface WeightedGraph extends Graph {
    /**
     * Metoda, ktora vrati vsetky ohodnotene hrany vychadzajuce z vrcholu vertex reprezentovaneho ohodnoteneho grafu.
     * @param vertex Vrchol ohodnoteneho grafu.
     * @return       Ohodnotene hrany vychadzajuce z vrcholu vertex ako instancia typu Iterable&lt;WeightedEdge&rt;.
     */
    Iterable<WeightedEdge> outgoingWeightedEdges(int vertex);
}

Orientované ohodnotené grafy pomocou zoznamov následníkov (WeightedSuccessorListsGraph)

Triedu WeightedSuccessorListsGraph reprezentujúcu ohodnotený orientovaný graf pomocou zoznamov následníkov napíšeme jednoduchým rozšírením príslušnej triedy SuccessorListsGraph pre neohodnotené grafy. Navyše si len pre každý vrchol budeme pamätať zoznam z neho vychádzajúcich ohodnotených hrán.

package graphs;

import java.util.*;

/**
 * Trieda reprezentujuca ohodnoteny orientovany graf pomocou zoznamov naslednikov.
 */
public class WeightedSuccessorListsGraph extends SuccessorListsGraph implements WeightedGraph {
    /**
     * Zoznamy ohodnotenych hran vychadzajucich z jednotlivych vrcholov.
     */
    private ArrayList<ArrayList<WeightedEdge>> outgoingWeightedEdges;

    /**
     * Konstruktor, ktory vytvori ohodnoteny graf s danym poctom vrcholov a obsahujuci hrany podla daneho zoskupenia.
     * @param vertexCount   Pocet vrcholov vytvaraneho grafu.
     * @param weightedEdges Ohodnotene hrany vytvaraneho grafu. Zoskupenie nesmie obsahovat viacero hran s roznymi
     *                      ohodnoteniami veduce medzi rovnakymi dvojicami vrcholov.
     */
    public WeightedSuccessorListsGraph(int vertexCount, Collection<WeightedEdge> weightedEdges) {
        super(vertexCount, WeightedEdges.unweightedEdgeCollection(weightedEdges));
        outgoingWeightedEdges = new ArrayList<>();
        for (int i = 0; i <= vertexCount - 1; i++) {
            outgoingWeightedEdges.add(new ArrayList<>());
        }
        Set<WeightedEdge> processedWeightedEdges = new HashSet<>();
        Set<Edge> processedUnweightedEdges = new HashSet<>();
        for (WeightedEdge we : weightedEdges) {
            if (!processedUnweightedEdges.contains(new Edge(we.getFrom(), we.getTo()))) {
                outgoingWeightedEdges.get(we.getFrom()).add(we);
                processedWeightedEdges.add(we);
                processedUnweightedEdges.add(new Edge(we.getFrom(), we.getTo()));
            } else if (!processedWeightedEdges.contains(we)) {
                throw new IllegalArgumentException("Multiple edges with different weights connecting the same" +
                        " pair of vertices.");
            }
        }
    }

    @Override
    public Iterable<WeightedEdge> outgoingWeightedEdges(int vertex) {
        return Collections.unmodifiableList(outgoingWeightedEdges.get(vertex));
    }
}

Hľadanie najdlhšej cesty v ohodnotenom grafe

Pod dĺžkou cesty v ohodnotenom grafe teraz budeme rozumieť súčet ohodnotení jej hrán. Podobne ako pre neohodnotené grafy potom možno najdlhšiu cestu medzi dvoma vrcholmi nájsť pomocou prehľadávania s návratom.

package graphs;

import java.util.*;

/**
 * Trieda realizujuca najdenie najdlhsej ohodnotenej cesty v ohodnotenom grafe z daneho pociatocneho do daneho koncoveho
 * vrcholu.
 */
public class LongestWeightedPath {
    /**
     * Ohodnoteny graf, v ktorom sa hladanie najdlhsej cesty realizuje.
     */
    private WeightedGraph g;

    /**
     * Pociatocny vrchol hladanej najdlhsej cesty.
     */
    private int from;

    /**
     * Koncovy vrchol hladanej najdlhsej cesty.
     */
    private int to;

    /**
     * Dlzka vygenerovanej casti cesty.
     */
    private double length;

    /**
     * Dlzka doposial najdlhsej najdenej cesty z vrcholu from do vrcholu to.
     */
    private double maxLength;

    /**
     * Zoznam, v ktorom sa postupne budu cesty generovat.
     */
    private LinkedList<Integer> path;

    /**
     * Doposial najdlhsia najdena cesta z vrcholu from do vrcholu to.
     */
    private LinkedList<Integer> longestWeightedPath;

    /**
     * Informacie o navstiveni jednotlivych vrcholov pri prehladavani s navratom.
     */
    private ArrayList<Boolean> visited;

    /**
     * Konstruktor, ktory najde najdlhsiu ohodnotenu cestu medzi danymi dvoma vrcholmi daneho ohodnoteneho grafu.
     * @param g    Graf, v ktorom sa hladanie ciest realizuje.
     * @param from Pociatocny vrchol.
     * @param to   Koncovy vrchol.
     */
    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 = Integer.MIN_VALUE;
        length = 0;
        path = new LinkedList<>();
        path.add(from);
        visited.set(from, true);
        search();
    }

    /**
     * Metoda realizujuca samotne prehladavanie s navratom.
     */
    private void search() {
        if (path.getLast() == to) {
            if (length > maxLength) {
                maxLength = length;
                longestWeightedPath = new LinkedList<>(path);
            }
        } else {
            for (WeightedEdge we : g.outgoingWeightedEdges(path.getLast())) {
                int successor = we.getTo();
                double weight = we.getWeight();
                if (!visited.get(successor)) {
                    visited.set(successor, true);
                    path.addLast(successor);
                    length += weight;
                    search();
                    length -= weight;
                    path.removeLast();
                    visited.set(successor, false);
                }
            }
        }
    }

    /**
     * Metoda, ktora vrati najdenu najdlhsiu ohodnotenu cestu.
     * @return Jedna z najdlhsich ohodnotenych ciest z from do to ako nemodifikovatelny zoznam. V pripade, ze ziadna
     *         cesta medzi vrcholmi from a to neexistuje, vrati sa na vystupe referencia null.
     */
    public List<Integer> getLongestWeightedPath() {
        if (longestWeightedPath != null) {
            return Collections.unmodifiableList(longestWeightedPath);
        } else {
            return null;
        }
    }
}

Použitie tejto triedy:

public class Trieda {

    public static WeightedGraph readWeightedGraph(Scanner s) {
        int n = s.nextInt();
        int m = s.nextInt();
        ArrayList<WeightedEdge> weightedEdges = new ArrayList<>();
        for (int i = 1; i <= m; i++) {
            int u = s.nextInt();
            int v = s.nextInt();
            double weight = s.nextDouble();
            weightedEdges.add(new WeightedEdge(u, v, weight));
        }
        return new WeightedSuccessorListsGraph(n, weightedEdges);
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Zadaj ohodnoteny graf:");
        WeightedGraph g = readWeightedGraph(scanner);
        System.out.println("Zadaj pociatocny a koncovy vrchol:");
        int from = scanner.nextInt();
        int to = scanner.nextInt();

        LongestWeightedPath longestWeightedPath = new LongestWeightedPath(g, from, to);
        List<Integer> longest = longestWeightedPath.getLongestWeightedPath();
        if (longest != null) {
            System.out.println("Najdlhsia cesta: " + longest);
        } else {
            System.out.println("Ziadna cesta neexistuje.");
        }
    }
}

Najkratšie cesty v ohodnotených grafoch

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. Tieto predpokladajú neexistenciu cyklov zápornej dĺžky (čo je v praxi zmysluplný predpoklad), a teda v skutočnosti hľadajú najkratšie sledy. V prípade možnej existencie cyklov zápornej dĺžky efektívny algoritmus, rovnako ako pri najdlhšej ceste, s najväčšou pravdepodobnosťou neexistuje.

Zhrnutie prebratých grafových algoritmov

Väčšina grafových algoritmov (s výnimkou prvého algoritmu pre topologické triedenie), s ktorými sme sa v rámci posledných niekoľkých prednášok stretli, bola založená na niektorej z nasledujúcich troch techník:

  • Prehľadávanie do hĺbky (angl. depth-first search).
  • Prehľadávanie do šírky (angl. breadth-first search).
  • Prehľadávanie s návratom (angl. backtracking).

Prvé dve z týchto techník sú pomerne efektívne – je pri nich zaručené, že žiadna hrana nebude pri „objavovaní” nových vrcholov použitá viac, než raz. To znamená, že časová zložitosť týchto algoritmov je rádovo O(n + m), kde n je počet vrcholov a m je počet hrán grafu.

Prehľadávanie s návratom je naopak extrémne neefektívna technika prehľadávajúca všetky možnosti, ktorých vo všeobecnosti môže byť veľmi veľa. Dá sa teda aplikovať iba na malé vstupy, a to najmä vtedy, keď efektívny algoritmus pre danú úlohu neexistuje alebo nie je známy.

Riešenia testu č. 4

Úloha č. 1

public class LongestCycle {
    
    // ...

    private LinkedList<Integer> walk;
    private ArrayList<Boolean> visited;

    public LongestCycle(Graph g, int vertex) {
        this.g = g;
        this.vertex = vertex;

        walk = new LinkedList<>();
        visited = new ArrayList<>();
        for (int i = 0; i <= g.getNumberOfVertices() - 1; i++) {
            visited.add(false);
        }
        walk.add(vertex);
        search();
    }

    private void search() {
        if (walk.size() > 1 && walk.getLast() == vertex) {
            if (longestCycle == null || walk.size() > longestCycle.size()) {
                longestCycle = new LinkedList<>(walk);
            }
        } else {
            for (int successor : g.outgoingEdgesDestinations(walk.getLast())) {
                if (!visited.get(successor)) {
                    visited.set(successor, true);
                    walk.add(successor);
                    search();
                    walk.removeLast();
                    visited.set(successor, false);
                }
            }
        }
    }

    // ...
}

Úloha č. 2

public class DepthFirstSearch {

    public static Map<Integer, Integer> depthFirstSearch(Graph g, int vertex) {
        Map<Integer, Integer> result = new HashMap<>();
        search(g, vertex, result);
        return result;
    }

    private static void search(Graph g, int vertex, Map<Integer, Integer> orderOfDiscovery) {
        orderOfDiscovery.put(vertex, orderOfDiscovery.size() + 1);
        for (int successor : g.outgoingEdgesDestinations(vertex)) {
            if (!orderOfDiscovery.containsKey(successor)) {
                search(g, successor, orderOfDiscovery);
            }
        }
    }

    // ...
}

Úloha č. 3

// ...

else {
    for (int colour = 0; colour <= colours - 1; colour++) {
        boolean isAdmissible = true;
        for (int neighbour : g.outgoingEdgesDestinations(vertex)) {
            if (neighbour == vertex || (neighbour < vertex && colouring.get(neighbour) == colour)) {
                isAdmissible = false;
            }
        }
        if (isAdmissible) {
            colouring.set(vertex, colour);
            search(vertex + 1);
        }
    }
}

Letný semester, prednáška č. 10

Oznamy

  • Stredajšie cvičenia budú aj napriek dekanskému voľnu (účasť je samozrejme dobrovoľná, rovnako ako aj na ostatných cvičeniach, na ktorých sa nepíše test). Bude zverejnených niekoľko nebodovaných úloh zameraných na látku z dnešnej prednášky (úlohy na JavaFX sa nebudú odovzdávať na testovač).
  • Štvrtú domácu úlohu treba odovzdať do pondelka 26. apríla, 9:00 a tretiu bonusovú úlohu do stredy 28. apríla, 11:30.
  • Najneskôr na budúcej prednáške, prosím, dajte vedieť o prípadných námietkach proti vypísaným termínom skúšky.

Lambda výrazy

Príklad: komparátor ako lambda výraz

Pripomeňme si z piatej prednášky zostupné triedenie zoznamu celých čísel pomocou metódy Collections.sort, ktorá ako druhý parameter berie komparátor, čiže inštanciu nejakej triedy implementujúcej rozhranie Comparator<? super Integer>. Videli sme pritom tri možné spôsoby, ako komparátor definovať:

  • Vytvoriť „bežnú” triedu implementujúcu rozhranie Comparator<Integer> a metódu sort zavolať pre novovytvorenú inštanciu tejto triedy.
  • To isté s použitím lokálnej triedy:
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        class DualComparator implements Comparator<Integer> {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        }
        List<Integer> a = new ArrayList<>();
        a.add(6);
        a.add(1);
        a.add(3);
        a.add(2);
        a.add(3);
        Collections.sort(a, new DualComparator());
        System.out.println(a);
    }
}
  • Definíciu triedy a vytvorenie jej inštancie spojiť do jediného príkazu s využitím mechanizmu anonymných tried:
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        List<Integer> a = new ArrayList<>();
        a.add(6);
        a.add(1);
        a.add(3);
        a.add(2);
        a.add(3);
        Collections.sort(a, new Comparator<>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        System.out.println(a);
    }
}

Aj posledný z uvedených spôsobov definície komparátora je však stále trochu ťažkopádny – jediná pre účely triedenia podstatná informácia je tu daná riadkom return o2.compareTo(o1);, pričom zvyšok konštrukcie by bol rovnaký aj pri definícii ľubovoľného iného komparátora.

Rozhranie Comparator<E> je príkladom takzvaného funkcionálneho rozhrania – čiže rozhrania, ktoré deklaruje jedinú abstraktnú metódu; v tomto prípade ide o metódu compare. Toto rozhranie síce definuje niekoľko ďalších statických metód a metód s modifikátorom default; jedinou metódou, ktorú je potrebné implementovať kedykoľvek implementujeme rozhranie Comparator<E>, je ale metóda compare. To okrem iného znamená, že proces vytvárania tried implementujúcich rozhranie Comparator<E> sa často môže redukovať iba na implementáciu tejto jednej metódy.

Ako skrátený zápis pre definíciu inštancie anonymnej triedy implementujúcej funkcionálne rozhranie, obsahujúcej iba definíciu jedinej abstraktnej metódy deklarovanej v tomto rozhraní, slúžia v Jave takzvané lambda výrazy. Utriedenie poľa tak možno realizovať pomocou príkazu

Collections.sort(a, (o1, o2) -> {return o2.compareTo(o1);});

alebo pomocou ešte kratšieho príkazu

Collections.sort(a, (o1, o2) -> o2.compareTo(o1));

V zátvorkách pred šípkou sú postupne uvedené identifikátory argumentov metódy compare a za šípkou nasleduje blok obsahujúci telo metódy compare, prípadne výraz udávajúci výstupnú hodnotu tejto metódy.

Syntax lambda výrazov v Jave

Lambda výraz je teda skráteným zápisom anonymnej triedy implementujúcej funkcionálne rozhranie, ako aj jej inštancie. Alternatívne ich možno považovať aj za reprezentáciu implementácie abstraktnej metódy v tomto rozhraní deklarovanej; preto sa v súvislosti s lambda výrazmi často hovorí aj o anonymných funkciách. Pomenovanie „lambda výraz” odkazuje na lambda kalkul – formalizmus, v ktorom sa podobným spôsobom definujú matematické funkcie (napríklad λx.x2 + 5 je funkcia f daná predpisom f: x ↦ x2 + 5) a ktorý je okrem iného aj teoretickým základom funkcionálneho programovania.

V prípade, že má jediná abstraktná metóda deklarovaná v implementovanom funkcionálnom rozhraní hlavičku

T f(T1 arg1, T2 arg2, ..., Tn argn);

môže byť lambda výraz reprezentujúci inštanciu anonymnej triedy implementujúcej toto rozhranie tvaru

(arg1, arg2, ..., argn) -> { /* Telo implementacie metody f */ }

Pred jednotlivé argumenty možno nepovinne uvádzať aj ich typy. V prípade, že je počet argumentov metódy f nulový, píšeme

() -> { /* Telo implementacie metody f */ }

a v prípade, že ide o metódu s jediným argumentom arg, možno vynechať zátvorky okolo argumentov a písať iba

arg -> { /* Telo implementacie metody f */ }

V prípade, že návratový typ T metódy f nie je void, možno na pravej strane lambda výrazu namiesto bloku obsahujúceho telo implementovanej metódy uviesť iba výraz, ktorý sa vyhodnotí na typ T; v takom prípade bude metóda f počítať tento výraz. V prípade, že návratovým typom metódy f je void, možno namiesto bloku s telom implementovanej metódy uviesť volanie jednej metódy s návratovým typom void (ktoré možno z určitého pohľadu chápať ako „výraz typu void”), prípadne volanie jednej metódy s iným návratovým typom, pričom sa však návratová hodnota tejto metódy odignoruje.

Anotácia @FunctionalInterface používaná v nasledujúcich príkladoch je nepovinná (všetko by rovnako dobre fungovalo aj bez nej), ale odporúčaná – kompilátor totiž vyhodí chybu kedykoľvek je táto anotácia použitá inde ako pri funkcionálnom rozhraní.

Príklad 1:

@FunctionalInterface
public interface MyFunctionalInterface {
    int f(int a, int b);
}
public class Trieda {

    public static void main(String[] args) {
        MyFunctionalInterface instance1 = (a, b) -> {
            if (a < b) {
                return 0;
            }
            return a + b;
        };
        MyFunctionalInterface instance2 = (a, b) -> a * b;

        System.out.println(instance1.f(2, 3));
        System.out.println(instance2.f(2, 3));
    }
}

Príklad 2:

@FunctionalInterface
public interface MyFunctionalInterface {
    int f(int a);
}
public class Trieda {

    public static void main(String[] args) {
        MyFunctionalInterface instance1 = (a) -> {return a + 1;};
        MyFunctionalInterface instance2 = a -> {return a - 1;};
        MyFunctionalInterface instance3 = x -> x + 3;

        System.out.println(instance1.f(2));
        System.out.println(instance2.f(2));
        System.out.println(instance3.f(2));
    }
}

Príklad 3:

@FunctionalInterface
public interface MyFunctionalInterface {
    void f();
}
public class Trieda {

    public static void main(String[] args) {
        MyFunctionalInterface instance1 = () -> {System.out.println("Hello, lambda!");};
        MyFunctionalInterface instance2 = () -> System.out.println("Hello, lambda!");

        instance1.f();
        instance2.f();
    }
}

Rovnako ako z lokálnych a anonymných tried, možno aj z definícií lambda výrazov pristupovať k premenným inštancií, či k finálnym alebo „v podstate finálnym” lokálnym premenným metód, ktorých sú súčasťou.

Ďalší príklad použitia lambda výrazov

Rozhranie Iterable<T> deklaruje okrem iného aj metódu forEach, ktorá postupne pre všetky prvky vracané iterátorom vykoná danú akciu a predstavuje tak alternatívu k použitiu cyklu for each. Akcia, ktorá sa má pre všetky prvky vykonať, je daná inštanciou nejakej triedy implementujúcej funkcionálne rozhranie Consumer<T> z balíka java.util.function. V ňom je deklarovaná jediná abstraktná metóda accept s návratovým typom void a jediným argumentom typu T. Metóda forEach zavolá túto metódu accept postupne pre všetky prvky vracané iterátorom.

Príklad: Uvažujme napríklad nasledujúci kód postupne vypisujúci všetky prvky zoznamu a.

List<Integer> a = new ArrayList<>();

// ...

for (int x : a) {
    System.out.print(x + " ");
}

Pomocou metódy forEach a anonymnej triedy môžeme rovnaké vypísanie prvkov zoznamu napísať aj nasledovne.

List<Integer> a = new ArrayList<>();
    
// ...

a.forEach(new Consumer<>() {
    @Override
    public void accept(Integer x) {
        System.out.print(x + " ");
    }
});

Keďže je rozhranie Consumer funkcionálne, môžeme namiesto anonymnej triedy použiť lambda výraz.

List<Integer> a = new ArrayList<>();

// ...

a.forEach(x -> System.out.print(x + " "));

Viacero ďalších funkcionálnych rozhraní, ktorých inštancie možno definovať pomocou lambda výrazov, je definovaných v balíku java.util.function.

Referencie na metódy

Občas sa stáva, že lambda výraz pozostáva iba z priameho volania nejakej už pomenovanej metódy. Pre takéto lambda výrazy existuje v Jave špeciálna syntax umožňujúca vyjadriť toto správanie o niečo stručnejším spôsobom. Lambda výrazy zadané pomocou tejto syntaxe sa nazývajú aj referenciami na metódy.

Pre triedu Trieda poskytujúcu statickú metódu statickaMetoda(T1 arg1, ..., Tn argn) a nestatickú metódu nestatickaMetoda(U1 arg1, ..., Um argm) a pre inštanciu instancia triedy Trieda možno písať:

  • Trieda::statickaMetoda namiesto lambda výrazu (arg1, ..., argn) -> Trieda.statickaMetoda(arg1, ..., argn),
  • instancia::nestatickaMetoda namiesto lambda výrazu (arg1, ..., argm) -> instancia.nestatickaMetoda(arg1, ..., argm),
  • Trieda::nestatickaMetoda namiesto lambda výrazu (arg, arg1, ..., argm) -> arg.nestatickaMetoda(arg1, ..., argm), kde arg je inštancia typu Trieda.

Príklad: Keby sme v príklade uvedenom vyššie prvky x zoznamu a vypisovali namiesto metódy System.out.print(x + " ") metódou System.out.println(x), mohli by sme celé vypisovanie zoznamu prepísať aj nasledovne.

List<Integer> a = new ArrayList<>();

// ...

a.forEach(System.out::println);

Úvod do JavaFX

JavaFX je platforma, ktorú možno využiť 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).

  • Pokyny k inštalácii JavaFX, ako aj návod na skompilovanie a spustenie prvého programu z IntelliJ IDEA a z príkazového riadku možno nájsť tu.
  • Dokumentácia k JavaFX 15 API.
  • Ďalšie dokumentácie a tutoriály možno nájsť na stránke projektu.

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:

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;

public class HelloFX 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 inštancia triedy 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 daná predovšetkým hierarchickým stromom 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 HelloFX, o vytvorenie hlavného grafického okna aplikácie, ako aj o následné zavolanie metódy start, ktorá dostane vytvorené okno ako argument.

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.

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:

import javafx.scene.control.*;

// ...

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:

import javafx.scene.text.*; // Kvoli triede Font.

// ...

label1.setFont(Font.font("Tahoma", FontWeight.BOLD, 12));
label1.setLayoutX(20);
label1.setLayoutY(10);

Analogicky vytvoríme aj ostatné komponenty:

import javafx.scene.paint.*; // Kvoli triede Color.

// ...

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.

Obidva tieto nedostatky napravíme 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 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.

import javafx.event.*;

// ...

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<>() {
        @Override
        public void handle(ActionEvent event) {
            label2.setText(textField.getText());
        }
    });

    // ...

}

Ak si navyše uvedomíme, že je rozhraní EventHandler deklarovaná jediná abstraktná metóda – ide teda o funkcionálne rozhranie – môžeme tento zápis ešte ďalej zjednodušiť použitím lambda výrazu.

public void start(Stage primaryStage) {
    
    // ...

    button.setOnAction(event -> 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.
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.shape.*;
import javafx.scene.paint.*;
import java.util.*;

public class Aplikacia extends Application {

    private Color randomColour(Random random) {
        return Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble());
    }

    @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;
                Color colour = randomColour(random);
                if (random.nextBoolean()) {
                    shape = new Rectangle(j * 60 + 5, i * 60 + 5, 50, 50);
                } else {
                    shape = new Circle(j * 60 + 30, i * 60 + 30, 25);
                }
                shape.setFill(colour);
                shape.setOnMouseClicked(event -> shape.setFill(randomColour(random)));
                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);
    }
}

Odkazy

Letný semester, prednáška č. 11

Oznamy

  • Do začiatku stredajších cvičení treba odovzdať tretiu bonusovú úlohu.
  • Dnes od 11:00 je v AISe otvorené prihlasovanie na skúšky.
  • Každý termín skúšky bude pozostávať z praktickej a ústnej časti (v ten istý deň):
    • Praktická časť: individuálne riešenie troch programátorských úloh a ich odovzdávanie na testovač. Je potrebné získať aspoň 15 bodov z 30. Čas na riešenie praktickej časti skúšky bude 135 minút.
    • Najneskôr pol hodinu pred avizovaným začiatkom ústnych skúšok sa na testovači u všetkých zúčastnených študentov objavia body z praktickej časti a v komentári aj očakávaný čas začiatku ústnej skúšky.
    • Ústna časť: diskusia o teórii z prednášok alebo o odovzdaných programoch, prípadne riešenie jednoduchých úloh. Účasť na ústnej skúške je možná iba v prípade zisku aspoň 15 bodov z praktickej časti a 50 bodov zo semestra celkom; aj v prípade neúspechu je ale možné čas vyhradený na ústnu skúšku využiť na konzultáciu.
  • Po dnešnej prednáške bude na testovači zverejnené zadanie piatej domácej úlohy s termínom odovzdania do pondelka 10. mája, 9:00, za ktorú bude možné získať aj dva bonusové body.
  • Počas cvičení v stredu 28. apríla bude prebiehať piaty test. Počas nasledujúcich dvoch cvičení budú prebiehať náhradné termíny testov.

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 podobná kostra programu ako na minulej prednáške – jedine rozmery scény už nebudeme nastavovať manuálne, pretože by to neskôr mohlo viesť k rozličným problémom.

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

Následne si v prípade práce s IntelliJ vyberme jednu z nasledujúcich možností:

  • Súbor uložme do adresára <Koreňový adresár projektu>/src. To je najjednoduchšia možnosť, ktorá ale hlavne pri väčších projektoch môže zneprehľadniť súborovú štruktúru projektu.
  • V koreňovom adresári projektu – t. j. na rovnakej úrovni ako je adresár src – vytvorme nový adresár, ktorý môžeme nazvať napríklad resources. Následne v IntelliJ na tento adresár kliknime pravou myšou a označme ho prostredníctvom možnosti Mark Directory as --> Resources Root. Súbor styles.css uložme do tohto novovytvoreného adresára.

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("styles.css");
        
    // ...
}

Ako argument metódy add tu môžeme zadať aj cestu relatívnu od niektorého z umiestnení opísaných vyššie, prípadne adresu URL.

Scéna a strom uzlov

Obsah JavaFX scény sa reprezentuje v podobe tzv. stromu uzlov alebo grafu uzlov. (Druhý termín je o niečo presnejší, pretože v skutočnosti môže ísť o niekoľko stromov tvoriacich les, pričom viditeľné sú uzly iba jedného z nich.)

  • 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ódy 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 lblNumber1 = new Label("Prvý argument:");
grid.add(lblNumber1, 0, 1);

Label lblNumber2 = new Label("Druhý argument:");
grid.add(lblNumber2, 0, 2);

Label lblOperation = new Label("Operácia:");
grid.add(lblOperation, 0, 3);

Label lblResultText = new Label("Výsledok:");
grid.add(lblResultText, 0, 5);

Label lblResult = new Label("0");
grid.add(lblResult, 1, 5);

TextField tfNumber1 = new TextField();
grid.add(tfNumber1, 1, 1);

TextField tfNumber2 = new TextField();
grid.add(tfNumber2, 1, 2);

ComboBox<String> cbOperation = new ComboBox<>();
grid.add(cbOperation, 1, 3);
cbOperation.getItems().addAll("+", "-", "*", "/");
cbOperation.setValue("+");

Button btnOK = new Button("Počítaj!");
grid.add(btnOK, 1, 4);

Novým prvkom je tu „vyskakovací zoznam” ComboBox<T> prvkov typu T. 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ť):

tfNumber1.setPrefWidth(300);
tfNumber2.setPrefWidth(300);
cbOperation.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:

// tfNumber1.setPrefWidth(300);
// tfNumber2.setPrefWidth(300);
// cbOperation.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”:

cbOperation.setMaxWidth(Integer.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(lblNumber1, HPos.RIGHT);
GridPane.setHalignment(lblNumber2, HPos.RIGHT);
GridPane.setHalignment(lblOperation, HPos.RIGHT);
GridPane.setHalignment(lblResultText, HPos.RIGHT);
GridPane.setHalignment(lblResult, HPos.RIGHT);
GridPane.setHalignment(btnOK, HPos.RIGHT);

S návrhom rozloženia ovládacích prvkov v mriežke 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, dolny, lavy)
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í prvkov v pravej časti okna.

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.

public class Calculator extends Application {

    // ...

    /**
     * Metoda, ktora aplikuje na argumenty arg1, arg2 operaciu reprezentovanu retazcom op.
     * @param operation Jeden z retazcov "+", "-", "*", "/" reprezentujucich vykonavanu operaciu.
     * @param arg1      Prvy operand.
     * @param arg2      Druhy operand.
     * @return          Vysledok operacie operation aplikovanej na operandy arg1, arg2.
     */
    private double calculate(String operation, double arg1, double arg2) {
        switch (operation) {
            case "+":
                return arg1 + arg2;
            case "-":
                return arg1 - arg2;
            case "*":
                return arg1 * arg2;
            case "/":
                return arg1 / arg2;
            default:
                throw new IllegalArgumentException();
        }
    }

    // ...

    @Override
    public void start(Stage primaryStage) {

        // ...
    
        btnOK.setOnAction(event -> {
            try {
                lblResult.setText(Double.toString(calculate(
                        cbOperation.getValue(),
                        Double.parseDouble(tfNumber1.getText()),
                        Double.parseDouble(tfNumber2.getText()))));
            } 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(event -> {
        tfNumber1.clear();
        tfNumber2.clear();
        cbOperation.setValue("+");
        lblResult.setText("0");
    });
        
    btnExit.setOnAction(event -> {
        Platform.exit();
    });
    
    // ...
}

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:

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 Calculator extends Application {

    /**
     * Metoda, ktora aplikuje na argumenty arg1, arg2 operaciu reprezentovanu retazcom op.
     * @param operation Jeden z retazcov "+", "-", "*", "/" reprezentujucich vykonavanu operaciu.
     * @param arg1      Prvy operand.
     * @param arg2      Druhy operand.
     * @return          Vysledok operacie operation aplikovanej na operandy arg1, arg2.
     */
    public double calculate(String operation, double arg1, double arg2) {
        switch (operation) {
            case "+":
                return arg1 + arg2;
            case "-":
                return arg1 - arg2;
            case "*":
                return arg1 * arg2;
            case "/":
                return arg1 / arg2;
            default:
                throw new IllegalArgumentException();
        }
    }

    @Override
    public void start(Stage primaryStage) {
        GridPane grid = new GridPane();

        Label lblHeader = new Label("Zadajte vstupné hodnoty:");
        grid.add(lblHeader, 0, 0, 2, 1);
        lblHeader.setId("header");

        Label lblNumber1 = new Label("Prvý argument:");
        grid.add(lblNumber1, 0, 1);
        GridPane.setHalignment(lblNumber1, HPos.RIGHT);

        Label lblNumber2 = new Label("Druhý argument:");
        grid.add(lblNumber2, 0, 2);
        GridPane.setHalignment(lblNumber2, HPos.RIGHT);

        Label lblOperation = new Label("Operácia:");
        grid.add(lblOperation, 0, 3);
        GridPane.setHalignment(lblOperation, 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 tfNumber1 = new TextField();
        grid.add(tfNumber1, 1, 1);

        TextField tfNumber2 = new TextField();
        grid.add(tfNumber2, 1, 2);

        ComboBox<String> cbOperation = new ComboBox<>();
        grid.add(cbOperation, 1, 3);
        cbOperation.getItems().addAll("+", "-", "*", "/");
        cbOperation.setValue("+");
        cbOperation.setMaxWidth(Integer.MAX_VALUE);

        Button btnOK = new Button("Počítaj!");
        grid.add(btnOK, 1, 4);
        GridPane.setHalignment(btnOK, HPos.RIGHT);

        ColumnConstraints cc = new ColumnConstraints();
        cc.setPrefWidth(300);
        cc.setHgrow(Priority.ALWAYS);
        grid.getColumnConstraints().addAll(new ColumnConstraints(), cc);

        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(10,20,10,20));
        grid.setAlignment(Pos.CENTER);


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


        btnOK.setOnAction(event -> {
            try {
                lblResult.setText(Double.toString(calculate(
                        cbOperation.getValue(),
                        Double.parseDouble(tfNumber1.getText()),
                        Double.parseDouble(tfNumber2.getText()))));
            } catch (NumberFormatException exception) {
                lblResult.setText("Výnimka!");
            }
        });

        btnClear.setOnAction(event -> {
            tfNumber1.setText("");
            tfNumber2.setText("");
            cbOperation.setValue("+");
            lblResult.setText("0");
        });

        btnExit.setOnAction(event -> {
            Platform.exit();
        });


        Scene scene = new Scene(border);
        scene.getStylesheets().add("styles.css");

        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.

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.

package events;

import javafx.event.*;

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

package events;

import javafx.event.*;

public 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.
package events;

import java.util.*;
import javafx.event.*;

public class MyEventSender {
    private String name;
    private 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 getName inštancie triedy MyEventSender.

public 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());
        }
    }
}

Trieda s metódou main potom môže vyzerať napríklad nasledovne.

package events;

public class SimpleEvents {

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

public 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();
    }
}
public 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 vstupným argumentom 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.

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.shape.*;
import javafx.scene.paint.*;
import javafx.animation.*;
import javafx.scene.input.*;
import java.util.*;

public class MovingCircle extends Application {

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

    private Color randomColour(Random random) {
        return Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble());
    }

    @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(randomColour(random));

        moveDirection = MoveDirection.values()[random.nextInt(4)];       // Nahodne zvoleny pociatocny smer pohybu

        circle.requestFocus();                                           // Kruh dostane fokus, aby mohol reagovat na klavesnicu
        circle.setOnKeyPressed(event -> {                                // Nastavime reakciu kruhu na stlacenie klavesy
            switch (event.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 lastColourChangeTime = 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 - lastColourChangeTime >= 500000000) {
                    circle.setFill(randomColour(random));
                    lastColourChangeTime = now;
                }
            }
        };
        animationTimer.start();                                    // Spusti casovac 

        primaryStage.setScene(scene);
        primaryStage.setTitle("Pohyblivý kruh");
        primaryStage.show();
    }

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

Odkazy

Riešenia testu č. 5

Úloha č. 1

btnLarger.setOnAction(event -> {
    circle.setRadius(circle.getRadius() + 10);
    if (circle.getRadius() == 100) {
        btnLarger.setDisable(true);
    }
    if (circle.getRadius() == 20) {
        btnSmaller.setDisable(false);
    }
});

btnSmaller.setOnAction(event -> {
    circle.setRadius(circle.getRadius() - 10);
    if (circle.getRadius() == 10) {
        btnSmaller.setDisable(true);
    }
    if (circle.getRadius() == 90) {
        btnLarger.setDisable(false);
    }
});

Úloha č. 2

public static Node<Boolean> createBooleanExpressionTree() {
    return new UnaryNode<>(a -> !a,
            new BinaryNode<>((a, b) -> a && b,
                    new NullaryNode<>(true),
                    new BinaryNode<>((a, b) -> a || b, new NullaryNode<>(false), new NullaryNode<>(true))));
}

public static Node<Integer> createArithmeticExpressionTree(int a, int n) {
    if (n == 1) {
        return new NullaryNode<>(a);
    } else {
        return new BinaryNode<>((x, y) -> x + y,
                createArithmeticExpressionTree(a, 1),
                createArithmeticExpressionTree(a, n - 1));
    }
}

Úloha č. 3

package graphs;

import java.util.*;

public class CommonTopologicalOrder {

    /* Metoda z prednasky: */
    public static List<Integer> topologicalSort(Graph g) {
        /* Inicializacia: */
        int n = g.getNumberOfVertices();
        List<Integer> edgesFromUnprocessedVertices = new ArrayList<>();
        for (int v = 0; v <= n - 1; v++) {
            edgesFromUnprocessedVertices.add(0);
        }
        for (int v = 0; v <= n - 1; v++) {
            for (int successor : g.outgoingEdgesDestinations(v)) {
                edgesFromUnprocessedVertices.set(successor, edgesFromUnprocessedVertices.get(successor) + 1);
            }
        }
        Queue<Integer> ready = new LinkedList<>();
        for (int v = 0; v <= n - 1; v++) {
            if (edgesFromUnprocessedVertices.get(v) == 0) {
                ready.add(v);
            }
        }
        List<Integer> result = new LinkedList<>();

        /* Samotne topologicke triedenie: */
        while (!ready.isEmpty()) {
            int vertex = ready.remove();
            result.add(vertex);
            for (int successor : g.outgoingEdgesDestinations(vertex)) {
                edgesFromUnprocessedVertices.set(successor, edgesFromUnprocessedVertices.get(successor) - 1);
                if (edgesFromUnprocessedVertices.get(successor) == 0) {
                    ready.add(successor);
                }
            }
        }

        if (result.size() == n) {
            return result;
        } else {
            return null;
        }
    }

    public static List<Integer> commonTopologicalOrder(Graph g1, Graph g2) {
        if (g1.getNumberOfVertices() != g2.getNumberOfVertices()) {
            throw new IllegalArgumentException();
        }
        int n = g1.getNumberOfVertices();
        List<Edge> edges = new ArrayList<>();
        for (int u = 0; u <= n - 1; u++) {
            for (int v : g1.outgoingEdgesDestinations(u)) {
                edges.add(new Edge(u, v));
            }
            for (int v : g2.outgoingEdgesDestinations(u)) {
                edges.add(new Edge(u, v));
            }
        }
        Graph g = new SuccessorListsGraph(n, edges);
        return topologicalSort(g);
    }
}

Letný semester, prednáška č. 12

Oznamy

  • Počas stredajších cvičení bude prebiehať prvý náhradný test. Okrem toho budú zverejnené ďalšie nebodované úlohy na JavaFX.
  • Piatu domácu úlohu treba odovzdať do pondelka 10. mája, 9:00 (čiže najneskôr do začiatku trinástej prednášky).

Zložitejšie ovládacie prvky a aplikácie s viacerými oknami: jednoduchý textový editor

Cieľom tejto a sčasti aj nasledujúcej 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), tvorbu aplikácií s viacerými oknami, ako aj mechanizmus tzv. vlastností.

Urobíme tak na ukážkovej aplikácii (opäť až priveľmi) jednoduchého textového editora zvládajúceho 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 inštanciu triedy BorderPane, s ktorou 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é inštancie 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 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.*;

public class Editor extends Application {

    private Stage primaryStage;

    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;

        BorderPane borderPane = new BorderPane();
        Scene scene = new Scene(borderPane);

        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.

V danom momente otvorený súbor si budeme pamätať v premennej openedFile; v prípade, že nie je otvorený žiaden súbor, bude obsahom tejto premennej referencia null. Premenná openedFileChanged bude rovná true práve vtedy, keď od posledného uloženia dokumentu došlo k jeho zmene alebo keď dokument nie je uložený v žiadnom súbore. 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.

// ...

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;
        updatePrimaryStageTitle();
    }

    private void updatePrimaryStageTitle() {
        StringBuilder title = new StringBuilder("Textový editor (");
        if (openedFile == null) {
            title.append("neuložené v žiadnom súbore");
        } else {
            title.append(openedFile.getName());
        }
        title.append(")");
        if (openedFileChanged) {
            title.append("*");
        }
        primaryStage.setTitle(title.toString());
    }
    
    @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.scene.text.*;
import javafx.geometry.*;

// ...

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();
        borderPane.setCenter(textArea);
        textArea.setPadding(new Insets(5, 5, 5, 5));
        textArea.setPrefWidth(1000);
        textArea.setPrefHeight(700);
        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”. Rozhranie Property<T> je rozšírením rozhrania ObservableValue<T> reprezentujúceho hodnoty typu T, ktorých zmeny možno v určitom presne definovanom zmysle sledovať.

Podobne ako sme k ovládacím prvkom pridávali spracúvateľov udalostí, možno k vlastnostiam a ďalším inštanciám rozhrania ObservableValue<T> pridávať „spracúvateľov zmien” volaných zakaždým, keď sa zmení nimi „obalená” hodnota. Týmito spracúvateľmi však teraz nebudú inštancie tried implementujúcich rozhranie EventHandler<T>, ale inštancie tried implementujúcich rozhranie ChangeListener<T>. To vyžaduje implementáciu jedinej metódy

void changed(ObservableValue<? extends T> observable, T oldValue, T newValue)

ktorá sa vykoná pri zmene hodnoty „obalenej” inštanciou 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. Registráciu inštancie rozhrania ChangeListener<T> ako „spracúvateľa zmeny” pre inštanciu observable rozhrania ObservableValue<T> vykonáme pomocou metódy observable.addListener.

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. To možno realizovať pomocou anonymnej triedy

import javafx.beans.value.*;

// ...

public void start(Stage primaryStage) {
    // ...

    textArea.textProperty().addListener(new ChangeListener<>() {
        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
            handleTextAreaChange();
        }
    });

    // ...
}

alebo alternatívne prostredníctvom lambda výrazu

public void start(Stage primaryStage) {
    // ...

    textArea.textProperty().addListener((observable, oldValue, 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.

Hlavné ponuky (MenuItem, Menu a MenuBar)

Kľúčovou súčasťou mnohých aplikácií bývajú hlavné ponuky (menu). V JavaFX ich do aplikácie možno pridať 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é inštanciami tried 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();
        borderPane.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().addAll(mFile, mFormat);   

        // ...
    } 
}

K dôležitejším položkám môžeme priradiť aj klávesové skratky.

// ...

import javafx.scene.input.*;

// ...

@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í panelu s ponukami.

V rámci metódy updateOpenedFileInformation ešte môžeme zabezpečiť, aby položka miFileSave bola aktívna práve vtedy, keď má argument hasChanged hodnotu true (v opačnom prípade nie je čo ukladať).

private void updateOpenedFileInformation(File file, boolean hasChanged) { 
    // ...

    miFileSave.setDisable(!hasChanged);

    // ...
}

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úcou 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ý rozdiel oproti tvorbe hlavnej ponuky bude spočívať v použití inštancie triedy ContextMenu. Tú následne pomocou 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 contextMenu;
    private MenuItem cmiFormatFont;
    private CheckMenuItem cmiFormatWrap;

    // ...

    @Override
    public void start(Stage primaryStage) {
        // ...       
        
        contextMenu = new ContextMenu();
        textArea.setContextMenu(contextMenu);
        cmiFormatFont = new MenuItem("Formát písma...");
        cmiFormatWrap = new CheckMenuItem("Zalamovať riadky");
        cmiFormatWrap.setSelected(false);
        contextMenu.getItems().addAll(cmiFormatFont, cmiFormatWrap);

        // ...
    } 
}

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 takmer 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) {
        // ...

        miFileNewEmpty.setOnAction(event -> newEmptyAction());
        miFileNewRandom.setOnAction(event -> newRandomAction());
        miFileOpen.setOnAction(event -> openAction());
        miFileSave.setOnAction(event -> saveAction());
        miFileSaveAs.setOnAction(event -> saveAsAction());
        miFileExit.setOnAction(event -> exitAction());
        miFormatFont.setOnAction(event -> fontAction());

        // ...

        cmiFormatFont.setOnAction(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 tomto tutoriáli.

Jednoduché dialógy (Alert)

Naším najbližším cieľom teraz bude implementácia metód newEmptyAction, newRandomAction a exitAction. Spoločnou črtou 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 vykonané 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 štandardný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 dialog typu Alert, variant 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");         // Vytvor instancie reprezentujuce typy tlacidiel
            ButtonType buttonTypeNo = new ButtonType("Nie");

            // Vdaka druhemu argumentu sa bude ako tlacidlo "Zrusit" spravat aj "krizik" v dialogovom okne vpravo hore:
            ButtonType buttonTypeCancel = new ButtonType("Zrušiť", ButtonBar.ButtonData.CANCEL_CLOSE);

            alert.getButtonTypes().setAll(buttonTypeYes, buttonTypeNo, buttonTypeCancel); // Nastav typy tlacidiel

            Optional<ButtonType> result = alert.showAndWait();        // Zobraz dialog a cakaj, kym sa zavrie
            if (result.get() == buttonTypeYes) {                      // Pokracuj 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;
        }
    }
    
    // ...
}

Dialóg typu Alert by ideálne mal obsahovať práve jedno tlačidlo, ktorého „dáta” sú nastavené na CANCEL_CLOSE. V opačnom prípade sa „krížik” v pravom hornom rohu dialógového okna nemusí správať korektne (v niektorých prípadoch sa dialógové okno po kliknutí naň dokonca ani nemusí zavrieť). Toto obmedzenie sa ale dá eliminovať ďalšími nastaveniami dialógu.

Môžeme teraz pristúpiť k implementácii spomínaných troch akcií.

public class Editor extends Application {
    // ...

    private final int N = 1000;  // V metode 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 builder = 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++) {
                    builder.append(random.nextInt(10));
                }
                builder.append(System.lineSeparator());
            }
            textArea.setText(builder.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 dialógu Alert 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 ukončeniu aplikácie aj zamedziť (napríklad keď používateľ klikne na tlačidlo Zrušiť alebo keď bude tlačidlom Zrušiť ukončený ukladací dialóg).

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 ukončení aplikácie pomocou položky hlavnej ponuky. To môžeme zariadiť nastavením spracúvateľa udalosti, ktorá vznikne pri požiadavke na zatvorenie okna. Ak túto udalosť v rámci jej spracovania skonzumujeme, zatvoreniu okna sa v konečnom dôsledku zamedzí.

public class Editor extends Application {
    // ...

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

    // ...

    @Override
    public void start(Stage primaryStage) {
        // ...
        
        primaryStage.setOnCloseRequest(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 nasledovne.

public class Editor extends Application {    
    // ...

    /**
     * Pomocna metoda, ktora vytvori dialog typu FileChooser a prednastavi niektore jeho vlastnosti.
     * @return Vytvoreny dialog typu FileChooser.
     */
    private FileChooser prepareFileChooser() {
        FileChooser fileChooser = new FileChooser();                                // Vytvorenie dialogoveho okna
        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;
    }

    /**
     * Pomocna metoda realizujuca vyber suboru na otvorenie.
     * @return Vybrany subor alebo null v pripade, ze ziaden subor vybrany nebol.
     */
    private File chooseFileToOpen() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Otvoriť");
        return fileChooser.showOpenDialog(primaryStage);
    }

    /**
     * Pomocna metoda realizujuca vyber suboru na ulozenie.
     * @return Vybrany subor alebo null v pripade, ze ziaden subor vybrany nebol.
     */
    private File chooseFileToSave() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Uložiť");
        return fileChooser.showSaveDialog(primaryStage);
    }
    
    // ...
}

Môžeme teraz pristúpiť k samotnej implementácii otváracích a ukladacích metód. Na čítanie a zápis do textového súboru použijeme statické metódy triedy Files z balíka java.nio.file, ktoré načítajú kompletný obsah súboru do reťazca resp. zapíšu obsah reťazca do súboru. Takýto prístup síce nie je úplne vhodný pre veľmi veľké súbory, no na manipuláciu s nimi by bolo potrebné našu aplikáciu vylepšiť aj v iných ohľadoch – v prvom rade by sme v rámci ovládacieho prvku typu TextArea nemali uchovávať kompletný obsah textového súboru. Použijeme preto teraz spomenuté jednoduché riešenie, hoci rovnako dobre by sme mohli použiť napríklad aj BufferedReader. U všetkých textových súborov predpokladáme kódovanie UTF-8.

// ...

import java.nio.file.*;
import java.nio.charset.*;

// ...

public class Editor extends Application {    
    // ...
    
    private String loadFromFile(File file) throws IOException {
        return Files.readString(Paths.get(file.getPath()));
    }

    private void writeToFile(File file, String s) throws IOException {
        Files.writeString(Paths.get(file.getPath()), s);
    }

    // ...

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

    // ...
}

Zvyšnú funkcionalitu aplikácie implementujeme nabudúce.

Riešenia náhradného testu č. 1

Úloha č. 1

public static <T extends Comparable<T>> void retainLocalMaxima(List<T> list) {
    if (list == null || list.contains(null)) {
        throw new IllegalArgumentException();
    }
    ArrayList<Boolean> toRetain = new ArrayList<>();
    for (int i = 0; i <= list.size() - 1; i++) {
        T current = list.get(i);
        toRetain.add((i == 0 || current.compareTo(list.get(i - 1)) >= 0)
                && (i == list.size() - 1 || current.compareTo(list.get(i + 1)) >= 0));
    }
    int i = 0;
    for (boolean b : toRetain) {
        if (!b) {
            list.remove(i);
        } else {
            i++;
        }
    }
}

Úloha č. 2

public static int bridgeCount(UndirectedGraph g) {
    int n = g.getNumberOfVertices();
    int componentCount = (new Components(g)).getComponentCount();
    ArrayList<Edge> allEdges = new ArrayList<>();
    for (int u = 0; u <= n - 1; u++) {
        for (int v : g.outgoingEdgesDestinations(u)) {
            if (u <= v) {
                allEdges.add(new Edge(u, v));
            }
        }
    }
    int result = 0;
    for (int e = 0; e <= allEdges.size() - 1; e++) {
        ArrayList<Edge> edges = new ArrayList<>(allEdges);
        edges.remove(e);
        if (new Components(new AdjacencyListsUndirectedGraph(n, edges)).getComponentCount() > componentCount) {
            result++;
        }
    }
    return result;
}

Úloha č. 3

import java.util.*;
import java.util.function.*;

public class FilteredIterator<E> implements Iterator<E> {
    private Iterator<E> iterator;
    private Predicate<E> filter;
    private E next;

    public FilteredIterator(Iterable<E> iterable, Predicate<E> filter) {
        iterator = iterable.iterator();
        this.filter = filter;
        findNext();
    }

    private void findNext() {
        while (iterator.hasNext()) {
            next = iterator.next();
            if (filter.test(next)) {
                return;
            }
        }
        next = null;
    }

    @Override
    public boolean hasNext() {
        return next != null;
    }

    @Override
    public E next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        E result = next;
        findNext();
        return result;
    }
}

Letný semester, prednáška č. 13

Oznamy

  • Počas stredajších cvičení bude prebiehať druhý náhradný test. Okrem toho budú zverejnené ďalšie nebodované úlohy na JavaFX.
  • Nezabudnite sa prihlásiť na niektorý zo skúškových termínov najneskôr 24 hodín pred jeho začiatkom. Na skúške sú vítaní všetci, čo zo semestra získali aspoň 20 bodov (a majú tak šancu na úspešné absolvovanie predmetu).
  • Jedna z úloh praktickej časti skúšky bude vždy zameraná na JavaFX (vrátane látky z poslednej prednášky).

Jednoduchý textový editor: pokračovanie

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 ukončeniu aplikácie aj zamedziť (napríklad keď používateľ klikne na tlačidlo Zrušiť alebo keď bude tlačidlom Zrušiť ukončený ukladací dialóg).

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 ukončení aplikácie pomocou položky hlavnej ponuky. To môžeme zariadiť nastavením spracúvateľa udalosti, ktorá vznikne pri požiadavke na zatvorenie okna. Ak túto udalosť v rámci jej spracovania skonzumujeme, zatvoreniu okna sa v konečnom dôsledku zamedzí.

public class Editor extends Application {
    // ...

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

    // ...

    @Override
    public void start(Stage primaryStage) {
        // ...
        
        primaryStage.setOnCloseRequest(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 nasledovne.

public class Editor extends Application {    
    // ...

    /**
     * Pomocna metoda, ktora vytvori dialog typu FileChooser a prednastavi niektore jeho vlastnosti.
     * @return Vytvoreny dialog typu FileChooser.
     */
    private FileChooser prepareFileChooser() {
        FileChooser fileChooser = new FileChooser();                                // Vytvorenie dialogoveho okna
        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;
    }

    /**
     * Pomocna metoda realizujuca vyber suboru na otvorenie.
     * @return Vybrany subor alebo null v pripade, ze ziaden subor vybrany nebol.
     */
    private File chooseFileToOpen() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Otvoriť");
        return fileChooser.showOpenDialog(primaryStage);
    }

    /**
     * Pomocna metoda realizujuca vyber suboru na ulozenie.
     * @return Vybrany subor alebo null v pripade, ze ziaden subor vybrany nebol.
     */
    private File chooseFileToSave() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Uložiť");
        return fileChooser.showSaveDialog(primaryStage);
    }
    
    // ...
}

Môžeme teraz pristúpiť k samotnej implementácii otváracích a ukladacích metód. Na čítanie a zápis do textového súboru použijeme statické metódy triedy Files z balíka java.nio.file, ktoré načítajú kompletný obsah súboru do reťazca resp. zapíšu obsah reťazca do súboru. Takýto prístup síce nie je úplne vhodný pre veľmi veľké súbory, no na manipuláciu s nimi by bolo potrebné našu aplikáciu vylepšiť aj v iných ohľadoch – v prvom rade by sme v rámci ovládacieho prvku typu TextArea nemali uchovávať kompletný obsah textového súboru. Použijeme preto teraz spomenuté jednoduché riešenie, hoci rovnako dobre by sme mohli použiť napríklad aj BufferedReader. U všetkých textových súborov predpokladáme kódovanie UTF-8.

// ...

import java.nio.file.*;
import java.nio.charset.*;

// ...

public class Editor extends Application {    
    // ...
    
    private String loadFromFile(File file) throws IOException {
        return Files.readString(Paths.get(file.getPath()));
    }

    private void writeToFile(File file, String s) throws IOException {
        Files.writeString(Paths.get(file.getPath()), s);
    }

    // ...

    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)

Na rozdiel 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 ú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, ako možno tvoriť aplikácie 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ť inštanciou samostatnej triedy FontDialog s metódou showFontDialog, ktorá tento dialóg zobrazí a po jeho ukončení vráti informácie o vybratom fonte. Návratovou hodnotou tejto metódy bude inštancia novovytvorenej triedy FontAttributes, ktorá bude slúžiť ako „obal” pre niektoré atribúty fontu (z inštancií triedy Font, ktorá nie je navrhnutá úplne najideálnejším spôsobom, sa niektoré z týchto atribútov zisťujú pomerne ťažko).

package editor;

import javafx.scene.text.*;

public class FontAttributes {
    private String family;
    private FontWeight weight;
    private FontPosture posture;
    private 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;
    }
}
package editor;

import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.scene.text.*;

public class FontDialog {
    private Stage stage;

    public FontDialog() {
        // Zatial iba zaklad implementacie.

        BorderPane borderPane = new BorderPane();

        Scene scene = new Scene(borderPane);

        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 nastavenie ovladacich prvkov bude urcene parametrom
     * oldFontAttributes. Po ukonceni dialogu vrati atributy fontu vybrane pouzivatelom.
     * @param oldFontAttributes Atributy fontu, ktorym sa text zobrazoval doposial. Na zaklade nich sa nastavia
     *                          vlastnosti zobrazovanych ovladacich prvkov dialogu.
     * @return                  Nova instancia triedy FontAttributes reprezentujuca atributy fontu vybraneho v dialogu
     *                          pouzivatelom. V pripade ukoncenia dialogu inym sposobom, nez potvrdenim, je navratovou
     *                          hodnotou metody referencia null.
     */
    public FontAttributes showFontDialog(FontAttributes oldFontAttributes) {
        // Zatial iba zaklad implementacie.

        stage.showAndWait(); // Od metody show sa showAndWait lisi tym, ze zvysok kodu sa vykona az po zavreti okna
        return new FontAttributes("Tahoma", FontWeight.NORMAL, FontPosture.REGULAR, 16); // Neskor nahradime zmysluplnym kodom
    }
}

V hlavnej triede Editor 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 inštancie triedy ListView je jej selectionModel, ktorý hovorí o móde výberu jednotlivých prvkov. My si vystačíme s východzím nastavením tejto vlastnosti, pri ktorom možno zo zoznamu vybrať najviac jeden prvok. Prístup k vybraným prvkom zoznamu je však vždy potrebné realizovať prostredníctvom metódy getSelectionModel vracajúcej inštanciu generickej triedy MultipleSelectionModel<T>, ktorá je vlastnosťou selectionModel „obalená”.
  • 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 predminulej prednáške; tentokrát však v konštruktore triedy FontDialog.

// ...

import java.util.*;
import javafx.geometry.*;
import javafx.collections.*;

// ...

public class FontDialog {
    // ...
    
    private ListView<String> lvFamilies;
    private ArrayList<RadioButton> rbSizes;
    private RadioButton rbRegular;
    private RadioButton rbBold;
    private RadioButton rbItalic;
    private RadioButton rbBoldItalic;
    
    public FontDialog() {
        // ...
        
        GridPane grid = new GridPane();
        borderPane.setCenter(grid);
        grid.setPadding(new Insets(10, 15, 10, 15));
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);
        grid.setVgap(20);

        lvFamilies = new ListView<>();
        grid.add(lvFamilies, 0, 0);
        lvFamilies.setItems(FXCollections.observableList(Font.getFamilies()));
        lvFamilies.setPrefHeight(200);

        rbSizes = new ArrayList<>();
        for (int i = 8; i <= 24; i += 2) {
            rbSizes.add(new RadioButton(Integer.toString(i)));
        }

        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();
        borderPane.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 tlačidiel typu RadioButton (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 jednej z dvoch skupín reprezentovaných inštanciami triedy ToggleGroup.

public class FontDialog {
    // ...
    
    private ToggleGroup toggleGroup1;        
    private ToggleGroup toggleGroup2;        

    // ...

    public FontDialog() {
        // ...

        toggleGroup1 = new ToggleGroup();           

        /* Cyklus for, v ktorom sa pridavaju prvky do zoznamu rbSizes, upravime nasledovne: */
        for (int i = 8; i <= 24; i += 2) {
            RadioButton rb = new RadioButton(Integer.toString(i));
            rb.setToggleGroup(toggleGroup1);       
            rbSizes.add(rb);
        } 

        // ...

        toggleGroup2 = new ToggleGroup();           
         
        rbRegular.setToggleGroup(toggleGroup2);     
        rbBold.setToggleGroup(toggleGroup2);        
        rbItalic.setToggleGroup(toggleGroup2);      
        rbBoldItalic.setToggleGroup(toggleGroup2);  

        // ...
    }

    // ...     
}

Dokončenie dialógu na výber fontu

Pridajme najprv funkcionalitu jednotlivých tlačidiel dialógu na výber fontu.

public class FontDialog {
    // ...

    private boolean confirmed;
    
    private void okAction() {
        confirmed = true;
        stage.close();
    }
    
    private void cancelAction() {
        stage.close();
    }

    public FontDialog() {
        // ...
        
        btnOK.setOnAction(event -> okAction());
        btnCancel.setOnAction(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á.

public class FontDialog {
    // ...

    public FontAttributes showFontDialog(FontAttributes oldFontAttributes) {
        /* Nastav predvoleny font podla oldFontAttributes: */  
 
        lvFamilies.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;
            newFamily = lvFamilies.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

Viacoknové aplikácie v JavaFX: minimalistický príklad

Zakončime teraz tému tvorby aplikácií v JavaFX príkladom aplikácie s dvoma oknami, ktorý je o niečo jednoduchší, než ten vyššie. 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, v ktorom sú ovládacie prvky rozmiestnené v hlavnom okne.

package nejakaaplikacia;

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.geometry.*;

public class NejakaAplikacia extends Application {

    private Stage primaryStage;
    private Button btnOpenDialog;
    private Label lblResult;

    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;

        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setPadding(new Insets(20,100,20,100));
        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);

        primaryStage.setTitle("Hlavné okno");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Vytvorenie druhého okna a jeho zobrazenie po stlačení tlačidla btnOpenDialog potom môžeme priamočiaro implementovať napríklad nasledovne.

public class NejakaAplikacia extends Application {
    // ...
    
    private Stage dialogStage;
    private Button btnYes;    
    private Button btnNo;
    
    private boolean result;    

    @Override
    public void start(Stage primaryStage) {
        // ...
        
        btnOpenDialog.setOnAction(event -> {
            result = false;
            dialogStage.showAndWait();
            if (result) {
                lblResult.setText("Áno");
            } else {
                lblResult.setText("Nie");
            }
        });      

        // ...

        dialogStage = new Stage();               
        
        HBox hb = new HBox();                    
        
        hb.setSpacing(10);
        hb.setPadding(new Insets(10,10,10,10));
        
        btnYes = new Button("Áno");              
        btnYes.setOnAction(event -> {  
            result = true;
            dialogStage.close();
        });
        btnNo = new Button("Nie");               
        btnNo.setOnAction(event -> 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);             

        // ...
    }
}

O niečo elegantnejším prístupom je však vytvorenie samostatnej triedy (napríklad 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.

package nejakaaplikacia;

import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.geometry.*;

public class CustomDialog {
    private Stage dialogStage;
    private Button btnYes;
    private Button btnNo;

    private 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(event -> {
            result = true;
            dialogStage.close();
        });
        btnNo = new Button("Nie");
        btnNo.setOnAction(event -> 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 {
    // ...     

    private CustomDialog dialog;
    
    @Override
    public void start(Stage primaryStage) {
        // ...

        dialog = new CustomDialog();
        btnOpenDialog.setOnAction(event -> lblResult.setText(dialog.showCustomDialog()));        

        // ...
    }
}

Textový editor: kompletný zdrojový kód

package editor;

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

public class Editor extends Application {

    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 contextMenu;
    private MenuItem cmiFormatFont;
    private CheckMenuItem cmiFormatWrap;

    private File openedFile;
    private boolean openedFileChanged;

    private String fontFamily = "Tahoma";
    private FontWeight fontWeight = FontWeight.NORMAL;
    private FontPosture fontPosture = FontPosture.REGULAR;
    private double fontSize = 16;

    private final int N = 1000;  // V metode newRandomAction budeme generovat N riadkov o N nahodnych cifrach

    private void updateOpenedFileInformation(File file, boolean hasChanged) {
        openedFile = file;
        openedFileChanged = hasChanged;
        miFileSave.setDisable(!hasChanged);
        updatePrimaryStageTitle();
    }

    private void updatePrimaryStageTitle() {
        StringBuilder title = new StringBuilder("Textový editor (");
        if (openedFile == null) {
            title.append("neuložené v žiadnom súbore");
        } else {
            title.append(openedFile.getName());
        }
        title.append(")");
        if (openedFileChanged) {
            title.append("*");
        }
        primaryStage.setTitle(title.toString());
    }

    private void applyFont() {
        textArea.setFont(Font.font(fontFamily, fontWeight, fontPosture, fontSize));
    }

    private void handleTextAreaChange() {
        if (!openedFileChanged) {
            updateOpenedFileInformation(openedFile, true);
        }
    }

    private boolean saveBeforeClosingAlert() {
        if (openedFileChanged) {                                      // Ak dokument nie je ulozeny
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);    // Vytvor dialog typu Alert, variant 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");         // Vytvor instancie reprezentujuce typy tlacidiel
            ButtonType buttonTypeNo = new ButtonType("Nie");

            // Vdaka druhemu argumentu sa bude ako tlacidlo "Zrusit" spravat aj "krizik" v dialogovom okne vpravo hore:
            ButtonType buttonTypeCancel = new ButtonType("Zrušiť", ButtonBar.ButtonData.CANCEL_CLOSE);

            alert.getButtonTypes().setAll(buttonTypeYes, buttonTypeNo, buttonTypeCancel); // Nastav typy tlacidiel

            Optional<ButtonType> result = alert.showAndWait();        // Zobraz dialog a cakaj, kym sa zavrie
            if (result.get() == buttonTypeYes) {                      // Pokracuj 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;
        }
    }

    /**
     * Pomocna metoda, ktora vytvori dialog typu FileChooser a prednastavi niektore jeho vlastnosti.
     * @return Vytvoreny dialog typu FileChooser.
     */
    private FileChooser prepareFileChooser() {
        FileChooser fileChooser = new FileChooser();                                // Vytvorenie dialogoveho okna
        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;
    }

    /**
     * Pomocna metoda realizujuca vyber suboru na otvorenie.
     * @return Vybrany subor alebo null v pripade, ze ziaden subor vybrany nebol.
     */
    private File chooseFileToOpen() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Otvoriť");
        return fileChooser.showOpenDialog(primaryStage);
    }

    /**
     * Pomocna metoda realizujuca vyber suboru na ulozenie.
     * @return Vybrany subor alebo null v pripade, ze ziaden subor vybrany nebol.
     */
    private File chooseFileToSave() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Uložiť");
        return fileChooser.showSaveDialog(primaryStage);
    }

    private String loadFromFile(File file) throws IOException {
        return Files.readString(Paths.get(file.getPath()));
    }

    private void writeToFile(File file, String s) throws IOException {
        Files.writeString(Paths.get(file.getPath()), s);
    }

    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 builder = 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++) {
                    builder.append(random.nextInt(10));
                }
                builder.append(System.lineSeparator());
            }
            textArea.setText(builder.toString());         // Vypis vygenerovany retazec do textoveho priestoru textArea
            return true;
        } else {
            return false;
        }
    }

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

    private boolean exitAction() {
        if (saveBeforeClosingAlert()) {   // Pokracuj len ak sa podarilo zavriet dokument
            Platform.exit();              // Ukonci aplikaciu
            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 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
            return false;
        }
    }

    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;

        BorderPane borderPane = new BorderPane();
        Scene scene = new Scene(borderPane);

        textArea = new TextArea();
        borderPane.setCenter(textArea);
        textArea.setPadding(new Insets(5, 5, 5, 5));
        textArea.setPrefWidth(1000);
        textArea.setPrefHeight(700);
        applyFont();
        textArea.textProperty().addListener((observable, oldValue, newValue) -> handleTextAreaChange());

        MenuBar menuBar = new MenuBar();
        borderPane.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().addAll(mFile, mFormat);

        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

        miFileNewEmpty.setOnAction(event -> newEmptyAction());
        miFileNewRandom.setOnAction(event -> newRandomAction());
        miFileOpen.setOnAction(event -> openAction());
        miFileSave.setOnAction(event -> saveAction());
        miFileSaveAs.setOnAction(event -> saveAsAction());
        miFileExit.setOnAction(event -> exitAction());
        miFormatFont.setOnAction(event -> fontAction());

        contextMenu = new ContextMenu();
        textArea.setContextMenu(contextMenu);
        cmiFormatFont = new MenuItem("Formát písma...");
        cmiFormatWrap = new CheckMenuItem("Zalamovať riadky");
        cmiFormatWrap.setSelected(false);
        contextMenu.getItems().addAll(cmiFormatFont, cmiFormatWrap);

        cmiFormatFont.setOnAction(event -> fontAction());

        miFormatWrap.selectedProperty().bindBidirectional(cmiFormatWrap.selectedProperty());
        miFormatWrap.selectedProperty().bindBidirectional(textArea.wrapTextProperty());

        fontDialog = new FontDialog();

        updateOpenedFileInformation(null, true);

        primaryStage.setOnCloseRequest(event -> handleStageCloseRequest(event));
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

import javafx.scene.text.*;

public class FontAttributes {
    private String family;
    private FontWeight weight;
    private FontPosture posture;
    private 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;
    }
}
package editor;

import java.util.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.scene.text.*;
import javafx.geometry.*;
import javafx.collections.*;

public class FontDialog {
    private Stage stage;

    private ListView<String> lvFamilies;
    private ArrayList<RadioButton> rbSizes;
    private RadioButton rbRegular;
    private RadioButton rbBold;
    private RadioButton rbItalic;
    private RadioButton rbBoldItalic;

    private ToggleGroup toggleGroup1;
    private ToggleGroup toggleGroup2;

    private boolean confirmed;

    private void okAction() {
        confirmed = true;
        stage.close();
    }

    private void cancelAction() {
        stage.close();
    }

    public FontDialog() {
        BorderPane borderPane = new BorderPane();
        Scene scene = new Scene(borderPane);

        GridPane grid = new GridPane();
        borderPane.setCenter(grid);
        grid.setPadding(new Insets(10, 15, 10, 15));
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);
        grid.setVgap(20);

        lvFamilies = new ListView<>();
        grid.add(lvFamilies, 0, 0);
        lvFamilies.setItems(FXCollections.observableList(Font.getFamilies()));
        lvFamilies.setPrefHeight(200);

        toggleGroup1 = new ToggleGroup();
        rbSizes = new ArrayList<>();
        for (int i = 8; i <= 24; i += 2) {
            RadioButton rb = new RadioButton(Integer.toString(i));
            rb.setToggleGroup(toggleGroup1);
            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);

        toggleGroup2 = 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(toggleGroup2);
        rbBold.setToggleGroup(toggleGroup2);
        rbItalic.setToggleGroup(toggleGroup2);
        rbBoldItalic.setToggleGroup(toggleGroup2);

        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();
        borderPane.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(event -> okAction());
        btnCancel.setOnAction(event -> cancelAction());

        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 nastavenie ovladacich prvkov bude urcene parametrom
     * oldFontAttributes. Po ukonceni dialogu vrati atributy fontu vybrane pouzivatelom.
     * @param oldFontAttributes Atributy fontu, ktorym sa text zobrazoval doposial. Na zaklade nich sa nastavia
     *                          vlastnosti zobrazovanych ovladacich prvkov dialogu.
     * @return                  Nova instancia triedy FontAttributes reprezentujuca atributy fontu vybraneho v dialogu
     *                          pouzivatelom. V pripade ukoncenia dialogu inym sposobom, nez potvrdenim, je navratovou
     *                          hodnotou metody referencia null.
     */
    public FontAttributes showFontDialog(FontAttributes oldFontAttributes) {
        /* Nastav predvoleny font podla oldFontAttributes: */

        lvFamilies.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;
            newFamily = lvFamilies.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;
        }
    }
}

Riešenia náhradného testu č. 2

Úloha č. 1

public class Circles extends Application {
    private int circleCount;

    private Color randomColour(Random random) {
        return Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble());
    }

    @Override
    public void start(Stage primaryStage) {
    
        // ...
   
        Random random = new Random();

        btnAdd.setOnAction(actionEvent -> {
            Circle circle = new Circle(random.nextInt(261) + 20, random.nextInt(261) + 20, 20);
            circle.setFill(randomColour(random));
            left.getChildren().add(circle);
            lblCircleCount.setText("Počet kruhov: " + (++circleCount));
            circle.setOnMouseClicked(mouseEvent -> {
                left.getChildren().remove(circle);
                lblCircleCount.setText("Počet kruhov: " + (--circleCount));
            });
        });

        // ...
    }            
}

Úloha č. 2

package graphs;

import java.util.*;

public class MaximumIndependentSet {
    private UndirectedGraph g;
    private LinkedList<Integer> maximumIndependentSet;
    private LinkedList<Integer> currentVertices;

    public MaximumIndependentSet(UndirectedGraph g) {
        this.g = g;
        maximumIndependentSet = new LinkedList<>();
        currentVertices = new LinkedList<>();
        search(0);
    }

    private void search(int vertex) {
        if (vertex == g.getNumberOfVertices()) {
            if (isIndependentSet(currentVertices) && currentVertices.size() > maximumIndependentSet.size()) {
                maximumIndependentSet = new LinkedList<>(currentVertices);
            }
        } else {
            currentVertices.addLast(vertex);
            search(vertex + 1);
            currentVertices.removeLast();
            search(vertex + 1);
        }
    }

    private boolean isIndependentSet(Collection<Integer> vertices) {
        for (int u : vertices) {
            for (int v : vertices) {
                if (g.existsEdge(u, v)) {
                    return false;
                }
            }
        }
        return true;
    }

    public List<Integer> getMaximumIndependentSet() {
        return Collections.unmodifiableList(maximumIndependentSet);
    }
}

Úloha č. 3

import java.util.*;

public class FixedLengthList<E> implements Iterable<E> {
    private ArrayList<E> list;

    public FixedLengthList(int length, E element) {
        list = new ArrayList<>();
        for (int i = 1; i <= length; i++) {
            list.add(element);
        }
    }

    public FixedLengthList(List<E> list) {
        this.list = new ArrayList<>(list);
    }

    public E get(int index) {
        return list.get(index);
    }

    public E set(int index, E element) {
        return list.set(index, element);
    }

    public int length() {
        return list.size();
    }

    public Iterator<E> iterator() {
        return new Iterator<E>() {
            int nextIndex = 0;

            @Override
            public boolean hasNext() {
                return nextIndex < list.size();
            }

            @Override
            public E next() {
                if (hasNext()) {
                    return get(nextIndex++);
                } else {
                    throw new NoSuchElementException();
                }
            }
        };
    }
}