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 implementovanej. 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 kolekciiisEmpty()
- vrátitrue
, pokiaľ sa v kolekcii nenachádza žiadny prvok, inakfalse
contains()
- vrátitrue
, pokiaľ 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 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, 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 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 poľa, 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é:
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ď ďalej. Ď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 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, 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. Uvažujme, že sme si vytvorili vlastnú kolekciu
SimpleList
, ktorá len obaľuje obyčajné pole, ktoré ju príde v
konštruktore. Triede nebudeme pridávať žiadne metódy, iba ju 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 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 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ž doš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 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ácie 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 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 rozhranie
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
List
, predstavíme si rôzne implementácie tejto kolekcie a ich
výhody a nevýhody..