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 implementovanej kolekcie. 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. Ide o relatívne zložitú hierarchiu rozhrania a tried, ktoré sú dostupné všetkým programátorom. Základná vizualizácia tohto frameworku je vidieť na UML diagrame nižšie:
Základné rozhranie, ktoré zaisťuje každú kolekciu v Jave, je rozhranie
Collection
. Toto rozhranie popisuje 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 kolekcii,isEmpty()
– vrátitrue
, pokiaľ sa v kolekcii nenachádza žiadny prvok, inakfalse
,contains()
– vrátitrue
, pokiaľ kolekcia obsahuje prvok z parametra,add()
– 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 odobraný), inakfalse
,clear()
– vymaže obsah kolekcie.
Rozhranie Collection
rozširuje rozhranie Iterable
.
Toto rozhranie definuje metódy na prehliadanie nielen kolekcií, ale všetkých
objektov, nad ktorými je možné iterovať. Rozhranie obsahuje metódu
iterator()
, ktorú musia implementovať všetky kolekcie. Tá
vracia tzv. iterátor, o ktorom si nižšie povieme viac. Ď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 prehliadanie kolekcií. Iterátor
sme vlastne už použili bez toho, aby sme o tom vedeli, a to pri kolekcii
ArrayList
.
Priechod cez indexy
Keď sme prechádzali pole, ktoré nie je plnohodnotnou kolekciou, mali sme
na výber dve konštrukcie: cez indexy pomocou cyklu for
:
String[] names = new String[] {"Kyle", "Peter", "Michael", "John"}; for (int i = 0; i < names.length; i++) { System.out.println(names[i]); }
A pomocou foreach:
String[] names = new String[] {"Kyle", "Peter", "Michael", "John"}; for (String name: names) { System.out.println(name); }
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šia syntax pre programátora, ktorá sa ešte pred kompiláciou automaticky nahradí iným, typicky zložitejším kódom.
Priechod kolekcií iterátorom
Na prechádzanie skutočných kolekcií, teda zložitejších štruktúr, ako
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> lastName = new ArrayList<>(); for (Iterator<String> iterator = lastName.iterator(); iterator.hasNext(); ) { String next = iterator.next(); System.out.println(next); iterator.remove(); // If the collection supports it, the current element is deleted }
Znalosť iterátorov sa nám v praxi oplatí v prípade, keď budeme chcieť počas prehliadania z kolekcie mazať. Vtedy ich musíme na prechádzanie explicitne použiť, viď nižšie. Ďalšie využitie iterátora 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é rovnomennou metódou. Toto rozhranie obsahuje dve dôležité metódy:
next()
a hasNext()
. Metódy si opäť popíšme:
next()
- vráti nasledujúci prvokhasNext()
- vrátitrue
, ak existuje nasledujúci prvok
Pomocou týchto dvoch metód je Java následne schopná kolekciu od začiatku do konca prejsť.
Od Javy verzie 8 sú na rozhraní dostupné tiež metódy:
remove()
- odstráni prvok z kolekcie, pokiaľ túto operáciu kolekcie podporuje, inak sa vyvolá výnimkaUnsupportedOperationException
; toto je jediný správny spôsob, ako sa dá odstrániť prvok z kolekcie, keď ňou 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
prechod nejakou kolekciou. Predstavme si, že sme si vytvorili vlastnú kolekciu
SimpleList
obaľujúcu obyčajné pole, ktoré sa jej pridelí v
konštruktore. Triede nebudeme pridávať žiadne metódy, iba jej
implementujeme rozhranie 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 konštruktore pole, 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 triedy
Iterator
. Iterátor je možné použiť iba na prechádzanie
kolekcie od začiatku do konca. Ak chceme iterovať odzadu, treba najskôr
vytvoriť kolekciu, ktorá bude prevrátená a až nad ňou vytvoriť nový
iterátor. V metóde hasNext()
zisťujeme, či môže iterátor
vrátiť ďalší prvok, alebo už prišiel nakoniec. Metódou
next()
vrátime aktuálny 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 rozhraní 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ácie rozhrania môžu
zdieľať implementáciu základných metód (size()
,
isEmpty()
), ale budú mať rozdielne metódy, ako sú
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 sme boli úplne presní, tak všetky vyššie vymenované abstraktné
triedy okrem AbstractMap
ešte dedia od spoločnej abstraktnej
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čenie 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ú rovnomenné
triedy s podporou modifikácie z viacerých vlákien. Napríklad pre
ArrayList
tu existuje thread-safe verzia v podobe
CopyOnWriteArrayList
.
V ďalších lekciách postupne preberieme najdôležitejšie rozhrania
List
, Set
, Queue
a Map
a ich
implementácie, konkrétne ArrayList
, LinkedList
,
HashSet
a HashMap
.
V budúcej lekcii, Zoznam (list) pomocou poľa v Jave, sa bližšie pozrieme na kolekciu zoznamy (listy), predstavíme si rôzne implementácie tejto kolekcie a ich výhody a nevýhody.