Programovanie (2) v Jave
1-INF-166, letný semester 2023/24
Letný semester, prednáška č. 5: Rozdiel medzi revíziami
(120 medziľahlých úprav od rovnakého používateľa nie je zobrazených.) | |||
Riadok 1: | Riadok 1: | ||
== Oznamy == | == Oznamy == | ||
− | * | + | * Minulý utorok bol v zadaní prvej domácej úlohy opravený chybný príklad vstupu č. 1. Ďakujem Filipovi Sivičekovi za upozornenie. |
− | * | + | * Riešenia domácej úlohy, ktoré v triede <tt>MarkInitialCellProgram</tt> nechávajú robota zapisovať na políčka toru referenciu <tt>null</tt>, ''nie sú správne'' – aj keď testovač možno takýmto spôsobom oklamať. |
+ | ** Zadanie hovorí, že robot zapisuje booleovské hodnoty; <tt>null</tt> nie je booleovská hodnota. (Po správnosti by trieda <tt>Move</tt> mala v takom prípade vyhadzovať výnimku.) | ||
+ | ** Ak <tt>null</tt> považujete za booleovskú hodnotu, fungovalo by vaše riešenie aj v prípade, že by na začiatku simulácie boli na niektorých políčkach takéto hodnoty zapísané? | ||
+ | * Budúci týždeň počas cvičení – čiže ''v utorok 26. marca od 9:50 do 11:20'' – bude prebiehať druhý test zameraný na látku z prvých piatich týždňov. Body z testu bude možné získať iba v prípade prítomnosti na cvičeniach v miestnosti I-H6. | ||
== Java Collections == | == Java Collections == | ||
− | Detailnejšie teraz preskúmame štandardné dátové štruktúry implementované v balíku <tt>java.util</tt> a známe pod súhrnným názvom ''Java Collections''. Na nasledujúcom obrázku je znázornený diagram 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á | + | Detailnejšie teraz preskúmame štandardné dátové štruktúry implementované v balíku <tt>java.util</tt> 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. |
[[Súbor:Collections.png|center|750px]] | [[Súbor:Collections.png|center|750px]] | ||
Riadok 12: | Riadok 15: | ||
=== Rozhranie <tt>Collection</tt> === | === Rozhranie <tt>Collection</tt> === | ||
− | 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 <tt>E</tt> – implementuje generické rozhranie [https://docs.oracle.com/en/java/javase/ | + | 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 <tt>E</tt> – implementuje generické rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collection.html <tt>Collection<E></tt>]. 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í <tt>Collection<E></tt> patria nasledujúce: |
* Metóda <tt>boolean contains(Object o)</tt> vráti <tt>true</tt> práve vtedy, keď dané zoskupenie objektov obsahuje objekt <tt>o</tt>. | * Metóda <tt>boolean contains(Object o)</tt> vráti <tt>true</tt> práve vtedy, keď dané zoskupenie objektov obsahuje objekt <tt>o</tt>. | ||
− | * Metóda <tt>boolean add(E e)</tt> pridá inštanciu <tt>e</tt> typu <tt>E</tt> 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 <tt>Collection<E></tt> môže byť správanie tejto metódy bližšie určené: napríklad u zoznamov sa typicky | + | * Metóda <tt>boolean add(E e)</tt> pridá inštanciu <tt>e</tt> typu <tt>E</tt> 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 <tt>Collection<E></tt> 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 <tt>Collection<E></tt> ju môžu implementovať iba ako vyhodenie výnimky typu <tt>UnsupportedOperationException</tt>. |
− | * Metóda <tt>boolean remove(Object o)</tt> odoberie zo zoskupenia jeden výskyt argumentu <tt>o</tt>. Opäť ide o nepovinnú metódu, ktorej správanie môže byť v jednotlivých implementáciách rozhrania bližšie špecifikované. | + | * Metóda <tt>boolean remove(Object o)</tt> odoberie zo zoskupenia jeden výskyt argumentu <tt>o</tt>. 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 <tt>remove</tt>, ktorá odoberie prvok na danom indexe; táto sa ale v rozhraní pre všeobecné zoskupenia objektov nedeklaruje.) |
* Metóda <tt>int size()</tt> vráti veľkosť daného zoskupenia objektov. | * Metóda <tt>int size()</tt> vráti veľkosť daného zoskupenia objektov. | ||
* Metóda <tt>boolean isEmpty()</tt> zistí, či je dané zoskupenie objektov prázdne. | * Metóda <tt>boolean isEmpty()</tt> zistí, či je dané zoskupenie objektov prázdne. | ||
* Metóda <tt>Iterator<E> iterator()</tt> 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. | * Metóda <tt>Iterator<E> iterator()</tt> 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. | ||
− | Je ešte potrebné ujasniť si, čo napríklad pri metóde <tt>contains</tt> znamená, že zoskupenie ''obsahuje'' objekt <tt>o</tt>, prípadne kedy sa pri metóde <tt>remove</tt> nejaký objekt považuje za ''výskyt'' jej argumentu. V obdivoch prípadoch sa na porovnávanie objektov používa metóda <tt>equals</tt>, 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 <tt>toString</tt> definovaná už v triede <tt>Object</tt> – jej východzou implementáciou je porovnávanie referencií pomocou operátora <tt>==</tt> – a v iných triedach môže byť prekrytá prirodzenejšou implementáciou. Programátor | + | === Použitie metódy <tt>equals</tt> === |
+ | |||
+ | Je ešte potrebné ujasniť si, čo napríklad pri metóde <tt>contains</tt> znamená, že zoskupenie ''obsahuje'' objekt <tt>o</tt>, prípadne kedy sa pri metóde <tt>remove</tt> nejaký objekt považuje za ''výskyt'' jej argumentu. V obdivoch prípadoch sa na porovnávanie objektov používa metóda <tt>equals</tt>, 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 <tt>toString</tt> definovaná už v triede [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html <tt>Object</tt>] – jej východzou implementáciou je porovnávanie referencií pomocou operátora <tt>==</tt> – a v iných triedach môže byť prekrytá prirodzenejšou implementáciou. Viacero štandardných tried (napr. <tt>String</tt> a <tt>Integer</tt>) túto metódu vhodným spôsobom prekrýva. | ||
+ | * Programátor prekrytej metódy <tt>equals</tt> by mal vždy zabezpečiť, aby išlo o ''reláciu ekvivalencie'' (reflexívnu, symetrickú a súčasne tranzitívnu reláciu). | ||
+ | * Pokiaľ nedôjde k ich modifikácii, mali by byť pre danú dvojicu objektov výstupy metódy <tt>equals</tt> vždy rovnaké. | ||
+ | * Dátové štruktúry, ktoré sú súčasťou Java Collections, sa na tieto vlastnosti metódy <tt>equals</tt> spoliehajú – iba za ich splnenia je teda garantované, že sa budú správať očakávaným spôsobom. | ||
+ | * Niektoré z týchto dátových štruktúr (napr. množiny) sa nemusia správať rozumne v prípade, že dôjde k internej modifikácii niektorého z ich prvkov, ktorá vyústi v odlišné výstupy metódy <tt>equals</tt> pre tento prvok. Pri práci so zoskupeniami modifikovateľných objektov teda treba postupovať s veľkou opatrnosťou. | ||
''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. | ''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. | ||
Riadok 41: | Riadok 50: | ||
@Override | @Override | ||
− | public | + | public boolean equals(Object o) { |
− | if (o | + | if (o == null) { |
− | |||
− | |||
return false; | return false; | ||
} | } | ||
+ | return o.getClass() == this.getClass() && ((Point) o).x == x && ((Point) o).y == y; | ||
} | } | ||
} | } | ||
Riadok 63: | Riadok 71: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Metóda <tt>equals</tt> nemusí byť nutne implementovaná tak, aby sa rovnali iba inštancie toho istého typu – často napríklad môže existovať viacero tried, ktorých inštancie intuitívne reprezentujú tú istú entitu (napríklad inštancie <tt>ArrayList<E></tt> aj inštancie <tt>LinkedList<E></tt> reprezentujú zoznamy prvkov typu <tt>E</tt>) a metóda <tt>equals</tt> sa v takom prípade môže implementovať ako rovnosť týchto entít bez ohľadu na typ reprezentujúcej inštancie. To je okrem iného aj dôvodom, prečo sú argumentmi metód <tt>contains</tt> a <tt>remove</tt> deklarovaných v rozhraní <tt>Collection<E></tt> inštancie triedy <tt>Object</tt> a nie inštancie typu <tt>E</tt>. | |
=== Zoznamy === | === Zoznamy === | ||
+ | |||
+ | Triedy pre zoznamy sú implementáciami rozhrania [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html <tt>List<E></tt>]. | ||
+ | * Prvok na koniec zoznamu 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 a dĺžku zoznamu vracia metóda <tt>size</tt>. | ||
+ | Dvoma najdôležitejšími triedami implementujúcimi rozhranie <tt>List<E></tt> sú: | ||
+ | * Trieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ArrayList.html <tt>ArrayList<E></tt>] reprezentujúca zoznamy implementované pomocou ''dynamických'' polí, ktoré dokážu automaticky meniť objem alokovanej pamäte. Konštruktor bez parametrov vytvorí prázdny zoznam, rovnako ako konštruktor s celočíselným parametrom <tt>initialCapacity</tt> umožňujúci nastaviť počiatočný objem alokovanej pamäte. | ||
+ | :''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> | ||
+ | * 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. | ||
+ | |||
+ | 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. | ||
+ | :<syntaxhighlight lang="java"> | ||
+ | ArrayList<Integer> a1 = new ArrayList<>(); | ||
+ | |||
+ | // ... | ||
+ | |||
+ | ArrayList<Integer> a2 = new ArrayList<>(a1); | ||
+ | LinkedList<Integer> list = new LinkedList<>(a1); | ||
+ | </syntaxhighlight> | ||
=== Rad a objostranný rad === | === Rad a objostranný rad === | ||
− | === Množiny === | + | * Trieda <tt>LinkedList<E></tt> okrem rozhrania <tt>List<E></tt> implementuje aj rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Queue.html <tt>Queue<E></tt>] reprezentujúce rady (fronty). Na koniec radu pridávame metódou <tt>add</tt> rovnako ako u zoznamov; metóda <tt>remove</tt> (bez parametrov) vyberie a vráti na výstupe prvok zo začiatku radu; metóda <tt>peek</tt> vráti prvok na začiatku radu bez toho, aby ho z radu vybrala. |
+ | * Okrem toho trieda <tt>LinkedList<E></tt> implementuje aj rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Deque.html <tt>Deque<E></tt>] (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 <tt>push</tt> a <tt>pop</tt> umožňujúce pracovať s ním ako so zásobníkom. | ||
+ | * Javovské spájané zoznamy typu <tt>LinkedList<E></tt> teda možno použiť aj ako rad, aj ako zásobník. | ||
+ | |||
+ | === Množiny a použitie metódy <tt>hashCode</tt> === | ||
+ | |||
+ | Triedy pre množiny sú implementáciami rozhrania [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Set.html <tt>Set<E></tt>] a pri práci s nimi sú podstatné predovšetkým metódy <tt>add</tt>, <tt>contains</tt> a <tt>remove</tt>. Tieto pracujú na základe výstupov metódy <tt>equals</tt> pre prvky množiny a správajú sa rozumným spôsobom ''za predpokladu, že nedochádza k modifikácii vnútorného stavu jednotlivých prvkov''. Najdôležitejšou implementáciou množín je trieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashSet.html <tt>HashSet<E></tt>]: | ||
+ | * Ide o implementáciu množín pomocou hešovania. | ||
+ | * Pri hešovaní sa využíva metóda <tt>hashCode</tt> 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 <tt>equals</tt> definovaná už v triede [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html <tt>Object</tt>] a viaceré štandardné triedy ju prekrývajú. Napríklad <tt>String</tt> 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. | ||
+ | * Pokiaľ nedôjde k modifikácii objektu, mala by metóda <tt>hashCode</tt> vracať vždy rovnaký výstup. | ||
+ | * Ďalšou požiadavkou je, aby pre ľubovoľnú dvojicu objektov, pre ktoré vráti metóda <tt>equals</tt> hodnotu <tt>true</tt>, vrátila metóda <tt>hashCode</tt> rovnaké výstupy. ''To znamená, že metódu <tt>hashCode</tt> je žiadúce prekryť kedykoľvek je prekrytá metóda <tt>equals</tt>.'' | ||
+ | * Pre dvojice objektov, pre ktoré <tt>equals</tt> vráti hodnotu <tt>false</tt>, metóda <tt>hashCode</tt> 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. | ||
+ | |||
+ | Trieda <tt>HashSet<E></tt> sa na uvedené vlastnosti metódy <tt>hashCode</tt> prvkov množiny spolieha a v prípade ich porušenia sa nemusí správať korektne. | ||
+ | |||
+ | ''Príklad'': | ||
+ | <syntaxhighlight lang="java"> | ||
+ | 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(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ''Cvičenie'': zakomentujte v kóde triedy <tt>Name</tt> prekrytú metódu <tt>hashCode</tt> a nájdite príklad vstupu, pre ktorý sa v metóde <tt>main</tt> na konzolu vypíšu neočakávané výsledky. | ||
=== Iterátory === | === Iterátory === | ||
+ | |||
+ | Všetky triedy implementujúce rozhranie <tt>Collection<E></tt> implementujú aj rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Iterable.html <tt>Iterable<E></tt>]. Rozhranie <tt>Iterable<T></tt> pritom deklaruje jedinú povinnú metódu <tt>Iterator<T> iterator()</tt>, ktorej výstupom je iterátor – objekt umožňujúci postupne prechádzať cez nejakú sadu prvkov typu <tt>T</tt>. Pri triedach implementujúcich <tt>Collection<E></tt> je výstupom metódy <tt>iterator()</tt> 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 <tt>HashSet</tt> 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 <tt>E</tt> je pritom inštancia ľubovoľnej triedy implementujúcej rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Iterator.html <tt>Iterator<E></tt>]. To deklaruje predovšetkým nasledujúce tri metódy: | ||
+ | * Metóda <tt>E next()</tt> vráti nasledujúci spomedzi prvkov, cez ktoré iterátor prechádza. Ak už žiaden ďalší prvok neexistuje, vyhodí výnimku typu <tt>NoSuchElementException</tt>. | ||
+ | * Metóda <tt>boolean hasNext()</tt> vráti <tt>true</tt>, ak ešte existuje nejaký ďalší prvok – čiže v prípade, že nasledujúce volanie metódy <tt>next</tt> nevyhodí výnimku. | ||
+ | * Nepovinná metóda <tt>default void remove()</tt> z prechádzaného zoskupenia odoberie prvok, ktorý bol dosiaľ posledným výstupom metódy <tt>next</tt>. Túto metódu nemusia implementovať všetky triedy implementujúce rozhranie <tt>Iterator<E></tt> – v takom prípade sa pri pokuse o jej volanie použije východzia implementácia spočívajúca vo vyhodení výnimky typu <tt>UnsupportedOperationException</tt>. | ||
+ | |||
+ | 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 <tt>next</tt> iterátor preskočí ďalší prvok a tento prvok vráti na výstupe. | ||
+ | |||
+ | ::[[Súbor: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 <tt>remove</tt> 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 <tt>list</tt> môžeme pomocou iterátora realizovať napríklad nasledovne: | ||
+ | <syntaxhighlight lang="java"> | ||
+ | LinkedList<Integer> list = new LinkedList<>(); | ||
+ | |||
+ | // ... | ||
+ | |||
+ | for (Iterator<Integer> it = list.iterator(); it.hasNext(); ) { | ||
+ | System.out.print(it.next() + " "); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | Takýto cyklus je pre spájané zoznamy omnoho efektívnejší ako cyklus | ||
+ | <syntaxhighlight lang="java"> | ||
+ | for (int i = 0; i <= list.size() - 1; i++) { | ||
+ | System.out.print(list.get(i) + " "); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | v ktorom je potrebné pre všetky <tt>i</tt> vykonať pomalú operáciu <tt>get(i)</tt>. Pre <tt>ArrayList</tt> je naopak efektívnosť oboch cyklov porovnateľná. | ||
+ | |||
+ | ''Príklad'': Pre jeden zoznam môžeme mať aj viacero nezávislých iterátorov ukazujúcich na rôzne pozície v zozname: | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | for (Iterator<Integer> it1 = list.iterator(); it1.hasNext(); ) { | ||
+ | int x = it1.next(); | ||
+ | for (Iterator<Integer> it2 = list.iterator(); it2.hasNext(); ) { | ||
+ | System.out.print(x + it2.next() + " "); | ||
+ | } | ||
+ | System.out.println(); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Rozhranie <tt>List<E></tt> okrem metódy <tt>iterator</tt> deklaruje aj metódu <tt>listIterator</tt>, ktorá vráti „vylepšený” iterátor implementujúci rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/ListIterator.html <tt>ListIterator<E></tt>]. Takýto iterátor umožňuje pohybovať sa po zozname obidvoma smermi. | ||
+ | |||
+ | === Cyklus <tt>for each</tt> a iterátory === | ||
+ | |||
+ | Cez prvky inštancií <tt>iterable</tt> tried implementujúcich rozhranie <tt>Iterable<T></tt> možno, podobne ako pri poliach, prechádzať pomocou cyklu <tt>for each</tt>: | ||
+ | <syntaxhighlight lang="java"> | ||
+ | for (T x : iterable) { | ||
+ | // ... | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | V takom prípade je (na rozdiel od polí) tento cyklus ekvivalentný cyklu | ||
+ | <syntaxhighlight lang="java"> | ||
+ | for (Iterator<T> it = iterable.iterator(); it.hasNext(); ) { | ||
+ | T x = it.next(); | ||
+ | // ... | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | (za predpokladu, že <tt>it</tt> 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 <tt>MyArrayList<E></tt> sa od triedy <tt>ArrayList</tt> líši iba implementáciou metódy <tt>iterator</tt>. Tá vráti iterátor novodefinovaného typu <tt>NullSkippingArrayListIterator</tt>, ktorý pri prechádzaní prvkov zoznamu vynecháva všetky výskyty referencie <tt>null</tt>. Takéto prekrytie metódy <tt>iterator</tt> v podtriede triedy <tt>ArrayList</tt> nie je dobrá prax, keďže sa ním poruší špecifikácia metódy <tt>iterator</tt> v rozhraní <tt>List<E></tt>; krajšie by bolo napríklad pridať novú metódu <tt>nullSkippingIterator</tt> za súčasného ponechania pôvodnej metódy <tt>iterator</tt>. V nasledujúcom ale ide aj o ukážku toho, že prekrytie metódy <tt>iterator</tt> má vplyv na správanie cyklu <tt>for each</tt>. | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | import java.util.*; | ||
+ | |||
+ | public class MyArrayList<E> extends ArrayList<E> { | ||
+ | |||
+ | @Override | ||
+ | public Iterator<E> iterator() { | ||
+ | return new NullSkippingArrayListIterator<>(this); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | 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. | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
=== Zobrazenia === | === Zobrazenia === | ||
+ | |||
+ | Rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Map.html <tt>Map<K, V></tt>] reprezentuje zobrazenia, ktoré priraďujú nejakej množine ''kľúčov'' (angl. ''keys'') typu <tt>K</tt> ich ''obrazy'' (resp. ''hodnoty'', angl. ''values'') typu <tt>V</tt>. Najdôležitejšou implementáciou tohto rozhrania je trieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/HashMap.html <tt>HashMap<K, V></tt>] založená na hešovaní. Kľúčom aj jeho obrazom tu môže byť aj <tt>null</tt>. Podobne ako <tt>HashSet</tt> využíva táto trieda metódy <tt>hashCode</tt> a <tt>equals</tt> pre jednotlivé kľúče. Niektoré poskytované metódy: | ||
+ | * Metóda <tt>public V get(Object key)</tt> vráti obraz kľúča <tt>key</tt> pri danom zobrazení, ak je definovaný; v opačnom prípade vráti <tt>null</tt>. | ||
+ | * Metóda <tt>public V put(K key, V value)</tt> nastaví obraz kľúča <tt>key</tt> na hodnotu <tt>value</tt>. | ||
+ | * Metóda <tt>public boolean containsKey(Object key)</tt> zistí, či je <tt>key</tt> kľúčom, t. j. či je preň definovaný obraz v danom zobrazení. | ||
+ | * Metóda <tt>public Set<K> keySet()</tt> vráti množinu všetkých kľúčov. | ||
+ | * Metóda <tt>public int size()</tt> 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í). | ||
+ | <syntaxhighlight lang="java"> | ||
+ | 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) + "."); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
=== Usporiadané množiny a zobrazenia na nich === | === Usporiadané množiny a zobrazenia na nich === | ||
− | < | + | Java Collections obsahuje aj rozhrania [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SortedSet.html <tt>SortedSet<E></tt>] resp. [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SortedMap.html <tt>SortedMap<K, V></tt>] rozširujúce rozhrania <tt>Set<E></tt> resp. <tt>Map<K, V></tt>. 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 [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/TreeSet.html <tt>TreeSet<E></tt>] resp. [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/TreeMap.html <tt>TreeMap<K, V></tt>]. |
+ | * V základnom variante – to jest pri použití konštruktora triedy <tt>TreeSet</tt> alebo <tt>TreeMap</tt> 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 <tt>compareTo</tt> (použije sa teda tzv. ''prirodzené usporiadanie'' prvkov). Typicky sa teda <tt>TreeSet<E></tt> a <tt>TreeMap<K, V></tt> používajú v prípade, keď typ <tt>E</tt> resp. <tt>K</tt> implementuje rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Comparable.html <tt>Comparable<E></tt>] resp. <tt>Comparable<K></tt>. Nie je to ale striktne vyžadované: pomocou konštruktora bez parametrov technicky môžeme vytvoriť napríklad aj inštanciu typu <tt>TreeSet<Object></tt>. Akonáhle však do takejto množiny pridáme dvojicu neporovnateľných objektov, dôjde k vyhodeniu výnimky typu [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ClassCastException.html <tt>ClassCastException</tt>]. | ||
+ | * Od prirodzeného usporiadania sa tu vyžaduje, aby bolo konzistentné s metódou <tt>equals</tt>. Pre všetky dvojice objektov <tt>e1, e2</tt> daného typu by teda mal mať logický výraz <tt>e1.compareTo(e2) == 0</tt> vždy tú istú booleovskú hodnotu ako <tt>e1.equals(e2)</tt>. Štandardné triedy implementujúce rozhranie <tt>Comparable</tt>, ako napríklad <tt>Integer</tt> alebo <tt>String</tt>, túto požiadavku spĺňajú. | ||
+ | * Pri iterovaní cez usporiadanú množinu, ako aj cez množinu kľúčov zobrazenia typu <tt>SortedMap</tt>, 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 === | === Komparátory === | ||
+ | |||
+ | Alternatívne možno pri triedach <tt>TreeSet<E></tt> a <tt>TreeMap<K, V></tt> 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 <tt>TreeSet<E></tt> resp. <tt>TreeMap<K, V></tt> pomocou konštruktora, ktorý ako argument berie tzv. ''komparátor'' prvkov typu <tt>E</tt> resp. <tt>K</tt> (alebo ich nadtried). | ||
+ | |||
+ | Pod komparátorom prvkov typu <tt>T</tt> rozumieme ľubovoľnú inštanciu triedy implementujúcej rozhranie [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Comparator.html <tt>Comparator<T></tt>]. | ||
+ | * Toto rozhranie vyžaduje povinnú implementáciu jedinej metódy – a to metódy <tt>int compare(T o1, T o2)</tt> na porovnanie dvojice prvkov typu <tt>T</tt>. | ||
+ | * 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 minimálne pri použití v <tt>TreeSet</tt> a <tt>TreeMap</tt> vyžaduje splnenie zvyčajných vlastností úplného usporiadania a konzistencia s metódou <tt>equals</tt> (detaily možno nájsť v dokumentácii). | ||
+ | |||
+ | ''Príklad'': | ||
+ | <syntaxhighlight lang="java"> | ||
+ | import java.util.*; | ||
+ | |||
+ | public class DualComparator implements Comparator<Integer> { | ||
+ | |||
+ | @Override | ||
+ | public int compare(Integer o1, Integer o2) { | ||
+ | return o2.compareTo(o1); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | import java.util.*; | ||
+ | |||
+ | public class Trieda { | ||
+ | |||
+ | public static void main(String[] args) { | ||
+ | Set<Integer> set = new TreeSet<>(new DualComparator()); | ||
+ | set.addAll(List.of(0, 1, 2, 3)); | ||
+ | for (Integer x : set) { | ||
+ | System.out.print(x + " "); // Vypise 3 2 1 0 | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ''Poznámka'': uvedený príklad treba chápať ako čisto ilustračný, keďže rovnako sa správajúci komparátor možno získať aj volaním statickej generickej metódy <tt>Comparator.<Integer>reverseOrder()</tt> resp. skrátene <tt>Comparator.reverseOrder()</tt>. | ||
=== Trieda <tt>Collections</tt> === | === Trieda <tt>Collections</tt> === | ||
− | < | + | Trieda [https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Collections.html <tt>Collections</tt>] obsahuje viacero užitočných statických metód na prácu s Java Collections (ide o akúsi obdobu triedy <tt>Arrays</tt> pre polia). Sú v nej definované napríklad: |
+ | * Metódy <tt>sort</tt> na utriedenie zoznamu (v dvoch verziách: s použitím prirodzeného usporiadania v prípade, že typ <tt>E</tt> prvkov zoznamu implementuje <tt>Comparable<E></tt>, ako aj s použitím komparátora). | ||
+ | :<syntaxhighlight lang="java">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); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | * Metódy <tt>shuffle</tt> na náhodné premiešanie zoznamu. | ||
+ | * Metóda <tt>reverse</tt> 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ť [https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html tu]. | ||
+ | |||
=== Statické vnorené triedy === | === 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 <tt>StatickaVnorenaTrieda</tt> definovanej v triede <tt>VonkajsiaTrieda</tt> ale mimo triedy <tt>VonkajsiaTrieda</tt> pristupujeme cez <tt>VonkajsiaTrieda.StatickaVnorenaTrieda</tt>. Užitočnou novinkou je možnosť nastaviť vnoreným triedam prístupový modifikátor <tt>private</tt>, čím sa trieda stane viditeľnou iba z vonkajšej triedy, v ktorej je definovaná. |
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | public class VonkajsiaTrieda { | ||
+ | |||
+ | private static class StatickaVnorenaTrieda1 { | ||
+ | // ... | ||
+ | } | ||
+ | |||
+ | public static class StatickaVnorenaTrieda2 { | ||
+ | // ... | ||
+ | } | ||
+ | |||
+ | public void metoda() { | ||
+ | StatickaVnorenaTrieda1 o1 = new StatickaVnorenaTrieda1(); | ||
+ | StatickaVnorenaTrieda2 o2 = new StatickaVnorenaTrieda2(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | import java.util.*; | ||
+ | |||
+ | public class Trieda { | ||
+ | |||
+ | public static void main(String[] args) { | ||
+ | VonkajsiaTrieda.StatickaVnorenaTrieda2 x = new VonkajsiaTrieda.StatickaVnorenaTrieda2(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 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'': | ||
+ | <syntaxhighlight lang="java"> | ||
+ | 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 premennu 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(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <syntaxhighlight lang="java"> | ||
+ | public class Trieda { | ||
+ | |||
+ | public static void main(String[] args) { | ||
+ | VonkajsiaTrieda o = new VonkajsiaTrieda(); | ||
+ | o.metoda(); | ||
+ | VonkajsiaTrieda.VnutornaTrieda2 o2 = o.new VnutornaTrieda2(); | ||
+ | o2.vypis(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
=== Lokálne triedy === | === 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 <tt>class</tt> žiaden modifikátor prístupu. | ||
+ | * Inštancie týchto tried sú použiteľné aj mimo bloku, v ktorom je lokálna trieda definovaná (tam ich možno považovať napríklad za inštancie nejakej nelokálnej nadtriedy alebo implementovaného rozhrania). | ||
+ | * 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 <tt>NullSkippingArrayListIterator<E></tt>, ktorú sme vracali ako iterátor v triede <tt>MyArrayList<E></tt>, môžeme napísať ako lokálnu triedu v metóde <tt>iterator</tt> triedy <tt>MyArrayList<E></tt>. | ||
+ | <syntaxhighlight lang="java"> | ||
+ | import java.util.*; | ||
+ | |||
+ | public class MyArrayList<E> extends ArrayList<E> { | ||
+ | |||
+ | @Override | ||
+ | public Iterator<E> iterator() { | ||
+ | class NullSkippingArrayListIterator implements Iterator<E> { | ||
+ | private int nextIndex; | ||
+ | |||
+ | public NullSkippingArrayListIterator() { | ||
+ | skipNullValues(); | ||
+ | } | ||
+ | |||
+ | private void skipNullValues() { | ||
+ | while (nextIndex <= size() - 1 && get(nextIndex) == null) { // while (nextIndex <= MyArrayList.this.size() - 1 && MyArrayList.this.get(nextIndex) == null) | ||
+ | nextIndex++; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public boolean hasNext() { | ||
+ | return nextIndex <= size() - 1; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public E next() { | ||
+ | E result = null; | ||
+ | if (hasNext()) { | ||
+ | result = get(nextIndex++); | ||
+ | } else { | ||
+ | throw new NoSuchElementException(); | ||
+ | } | ||
+ | skipNullValues(); | ||
+ | return result; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | return new NullSkippingArrayListIterator(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ''Príklad'': Komparátor <tt>DualComparator</tt> ako lokálna trieda. | ||
+ | <syntaxhighlight lang="java"> | ||
+ | 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); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
=== Anonymné triedy === | === 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 <tt>new</tt> 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 <tt>}</tt> definície triedy sa uvedie bodkočiarka, ktorou sa ukončí príkaz na vytvorenie nového objektu (prípadne príkaz obsahujúci <tt>new</tt> 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ípadne možno použiť tzv. [https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html inicializačné bloky]. | ||
+ | |||
+ | ''Príklad'': Iterátor pre <tt>MyArrayList</tt> ako anonymná trieda. | ||
+ | <syntaxhighlight lang="java"> | ||
+ | import java.util.*; | ||
+ | |||
+ | public class MyArrayList<E> extends ArrayList<E> { | ||
+ | |||
+ | @Override | ||
+ | public Iterator<E> iterator() { | ||
+ | return new Iterator<E>() { | ||
+ | private int nextIndex = 0; | ||
+ | |||
+ | @Override | ||
+ | public boolean hasNext() { | ||
+ | while (nextIndex <= size() - 1 && get(nextIndex) == null) { // while (nextIndex <= MyArrayList.this.size() - 1 && MyArrayList.this.get(nextIndex) == null) | ||
+ | nextIndex++; | ||
+ | } | ||
+ | return nextIndex <= size() - 1; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public E next() { | ||
+ | E result = null; | ||
+ | if (hasNext()) { | ||
+ | result = get(nextIndex++); | ||
+ | } else { | ||
+ | throw new NoSuchElementException(); | ||
+ | } | ||
+ | return result; | ||
+ | } | ||
+ | }; | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ''Príklad'': Komparátor ako anonymná trieda. | ||
+ | <syntaxhighlight lang="java"> | ||
+ | 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); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | 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. |
Aktuálna revízia z 16:23, 17. marec 2024
Obsah
- 1 Oznamy
- 2 Java Collections
- 2.1 Rozhranie Collection
- 2.2 Použitie metódy equals
- 2.3 Zoznamy
- 2.4 Rad a objostranný rad
- 2.5 Množiny a použitie metódy hashCode
- 2.6 Iterátory
- 2.7 Cyklus for each a iterátory
- 2.8 Definovanie vlastných iterátorov
- 2.9 Zobrazenia
- 2.10 Usporiadané množiny a zobrazenia na nich
- 2.11 Komparátory
- 2.12 Trieda Collections
- 3 Vnorené, lokálne a anonymné triedy
Oznamy
- Minulý utorok bol v zadaní prvej domácej úlohy opravený chybný príklad vstupu č. 1. Ďakujem Filipovi Sivičekovi za upozornenie.
- Riešenia domácej úlohy, ktoré v triede MarkInitialCellProgram nechávajú robota zapisovať na políčka toru referenciu null, nie sú správne – aj keď testovač možno takýmto spôsobom oklamať.
- Zadanie hovorí, že robot zapisuje booleovské hodnoty; null nie je booleovská hodnota. (Po správnosti by trieda Move mala v takom prípade vyhadzovať výnimku.)
- Ak null považujete za booleovskú hodnotu, fungovalo by vaše riešenie aj v prípade, že by na začiatku simulácie boli na niektorých políčkach takéto hodnoty zapísané?
- Budúci týždeň počas cvičení – čiže v utorok 26. marca od 9:50 do 11:20 – bude prebiehať druhý test zameraný na látku z prvých piatich týždňov. Body z testu bude možné získať iba v prípade prítomnosti na cvičeniach v miestnosti I-H6.
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.
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).
- Pokiaľ nedôjde k ich modifikácii, mali by byť pre danú dvojicu objektov výstupy metódy equals vždy rovnaké.
- 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.
- Niektoré z týchto dátových štruktúr (napr. množiny) sa nemusia správať rozumne v prípade, že dôjde k internej modifikácii niektorého z ich prvkov, ktorá vyústi v odlišné výstupy metódy equals pre tento prvok. Pri práci so zoskupeniami modifikovateľných objektov teda treba postupovať s veľkou opatrnosťou.
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
}
}
Metóda equals nemusí byť nutne implementovaná tak, aby sa rovnali iba inštancie toho istého typu – často napríklad môže existovať viacero tried, ktorých inštancie intuitívne reprezentujú tú istú entitu (napríklad inštancie ArrayList<E> aj inštancie LinkedList<E> reprezentujú zoznamy prvkov typu E) a metóda equals sa v takom prípade môže implementovať ako rovnosť týchto entít bez ohľadu na typ reprezentujúcej inštancie. To je okrem iného aj dôvodom, prečo sú argumentmi metód contains a remove deklarovaných v rozhraní Collection<E> inštancie triedy Object a nie inštancie typu E.
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. Konštruktor bez parametrov vytvorí prázdny zoznam, rovnako ako konštruktor s celočíselným parametrom initialCapacity umožňujúci nastaviť počiatočný 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) + " "); } System.out.println(a); } }
- 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. Tieto pracujú na základe výstupov metódy equals pre prvky množiny a správajú sa rozumným spôsobom za predpokladu, že nedochádza k modifikácii vnútorného stavu jednotlivých prvkov. 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.
- Pokiaľ nedôjde k modifikácii objektu, mala by metóda hashCode vracať vždy rovnaký výstup.
- Ď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.
Trieda HashSet<E> sa na uvedené vlastnosti metódy hashCode prvkov množiny spolieha a v prípade ich porušenia sa nemusí správať korektne.
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.
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á.
Príklad: Pre jeden zoznam môžeme mať aj viacero nezávislých iterátorov ukazujúcich na rôzne pozície v zozname:
for (Iterator<Integer> it1 = list.iterator(); it1.hasNext(); ) {
int x = it1.next();
for (Iterator<Integer> it2 = list.iterator(); it2.hasNext(); ) {
System.out.print(x + it2.next() + " ");
}
System.out.println();
}
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 triedy ArrayList líši iba implementáciou metódy iterator. Tá vráti iterátor novodefinovaného typu NullSkippingArrayListIterator, ktorý pri prechádzaní prvkov zoznamu vynecháva všetky výskyty referencie null. Takéto prekrytie metódy iterator v podtriede triedy ArrayList 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<K, V> 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 minimálne pri použití v TreeSet a TreeMap vyžaduje splnenie zvyčajných vlastností úplného usporiadania a konzistencia s metódou equals (detaily možno nájsť v dokumentácii).
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.addAll(List.of(0, 1, 2, 3));
for (Integer x : set) {
System.out.print(x + " "); // Vypise 3 2 1 0
}
}
}
Poznámka: uvedený príklad treba chápať ako čisto ilustračný, keďže rovnako sa správajúci komparátor možno získať aj volaním statickej generickej metódy Comparator.<Integer>reverseOrder() resp. skrátene Comparator.reverseOrder().
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óda 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 premennu 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á (tam ich možno považovať napríklad za inštancie nejakej nelokálnej nadtriedy alebo implementovaného rozhrania).
- 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 implements Iterator<E> {
private int nextIndex;
public NullSkippingArrayListIterator() {
skipNullValues();
}
private void skipNullValues() {
while (nextIndex <= size() - 1 && get(nextIndex) == null) { // while (nextIndex <= MyArrayList.this.size() - 1 && MyArrayList.this.get(nextIndex) == null)
nextIndex++;
}
}
@Override
public boolean hasNext() {
return nextIndex <= size() - 1;
}
@Override
public E next() {
E result = null;
if (hasNext()) {
result = 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ípadne možno použiť tzv. inicializačné bloky.
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<E>() {
private int nextIndex = 0;
@Override
public boolean hasNext() {
while (nextIndex <= size() - 1 && get(nextIndex) == null) { // while (nextIndex <= MyArrayList.this.size() - 1 && MyArrayList.this.get(nextIndex) == null)
nextIndex++;
}
return nextIndex <= size() - 1;
}
@Override
public E next() {
E result = null;
if (hasNext()) {
result = get(nextIndex++);
} else {
throw new NoSuchElementException();
}
return result;
}
};
}
}
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.