2. diel - Java Collections Framework
V minulej lekcii, Úvod do kolekcií a genericita v Jave , sme si urobili úvod do kolekcií a ukázali sme si, čo je to genericita. V dnešnej lekcii si povieme, ako má jazyk Java kolekcie implementované. Predstavíme si základné časť z Java Collections Frameworku.
Java Collections Framework
Každý dobrý programovací jazyk ponúka v štandardnej knižnici prácu s kolekciami. V jazyku Java túto časť rieši celý framework nazvaný Java Collections Framework. Jedná sa o relatívne zložitú hierarchiu rozhranie a tried, ktoré sú dostupné všetkým programátorom. Základné vizualizácie tohto frameworku je vidieť na UML diagrame nižšie:
Základné rozhranie, ktoré zastrešuje každú kolekciu v Jave, je
rozhranie Collection
. Toto rozhranie opisuje základné metódy pre
prácu s každou kolekciou. Výber najdôležitejších metód je k dispozícii
na obrázku nižšie:
Teraz si tieto metódy popíšeme:
size()
- vráti aktuálny počet prvkov v kolekciiisEmpty()
- vrátitrue
, ak sa v kolekcii nenachádza žiadny prvok, inakfalse
contains()
- vrátitrue
, ak kolekcia obsahuje prvok z parametraadd()
- pridá prvok do kolekcie; vrátitrue
, ak sa zmenila kolekcia (prvok bol pridaný), inakfalse
remove()
- odoberie prvok z kolekcie; vrátitrue
, ak sa zmenila kolekcia (prvok existoval a bol odobratý), inakfalse
clear()
- vymaže obsah kolekcia
Rozhranie Collection
rozširuje rozhranie Iterable
.
Toto rozhranie definuje metódy pre prechádzanie nielen kolekcií, ale
všetkých objektov, nad ktorým možno iterovat. Rozhranie obsahuje metódu
iterator()
, ktorú musí implementovať všetky kolekcie. Tá
vracia tzv. Iterátor, ktorý si hneď vysvetlíme. Ďalej rozhranie obsahuje
dve default
metódy s implementáciou: forEach()
a
spliterator()
, ktorým sa budeme venovať v ďalších
lekciách.
Iterátor
Iterátory sú objekty, ktoré slúžia na prechádzanie kolekcií. Iterátor
sme vlastne už použili, bez toho aby sme o tom vedeli, a to u kolekcie
ArrayList
.
Priechod cez indexy
Keď sme prechádzali pole, ktoré nie je plnohodnotnou kolekcií, mali sme
na výber dve konštrukcie: cez indexy pomocou cyklu for
:
String[] jmena = new String[] {"Karel", "Pepa", "Michal", "Vojta"}; for (int i = 0; i < jmena.length; i++) { System.out.println(jmena[i]); }
A pomocou foreach:
String[] jmena = new String[] {"Karel", "Pepa", "Michal", "Vojta"}; for (String jmeno: jmena) { System.out.println(jmeno); }
Keď použijeme foreach nad jednoduchým poľom, Java interne rovnako použije prístup cez indexy. Foreach je len tzv. Syntax sugar, krajší syntaxe pre programátora, ktorá sa ešte pred kompiláciou automaticky nahradí iným, typicky zložitejším kódom.
Priechod kolekcií iterátory
Pre prechádzanie skutočných kolekcií, teda zložitejších štruktúr
než je pole, napr. ArrayList
, môžeme tento syntaktický cukor
využiť úplne rovnako. Len Java interne použije tzv. Iterátor a náš kód
sa vnútorne preloží na niečo takéto:
List<String> prijmeni = new ArrayList<>(); for (Iterator<String> iterator = prijmeni.iterator(); iterator.hasNext(); ) { String next = iterator.next(); System.out.println(next); iterator.remove(); // Pokud to kolekce podporuje, tak se odstraní aktuální prvek }
Znalosť iterátory sa nám v praxi oplatí v prípade, keď budeme chcieť počas prehliadania z kolekcie mazať. Vtedy ich musíme pre prechádzanie explicitne použiť, viď ďalej. Ďalšie využitie Iterator je pre naše vlastné kolekcie, na ktoré následne pôjde používať foreach cyklus.
Rozhranie Iterator
Na chvíľu sa zastavíme pri rozhraní Iterator
, ktoré je
vrátené rovnomennú metódou. Toto rozhranie obsahuje dve dôležité metódy:
next()
a hasNext()
. Metódy si opäť popíšme:
next()
- vráti nasledujúce prvokhasNext()
- vrátitrue
, ak existuje nasledujúci prvok
Pomocou týchto 2 metód je Java následne schopná kolekciu od začiatku do konca prejsť.
Od Javy verzie 8 sú na rozhraní tiež metódy:
remove()
- odstráni prvok z kolekcie, ak túto operáciu kolekcie podporuje, inak sa vyvolá výnimkaUnsupportedOperationException
; toto je jediný správny spôsob, ako možno odstrániť prvok z kolekcie, keď jej prechádzameforEachRemaining()
- prejde každý prvok kolekcie a aplikuje naň príslušnú akciu
Vlastné iterátor
Ukážme si ako implementovať vlastné iterátor, teda objekt umožňujúci
priechod nejakú kolekcií. Uvažujme, že sme si vytvorili vlastnú kolekciu
SimpleList
, ktorá len obaľuje obyčajné pole, ktoré ju príde v
konstruktoru. Triede nebudeme pridávať žiadne metódy, len ju implementujeme
rozhrania Iterable
a metódu iterator()
, ktorá vráti
anonymné implementáciu rozhrania iterátor:
public class SimpleList<Type> implements Iterable <Type> { private Type[] arrayList; private int currentSize; public SimpleList(Type[] newArray) { this.arrayList = newArray; this.currentSize = arrayList.length; } @Override public Iterator <Type> iterator() { Iterator <Type> it = new Iterator<Type> () { private int currentIndex = 0; @Override public boolean hasNext() { return currentIndex < currentSize && arrayList[currentIndex] != null; } @Override public Type next() { return arrayList[currentIndex++]; } }; return it; } }
Trieda SimpleList
prijme v konstruktoru poľa, nad ktorým sa
bude vytvárať iterátor. Je dôležité, aby volanie metódy
iterator()
vždy vrátilo novú inštanciu Iterator
u.
Iterátory môžu použiť len na prechádzanie kolekcia od začiatku do konca.
Ak chceme iterovat odzadu, treba najprv vytvoriť kolekciu, ktorá bude
prevrátená a až nad ňou vytvoriť nový iterátor. V metóde
hasNext()
zisťujeme, či ak môže iterátor vrátiť ďalší
prvok, alebo už došiel nakoniec. Metódou next()
vrátime
aktuálne prvok a zvýšime index poľa.
Všimnite si, že sme rozhranie Iterator
implementovali ako anonymný
triedu. Samozrejme by sme si aj mohli deklarovať plnohodnotnú triedu,
napr. SimpleIterator
, a v metóde iterator()
vracať
jej inštanciu.
Potomkovia Collection
Rozhranie Collection
je rozšírené o metódy podľa spôsobu
použitia pomocou rozhrania List
, Set
a
Queue
. Úplne samostatne leží rozhranie Map
, ktoré
obsahuje metódy pre prácu s kolekciami typu "kľúč - hodnota". Základné
metódy týchto rozhraní sú implementované v abstraktných triedach podľa
typu rozhrania: AbstractList
, AbstractSet
,
AbstractQueue
a AbstractMap
. Abstraktné triedy sú tu
použité, pretože niektoré konkrétne implementácia rozhrania môžu
zdieľať implementáciu základných metód (size()
,
isEmpty()
), ale budú mať rozdielne metódy ako je
add()
, remove()
. Ďalej sú tieto abstraktné triedy
užitočné v prípade, že si budete chcieť implementovať vlastnú kolekciu,
ale chcete mať už nejaký základ implementovaný.
Aby som bol úplne presný, tak všetky vyššie vymenované abstraktné
triedy okrem AbstractMap
ešte dedí od spoločnej abstraktné
triedy AbstractCollection
. Všetky triedy možno nájsť v
balíčku java.util
. Tieto triedy majú jednu spoločnú
vlastnosť: nie sú thread-safe. To znamená, že nemajú zabezpečenia pre
modifikáciu prvkov z viacerých vlákien. Tento problém je v Jave riešený
pomocou tried, ktoré sa nachádzajú v balíčku
java.util.concurrent
. Tu sa okrem iného nachádzajú rovnomennej
triedy s podporou modifikácie z viacerých vlákien. Napríklad pre
ArrayList
tú existuje thread-safe verzia v podobe
CopyOnWriteArrayList
.
V ďalších lekciách postupne preberieme najdôležitejšie rozhranie
List
, Set
, Queue
a Map
a ich
implementácie, konkrétne ArrayList
, LinkedList
,
HashSet
a HashMap
. Nabudúce nás čaká Zoznam (List) pomocou poľa v Jave .