Programovanie (1) v C/C++
1-INF-127, ZS 2024/25
Letný semester, prednáška č. 4: Rozdiel medzi revíziami
(85 medziľahlých úprav od rovnakého používateľa nie je zobrazených.) | |||
Riadok 1: | Riadok 1: | ||
== Oznamy == | == Oznamy == | ||
− | + | * Počas zajtrajších cvičení – čiže ''od 9:50 do 11:20'' – bude prebiehať prvý test zameraný na látku ''z prvých troch týždňov semestra''. Body z testu bude možné získať iba v prípade prítomnosti na cvičeniach v miestnosti I-H6. | |
− | * Počas | + | * Krátko po dnešnej prednáške bude na testovači zverejnené zadanie prvej domácej úlohy, ktorú bude potrebné odovzdať najneskôr ''do utorka 26. marca, 9:50'' – čiže do začiatku šiestych cvičení. |
− | * | ||
− | |||
== Výnimky == | == Výnimky == | ||
Riadok 16: | Riadok 14: | ||
int a[] = new int[n]; | int a[] = new int[n]; | ||
for (int i = 0; i <= n - 1; i++) { | for (int i = 0; i <= n - 1; i++) { | ||
− | a[i] | + | a[i] = scanner.nextInt(); |
} | } | ||
String next = scanner.next(); | String next = scanner.next(); | ||
Riadok 45: | Riadok 43: | ||
} | } | ||
} else { | } else { | ||
− | System.err.println("Vstup sa nezacina cislom.") | + | System.err.println("Vstup sa nezacina cislom.") |
System.exit(1); | System.exit(1); | ||
} | } | ||
Riadok 51: | Riadok 49: | ||
for (int i = 0; i <= n - 1; i++) { | for (int i = 0; i <= n - 1; i++) { | ||
if (scanner.hasNextInt()) { | if (scanner.hasNextInt()) { | ||
− | a[i] | + | a[i] = scanner.nextInt(); |
} else { | } else { | ||
− | System.err.println(" | + | System.err.println("Chybne zadanych n celociselnych prvkov pola."); |
System.exit(3); | System.exit(3); | ||
} | } | ||
} | } | ||
− | String next = scanner.next(); | + | String next = null; |
+ | if (scanner.hasNext()) { | ||
+ | next = scanner.next(); | ||
+ | } else { | ||
+ | System.err.println("Chyba druha cast vstupu."); | ||
+ | System.exit(4); | ||
+ | } | ||
while (!next.equals("KONIEC")) { | while (!next.equals("KONIEC")) { | ||
if (isInteger(next)) { | if (isInteger(next)) { | ||
Riadok 65: | Riadok 69: | ||
} else { | } else { | ||
System.err.println("Niektory z indexov do pola je mimo povoleneho rozmedzia."); | System.err.println("Niektory z indexov do pola je mimo povoleneho rozmedzia."); | ||
− | System.exit( | + | System.exit(6); |
} | } | ||
} else { | } else { | ||
System.err.println("Niektory z indexov do pola nebol zadany ako cele cislo."); | System.err.println("Niektory z indexov do pola nebol zadany ako cele cislo."); | ||
− | System.exit( | + | System.exit(5); |
} | } | ||
next = scanner.next(); | next = scanner.next(); | ||
Riadok 84: | Riadok 88: | ||
=== Mechanizmus výnimiek v Jave === | === Mechanizmus výnimiek v Jave === | ||
− | Pod ''výnimkou'' (angl. ''exception'') sa v Jave rozumie inštancia špeciálnej triedy <tt>Exception</tt>, prípadne jej nadtriedy <tt>Throwable</tt>, reprezentujúca nejakú výnimočnú udalosť. Trieda <tt>Exception</tt> má pritom množstvo podtried reprezentujúcich rôzne typy chybových udalostí. | + | Pod ''výnimkou'' (angl. ''exception'') sa v Jave rozumie inštancia špeciálnej triedy [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Exception.html <tt>Exception</tt>], prípadne jej nadtriedy [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Throwable.html <tt>Throwable</tt>], reprezentujúca nejakú výnimočnú udalosť, ktorá môže nastať počas vykonávania programu. Trieda <tt>Exception</tt> má pritom množstvo podtried reprezentujúcich rôzne typy chybových udalostí. |
* Výnimka môže počas vykonávania nejakej metódy <tt>f</tt> 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 <tt>throw</tt> (detaily nižšie). | * Výnimka môže počas vykonávania nejakej metódy <tt>f</tt> 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 <tt>throw</tt> (detaily nižšie). | ||
* Vzniknutá výnimka môže byť priamo odchytená a ošetrená v rámci danej metódy <tt>f</tt>. V opačnom prípade je vykonávanie metódy ukončené a výnimka je posunutá metóde <tt>g</tt>, ktorá metódu <tt>f</tt> volala (posunieme sa teda v zásobníku volaní metód o úroveň nižšie). | * Vzniknutá výnimka môže byť priamo odchytená a ošetrená v rámci danej metódy <tt>f</tt>. V opačnom prípade je vykonávanie metódy ukončené a výnimka je posunutá metóde <tt>g</tt>, ktorá metódu <tt>f</tt> volala (posunieme sa teda v zásobníku volaní metód o úroveň nižšie). | ||
* V metóde <tt>g</tt> 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 <tt>f</tt>. Opäť teda môže byť výnimka buď odchytená a ošetrená, alebo rovnakým spôsobom predaná metóde, ktorá volala metódu <tt>g</tt> (aj v takom prípade hovoríme, že metóda <tt>g</tt> vyhodila výnimku, hoci reálne výnimka vznikla už v metóde <tt>f</tt>). | * V metóde <tt>g</tt> 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 <tt>f</tt>. Opäť teda môže byť výnimka buď odchytená a ošetrená, alebo rovnakým spôsobom predaná metóde, ktorá volala metódu <tt>g</tt> (aj v takom prípade hovoríme, že metóda <tt>g</tt> vyhodila výnimku, hoci reálne výnimka vznikla už v metóde <tt>f</tt>). | ||
* 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. | * 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 <tt>main</tt> 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 | + | * Ak sa takáto metóda na zásobníku volaní nenájde – čiže je výnimka vyhodená aj metódou <tt>main</tt> 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. |
[[Súbor:Vynimky.png|center]] | [[Súbor:Vynimky.png|center]] | ||
Riadok 100: | Riadok 104: | ||
* Kedykoľvek nastane v bloku <tt>try</tt> výnimka, okamžite sa vykonávanie tohto bloku ukončí. Ak je vzniknutá výnimka kompatibilná s argumentom bloku <tt>catch</tt>, pokračuje sa blokom <tt>catch</tt> 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 <tt>try</tt>.) | * Kedykoľvek nastane v bloku <tt>try</tt> výnimka, okamžite sa vykonávanie tohto bloku ukončí. Ak je vzniknutá výnimka kompatibilná s argumentom bloku <tt>catch</tt>, pokračuje sa blokom <tt>catch</tt> 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 <tt>try</tt>.) | ||
− | ''Príklad'': Uvažujme program, ktorý prečíta zo vstupného súboru <tt>vstup.txt</tt> prirodzené číslo <tt>n</tt> a za ním <tt>n</tt> 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 <tt>vstup.txt</tt> nemusí existovať alebo môže nastať iná chyba pri pokuse o vytvorenie inštancie triedy <tt>Scanner</tt>; nemusí byť dodržaný požadovaný formát vstupného súboru a môže tak vzniknúť výnimka pri volaní metódy <tt>nextInt</tt> triedy <tt>Scanner</tt>. Kód teda môžeme obaliť do bloku <tt>try</tt> a prípadnú výnimku môžeme spracovať napríklad jednoduchým výpisom textu <tt>"Nieco sa pokazilo."</tt> do štandardného chybového výstupu. | + | ''Príklad'': Uvažujme program, ktorý prečíta zo vstupného súboru <tt>vstup.txt</tt> prirodzené číslo <tt>n</tt> a za ním <tt>n</tt> 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 <tt>vstup.txt</tt> nemusí existovať alebo môže nastať iná chyba pri pokuse o vytvorenie inštancie triedy <tt>Scanner</tt>; nemusí byť dodržaný požadovaný formát vstupného súboru a môže tak vzniknúť výnimka pri volaní metódy <tt>nextInt</tt> inštancie triedy <tt>Scanner</tt>. Kód teda môžeme obaliť do bloku <tt>try</tt> a prípadnú výnimku môžeme spracovať napríklad jednoduchým výpisom textu <tt>"Nieco sa pokazilo."</tt> do štandardného chybového výstupu. Kód nasledujúci za blokom <tt>catch</tt> sa vykoná bez ohľadu na to, či takto odchytená výnimka nastala alebo nenastala. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
import java.io.*; | import java.io.*; | ||
Riadok 128: | Riadok 132: | ||
e.printStackTrace(); | e.printStackTrace(); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Uvedené riešenie má ale | + | Uvedené riešenie má ale tri menšie nedostatky: |
− | * V prípade vyhodenia výnimky nikdy nedôjde uzatvoreniu vstupného súboru, pretože vykonávanie bloku <tt>try</tt> 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 | + | * V prípade vyhodenia výnimky nikdy nedôjde uzatvoreniu vstupného súboru, pretože vykonávanie bloku <tt>try</tt> 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 zatvárajúceho <tt>scanner</tt> až za koniec príslušného bloku <tt>catch</tt>; samozrejme s ošetrením prípadu, keď <tt>scanner == null</tt>. 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 <tt>Throwable</tt>, alebo výnimka vyhodená v bloku <tt>catch</tt>). Riešením je pridať za bloky <tt>catch</tt> blok <tt>finally</tt>, 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 <tt>try</tt> bloku úspešne vykonal príkaz <tt>return</tt>; nevykoná sa ale napríklad v prípade, keď vykonávanie programu ukončíme metódou <tt>System.exit</tt>). |
<syntaxhighlight lang="java> | <syntaxhighlight lang="java> | ||
import java.io.*; | import java.io.*; | ||
Riadok 157: | Riadok 161: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | * 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 <tt>vstup.txt</tt> 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ú | + | * 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 <tt>vstup.txt</tt> 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ú obyčajne rôznych typov (vždy ale ide o inštancie podtried <tt>Throwable</tt> a typicky aj <tt>Exception</tt>). V prvom prípade sa vyhodí výnimka, ktorá je inštanciou triedy <tt>IOException</tt> z balíka <tt>java.io</tt> alebo nejakej jej podtriedy (napríklad <tt>FileNotFoundException</tt>). V druhom prípade pôjde o výnimku typu <tt>NoSuchElementException</tt> z balíka <tt>java.util</tt>, vyhodenú metódou <tt>nextInt</tt> triedy <tt>Scanner</tt>. Za blok <tt>try</tt> môžeme pridať aj viacero blokov <tt>catch</tt> 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. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
import java.io.*; | import java.io.*; | ||
Riadok 198: | Riadok 202: | ||
[[Súbor:Vynimky_hierarchia.png|center|750px]] | [[Súbor:Vynimky_hierarchia.png|center|750px]] | ||
− | Všetky výnimky v Jave dedia od triedy [https://docs.oracle.com/en/java/javase/ | + | Všetky výnimky v Jave dedia od triedy [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Throwable.html <tt>Throwable</tt>]. Tá má dve podtriedy: |
− | * [https://docs.oracle.com/en/java/javase/ | + | * [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Exception.html <tt>Exception</tt>], od ktorej dedí väčšina „bežných” výnimiek. |
− | * [https://docs.oracle.com/en/java/javase/ | + | * [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Error.html <tt>Error</tt>], od ktorej dedia triedy reprezentujúce závažné, v rámci aplikácie ťažko predvídateľné systémové chyby (napríklad nedostatok pamäte). Jednou z najznámejších podtried triedy <tt>Error</tt> je [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/StackOverflowError.html <tt>StackOverflowError</tt>]. |
Podtriedy triedy <tt>Exception</tt> možno ďalej rozdeliť na dve základné kategórie: | Podtriedy triedy <tt>Exception</tt> možno ďalej rozdeliť na dve základné kategórie: | ||
− | * [https://docs.oracle.com/en/java/javase/ | + | * [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/RuntimeException.html <tt>RuntimeException</tt>] 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 ([https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ArrayIndexOutOfBoundsException.html <tt>ArrayIndexOutOfBoundsException</tt>]), delenie nulou ([https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ArithmeticException.html <tt>ArithmeticException</tt>]), použitie kódu „očakávajúceho” objekt na referenciu <tt>null</tt> ([https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/NullPointerException.html <tt>NullPointerException</tt>]), atď. Do tejto kategórie patria aj výnimky typu [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/NoSuchElementException.html <tt>NoSuchElementException</tt>], 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 <tt>NoSuchElementException</tt> v našom príklade vyššie). |
− | * Zvyšné podtriedy triedy <tt>Exception</tt>. Tie často reprezentujú neočakávateľné udalosti, ktorým sa na rozdiel od výnimiek typu <tt>RuntimeException</tt> nedá úplne vyhnúť (napríklad [https://docs.oracle.com/en/java/javase/ | + | * Zvyšné podtriedy triedy <tt>Exception</tt>. Tie často reprezentujú neočakávateľné udalosti, ktorým sa na rozdiel od výnimiek typu <tt>RuntimeException</tt> nedá úplne vyhnúť (napríklad [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/FileNotFoundException.html <tt>FileNotFoundException</tt>] a jej nadtrieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/io/IOException.html <tt>IOException</tt>]). 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ť: | S týmto rozdelením podľa druhov chýb reprezentovaných jednotlivými výnimkami súvisí aj nasledujúca nutnosť: | ||
Riadok 215: | Riadok 219: | ||
* Popri vykonaní príkazu <tt>return</tt> 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. | * Popri vykonaní príkazu <tt>return</tt> 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 <tt>RuntimeException</tt> 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 <tt>Error</tt> 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. | * Pri inštanciách triedy <tt>RuntimeException</tt> 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 <tt>Error</tt> 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 <tt>throws</tt> deklarovať aj metóda <tt>main</tt> (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 <tt>Exception</tt>, by mali byť ošetrené. | ||
+ | |||
+ | Z hľadiska použitia tak môžeme výnimky – presnejšie inštancie triedy <tt>Throwable</tt> – rozdeliť na dve základné kategórie: | ||
+ | * '''Nestrážené výnimky''' (angl. ''unchecked exceptions''), ktorými sú inštancie tried <tt>RuntimeException</tt> a <tt>Error</tt> resp. ich podtried. Vyhadzovanie týchto výnimiek nie je potrebné explicitne deklarovať (kompilátor nesleduje miesta ich vzniku). | ||
+ | * '''Strážené výnimky''' (angl. ''checked exceptions''), ktorými sú inštancie triedy <tt>Throwable</tt> resp. jej podtried, ktoré nie sú inštanciami (podtried) triedy <tt>RuntimeException</tt> ani <tt>Error</tt>. Vyhadzovanie týchto výnimiek je potrebné deklarovať explicitne (kompilátor si o ich vyhadzovaní udržiava prehľad). | ||
+ | |||
+ | Prehľad kompilátora o vyhadzovaní strážených výnimiek sa prejavuje napríklad aj nemožnosťou skompilovať program obsahujúci blok <tt>catch</tt> taký, že: | ||
+ | * Typ výnimky odchytávanej v tomto bloku <tt>catch</tt> zahŕňa ''iba'' strážené výnimky (t. j. ide o ''vlastnú'' podtriedu triedy <tt>Exception</tt> a súčasne nejde o triedu <tt>RuntimeException</tt> alebo jej podtriedu). | ||
+ | * V príslušnom bloku <tt>try</tt> sa výnimka tohto typu nikdy nevyhadzuje. | ||
=== Hádzanie výnimiek a výnimky nových typov === | === Hádzanie výnimiek a výnimky nových typov === | ||
Riadok 234: | Riadok 247: | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
public class NegativeElementCountException extends Exception { | public class NegativeElementCountException extends Exception { | ||
− | Integer number; | + | private Integer number; |
public NegativeElementCountException() { | public NegativeElementCountException() { | ||
Riadok 254: | Riadok 267: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | Keďže trieda <tt>NegativeElementCountException</tt> dedí od triedy <tt>Exception</tt>, pôjde o stráženú výnimku. Nestráženú výnimku by sme dostali dedením od triedy <tt>RuntimeException</tt> alebo jej podtried. | ||
Použitie tejto výnimky v samotnom programe potom môže vyzerať napríklad takto. | Použitie tejto výnimky v samotnom programe potom môže vyzerať napríklad takto. | ||
Riadok 294: | Riadok 309: | ||
== Generické programovanie == | == Generické programovanie == | ||
− | V minulom semestri | + | V minulom semestri ste 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 <tt>T</tt>, či binárny strom uchovávajúci v uzloch hodnoty typu <tt>T</tt>. Typ <tt>T</tt> 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 <tt>T</tt>). Zakaždým ste ale implementácie týchto dátových štruktúr písali iba pre jeden pevne zvolený typ; v C++ ste síce pomocou <tt>typedef</tt> vedeli tento typ rýchlo meniť, ale mohli ste sa tak dostať do problémov v situáciách, keď ste 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 | + | 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 <tt>Object</tt> – inštanciami triedy <tt>Object</tt> 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 <tt>String</tt>, č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 <tt>String</tt>). 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 === | === Generické triedy === | ||
Riadok 312: | Riadok 327: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Vo vnútri generickej triedy možno 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: | + | 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: |
<syntaxhighlight lang="Java"> | <syntaxhighlight lang="Java"> | ||
import java.util.*; | import java.util.*; | ||
Riadok 326: | Riadok 341: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Pri vytváraní inštancií generickej triedy za typový parameter dosadíme | + | 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): |
<syntaxhighlight lang="Java"> | <syntaxhighlight lang="Java"> | ||
− | Integer | + | Integer[] a = {1, 2, 3, 4, 5}; |
GenerickaTrieda<Integer> g = new GenerickaTrieda<Integer>(a); | GenerickaTrieda<Integer> g = new GenerickaTrieda<Integer>(a); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
alebo skrátene | alebo skrátene | ||
<syntaxhighlight lang="Java"> | <syntaxhighlight lang="Java"> | ||
− | Integer | + | Integer[] a = {1, 2, 3, 4, 5}; |
GenerickaTrieda<Integer> g = new GenerickaTrieda<>(a); | GenerickaTrieda<Integer> g = new GenerickaTrieda<>(a); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Pre | + | * Zápis <tt><></tt> (takzvaný ''diamant'') možno použiť kedykoľvek je typový argument zrejmý z kontextu (využíva sa tu mechanizmus automatickej inferencie typov). |
− | * Ak je pritom <tt> | + | * '''Pozor:''' hoci podobne ako „<tt>GenerickaTrieda<Integer> g = new GenerickaTrieda<>(a);</tt>” skompiluje aj „<tt>GenerickaTrieda<Integer> g = new GenerickaTrieda(a);</tt>”, je druhý z týchto príkazov ''nesprávny'', keďže v ňom namiesto inštancie generickej triedy vytvárame inštanciu tzv. hrubého typu. |
− | * Napríklad <tt>GenerickaTrieda<Integer></tt> teda '' | + | ** Inštancia hrubého typu <tt>GenerickaTrieda</tt> sa chová podobne ako inštancia typu <tt>GenerickaTrieda<Object></tt> – avšak s tým rozdielom, že ''z čisto historických dôvodov'' je možné inštanciu tohto hrubého typu priradiť napríklad aj do premennej typu <tt>GenerickaTrieda<Integer></tt>, čím sa celý zmysel generického programovania stráca. |
+ | ** Príkaz <tt>GenerickaTrieda<Integer> g = new GenerickaTrieda(a)</tt> by skompiloval napríklad aj v prípade, keby <tt>a</tt> bolo pole reťazcov – tým vzniká priestor na chyby. Ešte horšia situácia by nastala, keby sme typový parameter vynechali aj na ľavej strane v type referencie. | ||
+ | ** '''Hrubé typy preto nikdy nepoužívame.''' Kompilátor <tt>javac</tt> na ich použitie typicky upozorňuje hláškou „<tt>Note: Some input files use unchecked or unsafe operations.</tt>”, ktorá sa zobrazuje aj na testovači (čo pri hodnotených zadaniach bude považované za chybu). | ||
+ | |||
+ | Pre každý konkrétny neprimitívny typ <tt>Typ</tt> teda máme parametrizovaný typ <tt>GenerickaTrieda<Typ></tt>, 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 <tt>Typ</tt> podtriedou triedy <tt>InyTyp</tt>, tak každý objekt typu <tt>Typ</tt> je súčasne aj inštanciou triedy <tt>InyTyp</tt>. Táto relácia sa ''neprenáša'' na typy <tt>GenerickaTrieda<Typ></tt> a <tt>GenerickaTrieda<InyTyp></tt>. | ||
+ | * Napríklad inštanciu typu <tt>GenerickaTrieda<Integer></tt> teda ''nemožno'' použiť v situácii, keď sa očakáva inštancia typu <tt>GenerickaTrieda<Object></tt>. | ||
* Aj generická trieda ale môže dediť od inej triedy, rovnako ako každá iná trieda: | * Aj generická trieda ale môže dediť od inej triedy, rovnako ako každá iná trieda: | ||
<syntaxhighlight lang="Java"> | <syntaxhighlight lang="Java"> | ||
Riadok 350: | Riadok 371: | ||
class Trieda3<T> extends Trieda2<T> { | class Trieda3<T> extends Trieda2<T> { | ||
− | // OK, pre | + | // OK, pre kazde T je instancia parametrizovanej triedy Trieda3<T> sucasne aj instanciou Trieda2<T>. |
} | } | ||
class Trieda4<T> extends Trieda2<Integer> { | class Trieda4<T> extends Trieda2<Integer> { | ||
− | // OK, pre vsetky T je Trieda4<T> | + | // OK, pre vsetky T je instancia Trieda4<T> sucasne aj instanciou Trieda2<Integer>. |
+ | // Parameter T pre Trieda4 nijak nesuvisi s parametrom T pre Trieda2. | ||
} | } | ||
Riadok 365: | Riadok 387: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | * Podobne ako generické triedy možno | + | * Podobne ako generické triedy možno tvoriť aj generické rozhrania. |
=== ''Príklad'': uzol binárneho stromu ako generická trieda === | === ''Príklad'': uzol binárneho stromu ako generická trieda === | ||
+ | |||
+ | Základ generickej triedy <tt>Node<T></tt> reprezentujúcej uzol binárneho stromu uchovávajúci inštanciu <tt>value</tt> nejakej triedy <tt>T</tt> môže vyzerať napríklad nasledovne. | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | 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 + ") "; | ||
+ | } | ||
+ | if (right != null) { | ||
+ | rightString = " (" + right + ")"; | ||
+ | } | ||
+ | return leftString + value + rightString; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | * Trieda <tt>Node<T></tt> teda poskytuje konštruktor, ktorý ako parameter berie uchovávanú inštanciu triedy <tt>T</tt> a referencie na ľavého a pravého syna. | ||
+ | * Trieda taktiež implementuje metódu <tt>toString</tt>, ktorá vráti „infixovú textovú reprezentáciu” stromu. | ||
+ | |||
+ | Program využívajúci triedu <tt>Node<T></tt> môže vyzerať napríklad takto: | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | 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); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
=== Generické metódy === | === 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 <tt>createPerfectTree</tt> vytvorí dokonalý binárny strom danej výšky <tt>height</tt> s uzlami obsahujúcimi všetky rovnakú inštanciu <tt>value</tt> triedy dosadenej ako argument za typový parameter <tt>T</tt>. | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | public class Trieda { | ||
+ | |||
+ | public static <T> Node<T> createPerfectTree(int height, T value) { | ||
+ | if (height == 0) { | ||
+ | return new Node<>(value, null, null); | ||
+ | } else { | ||
+ | return new Node<>(value, createPerfectTree(height - 1, value), createPerfectTree(height - 1, value)); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public static void main(String[] args) { | ||
+ | Node<Integer> root1 = Trieda.<Integer>createPerfectTree(4, 0); | ||
+ | Node<String> root2 = createPerfectTree(4, "retazec"); // Skrateny zapis spoliehajuci sa na automaticku inferenciu typu | ||
+ | System.out.println(root1); | ||
+ | System.out.println(root2); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
=== Ohraničené typové parametre === | === 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 <tt>Node<T></tt> chceli pridať metódu, ktorá nájde ''najmenší'' prvok typu <tt>T</tt> 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 <tt>T</tt> definované úplné usporiadanie – čiže keď možno inštancie triedy <tt>T</tt> navzájom porovnávať. V štandardných triedach jazyka Java býva táto možnosť vyjadrená tým, že daná trieda <tt>T</tt> implementuje rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Comparable.html <tt>Comparable<T></tt>]. | ||
+ | |||
+ | Trieda <tt>T</tt> implementujúca <tt>Comparable<U></tt> pre nejaký – vo všeobecnosti aj iný – typ <tt>U</tt>, musí poskytovať metódu | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | public int compareTo(U o); | ||
+ | </syntaxhighlight> | ||
+ | ktorá vráti záporné číslo, nulu, alebo kladné číslo podľa toho, či je inštancia <tt>this</tt> triedy <tt>T</tt> menšia, rovná, alebo väčšia ako inštancia <tt>o</tt> typu <tt>U</tt>. To samozrejme dáva najväčší zmysel, keď je <tt>U</tt> to isté ako <tt>T</tt> – v takom prípade je možné inštancie triedy <tt>T</tt> porovnávať medzi sebou. Triedy <tt>T</tt> implementujúce rozhranie <tt>Comparable<T></tt> vždy poskytujú metódu | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | public int compareTo(T o); | ||
+ | </syntaxhighlight> | ||
+ | s významom opísaným vyššie. | ||
+ | |||
+ | Chceli by sme teda obmedziť typový parameter pre generickú triedu <tt>Node<T></tt> tak, aby zaň mohla byť dosadená iba inštancia triedy <tt>T</tt> implementujúcej rozhranie <tt>Comparable<T></tt>. To možno urobiť pomocou takzvaného ''ohraničeného typového parametra''. Pre ľubovoľnú triedu <tt>AnyClass</tt> a pre ľubovoľné rozhranie <tt>AnyInterface</tt> možno typový parameter písať ako | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | <T extends AnyClass> | ||
+ | </syntaxhighlight> | ||
+ | resp. | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | <T extends AnyInterface> | ||
+ | </syntaxhighlight> | ||
+ | čím sa vyjadrí, že typový parameter <tt>T</tt> musí byť podtriedou triedy <tt>AnyClass</tt> resp. musí implementovať rozhranie <tt>AnyInterface</tt> (aj pri rozhraniach sa naozaj píše slovo <tt>extends</tt>). | ||
+ | |||
+ | Upravená trieda pre uzol binárneho stromu tak môže vyzerať napríklad nasledovne. | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | 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) { | ||
+ | T leftValue = left.minValue(); | ||
+ | if (result.compareTo(leftValue) > 0) { | ||
+ | result = leftValue; | ||
+ | } | ||
+ | } | ||
+ | if (right != null) { | ||
+ | T rightValue = right.minValue(); | ||
+ | if (result.compareTo(rightValue) > 0) { | ||
+ | result = rightValue; | ||
+ | } | ||
+ | } | ||
+ | return result; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <syntaxhighlight lang="Java"> | ||
+ | public class Trieda { | ||
+ | |||
+ | public static <T extends Comparable<T>> Node<T> createPerfectTree(int height, T value) { | ||
+ | if (height == 0) { | ||
+ | return new Node<>(value, null, null); | ||
+ | } else { | ||
+ | return new Node<>(value, createPerfectTree(height - 1, value), createPerfectTree(height - 1, value)); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | public static void main(String[] args) { | ||
+ | // ... | ||
+ | System.out.println(root.minValue()); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
=== Divoké karty === | === Divoké karty === | ||
+ | |||
+ | Vieme už, že ak je trieda <tt>SubClass</tt> podtriedou triedy <tt>SuperClass</tt>, tak napríklad inštancia parametrizovanej triedy <tt>Node<SubClass></tt> už nebude inštanciou parametrizovanej triedy <tt>Node<SuperClass></tt>. Hoci teda každý uzol prvého typu môže byť svojím spôsobom reprezentovaný aj uzlom druhého typu, nemožno v situácii, keď sa požaduje inštancia typu <tt>Node<SuperClass></tt>, použiť inštanciu typu <tt>Node<SubClass></tt>. Keby sme teda napríklad chceli napísať (bežnú negenerickú) metódu <tt>f</tt>, ktorá berie ako argument uzol binárneho stromu uchovávajúci inštanciu triedy <tt>SuperClass</tt>, alebo niektorej jej podtriedy, ''nestačilo'' by napísať metódu | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | public static void f(Node<SuperClass> root) { | ||
+ | // ... | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | Keby sme totiž skúsili zavolať túto metódu s parametrom typu <tt>Node<SubClass></tt>, program by neskompiloval. Riešením v takýchto situáciách je použitie tzv. ''divokej karty'' (angl. ''wildcard'') reprezentovanej symbolom „<tt>?</tt>”. Napríklad <tt>Node<? extends SuperClass> root</tt> reprezentuje argument metódy ľubovoľného z typov <tt>Node<SomeClass></tt>, kde <tt>SomeClass</tt> je podtriedou <tt>SuperClass</tt> 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é. | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | public static void f(Node<? extends SuperClass> root) { | ||
+ | SuperClass minValue = root.minValue(); | ||
+ | // ... | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | Spolu s takýmito „zhora ohraničenými” divokými kartami možno používať aj „zdola ohraničené” divoké karty v tvare <tt><? super SubClass></tt>, ktorými sa vyjadrí požiadavka, že <tt>T</tt> je ľubovoľná nadtrieda triedy <tt>SubClass</tt> kompatibilná s ohraničením typového parametra danej generickej triedy. Podobne možno používať aj „neohraničenú” divokú kartu <tt><?></tt> vyjadrujúcu ľubovoľnú triedu kompatibilnú s ohraničením typového parametra danej generickej triedy (pre generickú triedu <tt>Node<T extends Comparable<T>></tt> je teda napríklad parameter <tt>Node<?> root</tt> ekvivalentný parametru <tt>Node<? extends Comparable<?>> root</tt>). | ||
== Úvod do Java Collections == | == Ú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 <tt>java.util</tt> a vyznačujú sa predovšetkým tým, že implementujú generické rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collection.html <tt>Collection<E></tt>] 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 <tt>ArrayList</tt> === | ||
+ | |||
+ | Generická trieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ArrayList.html <tt>ArrayList<E></tt>] reprezentuje ''dynamické'' pole prvkov typu <tt>E</tt>. Toto pole mení svoju veľkosť podľa momentálnej potreby. | ||
+ | * Prvok na koniec poľa pridáva metóda <tt>add</tt>. | ||
+ | * Prvok na danej pozícii dostaneme metódou <tt>get</tt>. | ||
+ | * Metóda <tt>set</tt> mení prvok na danej pozícii na určenú hodnotu. | ||
+ | * Dĺžku poľa vracia metóda <tt>size</tt>. | ||
+ | |||
+ | ''Príklad'': | ||
+ | <syntaxhighlight lang="Java"> | ||
+ | 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) + " "); | ||
+ | } | ||
+ | System.out.println(a); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <small>'''Pozor''': trieda <tt>ArrayList<E></tt> poskytuje aj konštruktor s jedným celočíselným parametrom <tt>initialCapacity</tt>. Častou chybou býva nepochopenie významu tohto parametra. Ten totiž neurčuje počiatočnú dĺžku vytváraného poľa, ale iba počiatočný objem pamäte interne alokovanej pre dané pole (podobne ako pri manuálnej implementácii dynamického poľa z minulého semestra). Volanie konštruktora s parametrom 10 teda stále vytvorí prázdne pole, avšak interne sa preň alokuje pamäť postačujúca na uloženie desiatich prvkov. Pri pridávaní prvých desiatich prvkov sa tak pamäť nemusí realokovať. Vhodným nastavením tohto parametra teda možno trochu ovplyvniť efektívnosť vykonávania programu; vo väčšine prípadov je však lepšie použiť konštruktor bez parametra.</small> | ||
+ | |||
+ | === Rozhranie <tt>List</tt> a trieda <tt>LinkedList</tt> === | ||
+ | |||
+ | Veľká časť metód poskytovaných inštanciami triedy <tt>ArrayList</tt> je deklarovaná v rozhraní <tt>List</tt>, ktoré je implementované triedami reprezentujúcimi nejaký typ zoznamu. Popri triede <tt>ArrayList<E></tt> je ďalšou triedou implementujúcou toto rozhranie trieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/LinkedList.html <tt>LinkedList<E></tt>] reprezentujúca obojsmerne spájaný zoznam. | ||
+ | * Keďže implementuje rovnaké rozhranie <tt>List<E></tt> ako <tt>ArrayList<E></tt>, 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 <tt>List</tt>; v takom prípade možno metódu volať ako pre <tt>ArrayList</tt>, tak aj pre <tt>LinkedList</tt>. | ||
+ | * Treba však pamätať na to, že odlišná implementácia tried <tt>ArrayList</tt> a <tt>LinkedList</tt> 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 (<tt>LinkedList</tt> tu oproti rozhraniu <tt>List</tt> a triede <tt>ArrayList</tt> 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 <tt>get</tt>. | ||
+ | * Vypísanie všetkých prvkov spájaného zoznamu by sme teda v princípe mohli realizovať aj rovnako ako pre polia: | ||
+ | : <syntaxhighlight lang="Java"> | ||
+ | LinkedList<Integer> list = new LinkedList<>(); | ||
+ | // ... | ||
+ | for (int i = 0; i <= list.size() - 1; i++) { | ||
+ | System.out.print(list.get(i) + " "); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | : Omnoho efektívnejšie je ale použitie ''iterátora'' (o tom viac nabudúce): | ||
+ | : <syntaxhighlight lang="Java"> | ||
+ | LinkedList<Integer> list = new LinkedList<>(); | ||
+ | // ... | ||
+ | for (Iterator<Integer> it = list.iterator(); it.hasNext(); ) { | ||
+ | System.out.print(it.next() + " "); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | : Prípadne tiež možno použiť ekvivalentnú konštrukciu | ||
+ | : <syntaxhighlight lang="Java"> | ||
+ | LinkedList<Integer> list = new LinkedList<>(); | ||
+ | // ... | ||
+ | for (Integer x : list) { | ||
+ | System.out.print(x + " "); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Niektoré ďalšie dátové štruktúry === | ||
+ | |||
+ | Java Collections obsahuje aj množstvo ďalších tried a rozhraní. Napríklad: | ||
+ | * Rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Set.html <tt>Set<E></tt>] pre množiny a jeho implementácie ako napríklad [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashSet.html <tt>HashSet<E></tt>]. | ||
+ | * Rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Map.html <tt>Map<K,V></tt>] pre zobrazenia (resp. slovníky alebo asociatívne polia), pri ktorých sú kľúče typu <tt>K</tt> zobrazované na hodnoty typu <tt>V</tt>. Implementáciou tohto rozhrania je napríklad trieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashMap.html <tt>HashMap<K,V></tt>]. | ||
+ | * Rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Queue.html <tt>Queue<E></tt>] pre rad, ktoré okrem iného implementuje aj trieda <tt>LinkedList<E></tt>. Triedu <tt>LinkedList<E></tt> možno použiť aj ako zásobník, pretože implementuje rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Deque.html <tt>Deque<E></tt>] pre ''obojstranný rad'', ktoré okrem iného deklaruje metódy <tt>push</tt> a <tt>pop</tt> (implementácie rozhrania <tt>Deque</tt> sú pritom efektívnejšie, než trieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Stack.html <tt>Stack<E></tt>], ktorú Java Collections tiež obsahuje). | ||
+ | * ... | ||
== Odkazy == | == Odkazy == | ||
Riadok 381: | Riadok 644: | ||
* [https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html Java tutoriál: Exceptions] | * [https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html Java tutoriál: Exceptions] | ||
* [https://docs.oracle.com/javase/tutorial/java/generics/index.html Java tutoriál: Generics] | * [https://docs.oracle.com/javase/tutorial/java/generics/index.html Java tutoriál: Generics] | ||
+ | * [https://docs.oracle.com/javase/tutorial/collections/index.html Java tutoriál: Collections] |
Aktuálna revízia z 18:48, 10. marec 2024
Oznamy
- Počas zajtrajších cvičení – čiže od 9:50 do 11:20 – bude prebiehať prvý test zameraný na látku z prvých troch týždňov semestra. Body z testu bude možné získať iba v prípade prítomnosti na cvičeniach v miestnosti I-H6.
- Krátko po dnešnej prednáške bude na testovači zverejnené zadanie prvej domácej úlohy, ktorú bude potrebné odovzdať najneskôr do utorka 26. marca, 9:50 – čiže do začiatku šiestych cvičení.
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í 0 až n-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("Chybne zadanych n celociselnych prvkov pola.");
System.exit(3);
}
}
String next = null;
if (scanner.hasNext()) {
next = scanner.next();
} else {
System.err.println("Chyba druha cast vstupu.");
System.exit(4);
}
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(6);
}
} else {
System.err.println("Niektory z indexov do pola nebol zadany ako cele cislo.");
System.exit(5);
}
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ť, ktorá môže nastať počas vykonávania programu. 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.
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 inštancie 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. Kód nasledujúci za blokom catch sa vykoná bez ohľadu na to, či takto odchytená 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 tri 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 zatvárajúceho scanner až za koniec príslušného bloku catch; samozrejme s ošetrením prípadu, keď scanner == null. 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; nevykoná sa ale napríklad v prípade, keď vykonávanie programu ukončíme metódou System.exit).
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ú obyčajne rôznych typov (vždy ale ide o inštancie podtried Throwable a typicky 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.
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 predvídateľ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é.
Z hľadiska použitia tak môžeme výnimky – presnejšie inštancie triedy Throwable – rozdeliť na dve základné kategórie:
- Nestrážené výnimky (angl. unchecked exceptions), ktorými sú inštancie tried RuntimeException a Error resp. ich podtried. Vyhadzovanie týchto výnimiek nie je potrebné explicitne deklarovať (kompilátor nesleduje miesta ich vzniku).
- Strážené výnimky (angl. checked exceptions), ktorými sú inštancie triedy Throwable resp. jej podtried, ktoré nie sú inštanciami (podtried) triedy RuntimeException ani Error. Vyhadzovanie týchto výnimiek je potrebné deklarovať explicitne (kompilátor si o ich vyhadzovaní udržiava prehľad).
Prehľad kompilátora o vyhadzovaní strážených výnimiek sa prejavuje napríklad aj nemožnosťou skompilovať program obsahujúci blok catch taký, že:
- Typ výnimky odchytávanej v tomto bloku catch zahŕňa iba strážené výnimky (t. j. ide o vlastnú podtriedu triedy Exception a súčasne nejde o triedu RuntimeException alebo jej podtriedu).
- V príslušnom bloku try sa výnimka tohto typu nikdy nevyhadzuje.
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 {
private 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.";
}
}
}
Keďže trieda NegativeElementCountException dedí od triedy Exception, pôjde o stráženú výnimku. Nestráženú výnimku by sme dostali dedením od triedy RuntimeException alebo jej podtried.
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 ste 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 ste ale implementácie týchto dátových štruktúr písali iba pre jeden pevne zvolený typ; v C++ ste síce pomocou typedef vedeli tento typ rýchlo meniť, ale mohli ste sa tak dostať do problémov v situáciách, keď ste 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).
- Pozor: hoci podobne ako „GenerickaTrieda<Integer> g = new GenerickaTrieda<>(a);” skompiluje aj „GenerickaTrieda<Integer> g = new GenerickaTrieda(a);”, je druhý z týchto príkazov nesprávny, keďže v ňom namiesto inštancie generickej triedy vytvárame inštanciu tzv. hrubého typu.
- Inštancia hrubého typu GenerickaTrieda sa chová podobne ako inštancia typu GenerickaTrieda<Object> – avšak s tým rozdielom, že z čisto historických dôvodov je možné inštanciu tohto hrubého typu priradiť napríklad aj do premennej typu GenerickaTrieda<Integer>, čím sa celý zmysel generického programovania stráca.
- Príkaz GenerickaTrieda<Integer> g = new GenerickaTrieda(a) by skompiloval napríklad aj v prípade, keby a bolo pole reťazcov – tým vzniká priestor na chyby. Ešte horšia situácia by nastala, keby sme typový parameter vynechali aj na ľavej strane v type referencie.
- Hrubé typy preto nikdy nepoužívame. Kompilátor javac na ich použitie typicky upozorňuje hláškou „Note: Some input files use unchecked or unsafe operations.”, ktorá sa zobrazuje aj na testovači (čo pri hodnotených zadaniach bude považované za chybu).
Pre každý konkrétny neprimitívny typ Typ teda máme parametrizovaný typ GenerickaTrieda<Typ>, 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 Typ podtriedou triedy InyTyp, tak každý objekt typu Typ je súčasne aj inštanciou triedy InyTyp. Táto relácia sa neprenáša na typy GenerickaTrieda<Typ> a GenerickaTrieda<InyTyp>.
- 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 + ") ";
}
if (right != null) {
rightString = " (" + right + ")";
}
return leftString + 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 createPerfectTree vytvorí dokonalý binárny strom danej výšky height s uzlami obsahujúcimi všetky rovnakú inštanciu value triedy dosadenej ako argument za typový parameter T.
public class Trieda {
public static <T> Node<T> createPerfectTree(int height, T value) {
if (height == 0) {
return new Node<>(value, null, null);
} else {
return new Node<>(value, createPerfectTree(height - 1, value), createPerfectTree(height - 1, value));
}
}
public static void main(String[] args) {
Node<Integer> root1 = Trieda.<Integer>createPerfectTree(4, 0);
Node<String> root2 = createPerfectTree(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ý – typ 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 typu 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) {
T leftValue = left.minValue();
if (result.compareTo(leftValue) > 0) {
result = leftValue;
}
}
if (right != null) {
T rightValue = right.minValue();
if (result.compareTo(rightValue) > 0) {
result = rightValue;
}
}
return result;
}
}
public class Trieda {
public static <T extends Comparable<T>> Node<T> createPerfectTree(int height, T value) {
if (height == 0) {
return new Node<>(value, null, null);
} else {
return new Node<>(value, createPerfectTree(height - 1, value), createPerfectTree(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 môže byť svojím spôsobom reprezentovaný aj uzlom 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<T>> 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) + " ");
}
System.out.println(a);
}
}
Pozor: trieda ArrayList<E> poskytuje aj konštruktor s jedným celočíselným parametrom initialCapacity. Častou chybou býva nepochopenie významu tohto parametra. Ten totiž neurčuje počiatočnú dĺžku vytváraného poľa, ale iba počiatočný objem pamäte interne alokovanej pre dané pole (podobne ako pri manuálnej implementácii dynamického poľa z minulého semestra). Volanie konštruktora s parametrom 10 teda stále vytvorí prázdne pole, avšak interne sa preň alokuje pamäť postačujúca na uloženie desiatich prvkov. Pri pridávaní prvých desiatich prvkov sa tak pamäť nemusí realokovať. Vhodným nastavením tohto parametra teda možno trochu ovplyvniť efektívnosť vykonávania programu; vo väčšine prípadov je však lepšie použiť konštruktor bez parametra.
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>.
- 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 (implementácie rozhrania Deque sú pritom efektívnejšie, než trieda Stack<E>, ktorú Java Collections tiež obsahuje).
- ...