Lekce 2 - Java Collections Framework
V minulé lekci, Úvod do kolekcí a genericita v Javě, jsme si udělali úvod do kolekcí a ukázali jsme si, co je to genericita.
V dnešní lekci si řekneme, jak má jazyk Java kolekce implementované. Představíme si základní část z Java Collections Frameworku.
Java Collections Framework
Každý dobrý programovací jazyk nabízí ve standardní knihovně práci s kolekcemi. V jazyce Java tuto část řeší celý framework nazvaný Java Collections Framework. Jedná se o relativně složitou hierarchii rozhraní a tříd, které jsou dostupné všem programátorům. Základní vizualizace tohoto frameworku je vidět na UML diagramu níže:
Základní rozhraní, které zaštiťuje každou kolekci v Javě, je
rozhraní Collection
. Toto rozhraní popisuje základní metody pro
práci s každou kolekcí. Výběr nejdůležitějších metod je k dispozici na
obrázku níže:
Nyní si tyto metody popíšeme:
size()
- vrátí aktuální počet prvků v kolekciisEmpty()
- vrátítrue
, pokud se v kolekci nenachází žádný prvek, jinakfalse
contains()
- vrátítrue
, pokud kolekce obsahuje prvek z parametruadd()
- přidá prvek do kolekce; vrátítrue
, pokud se změnila kolekce (prvek byl přidán), jinakfalse
remove()
- odebere prvek z kolekce; vrátítrue
, pokud se změnila kolekce (prvek existoval a byl odebrán), jinakfalse
clear()
- vymaže obsah kolekce
Rozhraní Collection
rozšiřuje rozhraní
Iterable
. Toto rozhraní definuje metody pro procházení nejen
kolekcí, ale všech objektů, nad kterými lze iterovat. Rozhraní obsahuje
metodu iterator()
, kterou musí implementovat všechny kolekce. Ta
vrací tzv. iterátor, který si hned vysvětlíme. Dále rozhraní obsahuje
dvě default
metody s implementací: forEach()
a
spliterator()
, kterým se budeme věnovat v dalších lekcích.
Iterátor
Iterátory jsou objekty, které slouží k procházení kolekcí. Iterátor
jsme vlastně již použili, aniž bychom o tom věděli, a to u kolekce
ArrayList
.
Průchod přes indexy
Když jsme procházeli pole, které není plnohodnotnou kolekcí, měli jsme
na výběr dvě konstrukce: přes indexy pomocí cyklu for
:
String[] jmena = new String[] {"Karel", "Pepa", "Michal", "Vojta"}; for (int i = 0; i < jmena.length; i++) { System.out.println(jmena[i]); }
A pomocí foreach:
String[] jmena = new String[] {"Karel", "Pepa", "Michal", "Vojta"}; for (String jmeno: jmena) { System.out.println(jmeno); }
Když použijeme foreach nad jednoduchým polem, Java interně stejně použije přístup přes indexy. Foreach je jen tzv. syntax sugar, hezčí syntaxe pro programátora, která se ještě před kompilací automaticky nahradí jiným, typicky složitějším kódem.
Průchod kolekcí iterátorem
Pro procházení skutečných kolekcí, tedy složitějších struktur než
je pole, např. ArrayList
, můžeme tento syntaktický cukr
využít úplně stejně. Jen Java interně použije tzv. iterátor a náš kód
se vnitřně přeloží na něco takového:
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 }
Znalost iterátorů se nám v praxi vyplatí v případě, když budeme chtít během procházení z kolekce mazat. Tehdy je musíme pro procházení explicitně použít, viz dále. Další využití iterátoru je pro naše vlastní kolekce, na které následně půjde používat foreach cyklus.
Rozhraní Iterator
Na chvíli se zastavíme u rozhraní Iterator
, které je
vráceno stejnojmennou metodou. Toto rozhraní obsahuje dvě důležité metody:
next()
a hasNext()
. Metody si opět popišme:
next()
- vrátí následující prvekhasNext()
- vrátítrue
, pokud existuje následující prvek
Pomocí těchto 2 metod je Java následně schopná kolekci od začátku do konce projet.
Od Javy verze 8 jsou na rozhraní také metody:
remove()
- odstraní prvek z kolekce, pokud tuto operaci kolekce podporuje, jinak se vyvolá výjimkaUnsupportedOperationException
; toto je jediný správný způsob, jak lze odstranit prvek z kolekce, když jí procházímeforEachRemaining()
- projde každý prvek kolekce a aplikuje na něj příslušnou akci
Vlastní iterátor
Ukažme si jak implementovat vlastní iterátor, tedy objekt umožňující
průchod nějakou kolekcí. Uvažujme, že jsme si vytvořili vlastní kolekci
SimpleList
, která jen obaluje obyčejné pole, které ji přijde v
konstruktoru. Třídě nebudeme přidávat žádné metody, pouze ji
implementujeme rozhraní Iterable
a metodu iterator()
,
která vrátí anonymní implementaci rozhraní 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; } }
Třída SimpleList
přijme v konstruktoru pole, nad kterým se
bude vytvářet iterátor. Je důležité, aby volání metody
iterator()
vždy vrátilo novou instanci Iterator
u.
Iterátor lze použít pouze k procházení kolekce od začátku do konce. Pokud
chceme iterovat odzadu, je třeba nejdříve vytvořit kolekci, která bude
převrácená a až nad ní vytvořit nový iterátor. V metodě
hasNext()
zjišťujeme, zda-li může iterátor vrátit další
prvek, nebo již došel nakonec. Metodou next()
vrátíme
aktuální prvek a zvýšíme index pole.
Všimněte si, že jsme rozhraní Iterator
implementovali jako anonymní
třídu. Samozřejmě bychom si i mohli deklarovat plnohodnotnou třídu,
např. SimpleIterator
, a v metodě iterator()
vracet
její instanci.
Potomci Collection
Rozhraní Collection
je rozšířeno o metody podle způsobu
použití pomocí rozhraní List
, Set
a
Queue
. Úplně samostatně leží rozhraní Map
,
které obsahuje metody pro práci s kolekcemi typu "klíč - hodnota".
Základní metody těchto rozhraní jsou implementovány v abstraktních
třídách podle typu rozhraní: AbstractList
,
AbstractSet
, AbstractQueue
a AbstractMap
.
Abstraktní třídy jsou zde použity, protože některé konkrétní
implementace rozhraní mohou sdílet implementaci základních metod
(size()
, isEmpty()
), ale budou mít rozdílné metody
jako je add()
, remove()
. Dále jsou tyto abstraktní
třídy užitečné v případě, že si budete chtít implementovat vlastní
kolekci, ale chcete mít již nějaký základ implementovaný.
Abych byl úplně přesný, tak všechny výše vyjmenované abstraktní
třídy kromě AbstractMap
ještě dědí od společné abstraktní
třídy AbstractCollection
. Všechny třídy lze najít v balíčku
java.util
. Tyto třídy mají jednu společnou vlastnost: nejsou
thread-safe. To znamená, že nemají zabezpečení pro modifikaci prvků z
více vláken. Tento problém je v Javě řešen pomocí tříd, které se
nacházejí v balíčku java.util.concurrent
. Zde se mimo jiné
nacházejí stejnojmenné třídy s podporou modifikace z více vláken.
Například pro ArrayList
tu existuje thread-safe verze v podobě
CopyOnWriteArrayList
.
V dalších lekcích postupně probereme nejdůležitější rozhraní
List
, Set
, Queue
a Map
a
jejich implementace, konkrétně ArrayList
,
LinkedList
, HashSet
a HashMap
.
Příště nás čeká Seznam (List) pomocí pole v Javě.