Kontejnery

Co jsou kontejnery?

  • dynamické datové struktury vhodné k ukládání proměnného počtu objektů (přesněji odkazů na objekty)

  • kontejnery jsou datové struktury v operační paměti, nejsou automaticky ukládané do trvalé paměti (na disk)

  • podle povahy mohou mít různé specifické vlastnosti

Hlavní typy kontejnerů

Seznam (List)

každý prvek v nich uložený má svou pozici (číselný index),

  • podobně jako u pole, ale prvků lze uložit potenciálně neomezený počet

  • počet se může za běhu dynamicky měnit (i snižovat)

Množiny (Set)

prvek lze do množiny vložit nejvýš jedenkrát

  • odpovídá matematické představě množiny,

  • při porovnávání rozhoduje rovnost podle výsledku volání equals

Asociativní pole (Map)

v kontejneru jsou dvojice (klíč, hodnota),

  • při znalosti klíče lze v kontejneru rychle najít hodnotu,

  • klíč v mapě přibližně odpovídá databázovému klíči.

K čemu slouží

  • Kontejnery slouží k ukládání odkazů na objekty, ne přímo hodnot primitivních typů.

  • Při ukládání hodnot jako jsou čísla, booleovské hodnoty, znaky apod. se de facto ukládají jejich objektové protějšky, tzn. např. Integer, Char, Boolean, Double apod.

  • V Javě byly dříve kontejnery koncipovány jako beztypové, to už ale od verze Java 1.5 (tedy Java 5) neplatí!

  • Od Javy 5 mají tzv. typové parametry vyznačené ve špičatých závorkách (např. List<Person>), jimiž určujeme, jaké položky se do kontejneru smějí dostat.

Proč vůbec

  • Kontejnery jsou dynamickými alternativami k poli a mají daleko širší použití.

  • Slouží k uchování proměnného počtu objektů.

  • Počet prvků se v průběhu existence kontejneru může měnit.

  • Oproti polím nabízejí časově efektivnější algoritmy přístupu k prvkům — množiny, mapy.

Co použít

  • Většinou se používají kontejnery hotové, vestavěné, tj. ty, jež jsou součastí Java Core API,

  • tyto vestavěné kontejnerové třídy jsou definovány v balíku java.util,

  • je možné vytvořit si vlastní implementace, obvykle ale zachovávající/implementující standardní rozhraní.

Základní kategorie

Kategorie jsou dány tím, které rozhraní příslušný kontejner implementuje. Základní jsou:

Seznam (List<E>)

lineární struktura, každý prvek má svůj číselný index (pozici)

Množina (Set<E>)

struktura bez duplicitních hodnot a obecně také bez uspořádání, umožňuje rychlé dotazování na přítomnost prvku

Asociativní pole, mapa (Map<K,V>)

struktura uchovávající dvojice (klíč→hodnota), rychlý přístup přes klíč

Kolekce

  • Kolekce jsou kontejnery implementující rozhraní Collection

  • Rozhraní kolekce popisuje velmi obecný kontejner, disponující operacemi: přidávání, rušení prvku, získání iterátoru, zjišťování prázdnosti atd.

  • Mezi kolekce patří mimo mapy všechny ostatní vestavěné kontejnery List, Set.

  • Prvky kolekce nemusí mít svou pozici danou indexem, viz např. Set.

Kontejnery — rozhraní, nepovinné metody

  • Funkcionalita vestavěných kontejnerů je obvykle předepsána výhradně rozhraním, jenž implementují.

  • Rozhraní vestavěných kontejnerů připouštějí, že některé metody jsou nepovinné, třídy jej nemusí implementovat.

  • Fakticky to v takovém případě vypadá tak, že metoda tam sice je, jinak by to typově nesouhlasilo, ale nelze ji použít, protože volání obvykle vyhodí výjimku.

  • V praxi se totiž někdy nehodí implementovat typicky zápisové operace, protože některé kontejnery chceme mít read-only.

Iterátory

  • Iterátory jsou prostředkem, jak sekvenčně procházet prvky kolekce buďto:

  • v neurčeném pořadí nebo

  • v uspořádání (u uspořádaných kolekcí)

  • Každý iterátor musí implementovat velmi jednoduché rozhraní Iterator<E> se třemi metodami:

boolean hasNext()
E next()
void remove()

"Hromadné" operace nad kolekcemi

  • Některé operace je vhodné provádět nikoli postupně po prvcích, ale jedním voláním vhodné metody:

    boolean c1.addAll(c2)

    přidá do c1 všechny prvky z c2

    boolean c1.containsAll(c2)

    vrátí true, právě když c1 obsahuje všechny prvky z c2, tzn. když c1 je její nadmnožinou

    boolean c1.removeAll(c2)

    odstraní z c1 všechny prvky, které jsou současně v c2.

    boolean c1.removeIf(Predicate filter)

    odstraní z kolekce všechny prvky, které splňují predikát (podmínku) filter [až od Java 8]

    boolean c1.retainAll(c2)

    zachová v c1 jen ty prvky, které jsou současně v c2.

Seznamy

  • Seznamy jsou lineární struktury,

  • implementují rozhraní List, což je rozšíření Collection.

  • Prvky lze adresovat celočíselným nezáporným indexem (typu int).

  • Poskytují možnost získat dopředný i zpětný iterátor.

  • Lze pracovat i s podseznamy.

Implementace seznamu: ArrayList

ArrayList
  • nejpoužívanější implementace seznamu

  • využívá vnitřně pole pro uchování prvků

  • rychlé operace přístupu k prvkům dle indexu

  • přičemž o něco pomalejší jsou operace přidávání a odebírání prvků blíže k začátku seznamu (pole, v němž je seznam, se musí realokovat)

Implementace seznamu: LinkedList

LinkedList
  • druhá nejpoužívanější implementace seznamu

  • využívá vnitřně zřetězený seznam pro uchování prvků

  • pomalejší operace přístupu k prvkům dle indexu "uvnitř" seznamu

  • rychlejší operace přidávání a odebírání prvků na začátku a na konci, resp. blízko nich

Výkonnostní porovnání seznamů

  • Výsledek spuštění dema CollectionsDemo.java

    List implemented as java.util.ArrayList test done:
       add 100000 elements took 12 ms
       remove all elements from last to first took 5 ms
       add at 0 of 100000 elements took 1025 ms
       remove all elements from 0 took 1014 ms
       add at random position of 100000 elements took 483 ms
       remove all elements at random position took 462 ms
    List implemented as java.util.LinkedList test done:
       add 100000 elements took 8 ms
       remove all elements from last to first took 9 ms
       add at 0 of 100000 elements took 18 ms
       remove all elements from 0 took 10 ms
       add at random position of 100000 elements took 34504 ms
       remove all elements at random position took 36867 ms

Příklad použití seznamu

    // declaring and creating list
List<String> ls = new ArrayList<>();
// using method add
ls.add("Ahoj");
ls.add("Cheers");
ls.add("Nazdar");
// using method get
System.out.println(ls.get(0));
// using "add" at specified index
ls.add(0, "Bye");
System.out.println(ls.get(0));
System.out.println("Whole list:");
// using index
for(int i = 0; i < ls.size(); i++) {
   System.out.println(i + ". " + ls.get(i));
}
System.out.println("Whole list without indices:");
// using for-each
for(String s: ls) {
   System.out.println(s);
}

Další lineární struktury

Z datových struktur máme v Javě ještě např.:

zásobník

třída Stack, struktura "LIFO" s operacemi

  • push — vložení na vrchol zásobníku

  • pop — odebrání z vrcholu zásobníku

  • peek — přečtení (neodebrání) z vrcholu zásobníku

fronta

třída Queue, struktura "FIFO" s operacemi

  • add — přidání prvku do fronty

  • remove — vybrání prvku z fronty

  • element — přečtení (neodebrání) prvku z fronty

  • fronta může případně být prioritní (PriorityQueue)

oboustranná fronta

třída Deque (čteme "deck")

  • slučuje vlastnosti zásobníku a fronty

  • nabízí operace příslušné oběma typům

Množiny

  • Množiny jsou struktury standardně bez uspořádání prvků (ale existují i uspořádané, viz dále),

  • implementují rozhraní Set (což je rozšíření Collection).

  • Cílem množin je mít možnost rychle (se složitostí O(log(n))) provádět atomické operace:

    • vkládání prvku (add)

    • odebírání prvku (remove)

    • dotaz na přítomnost prvku (contains)

Množiny — implementace

Standardní implementace množiny:

hašovací tabulka

třída HashSet,

  • potenciálně rychlejší (ideálně konstantní, tj. sub-logaritmická složitost),

  • ale neumožňuje uspořádání hodnot

vyhledávací strom

konkrétně černobílý strom (Red-Black Tree),

  • třída TreeSet,

  • uspořádané hodnoty,

  • s garantovanou logaritmickou složitostí

Uspořádané množiny

  • Výše uvedená vestavěná implementace TreeSet.

  • Implementují rozhraní SortedSet

  • Jednotlivé prvky lze tedy iterátorem procházet v přesně definovaném pořadí — uspořádání podle hodnot prvků.

  • černobílé stromy (Red-Black Trees)

  • Uspořádání je dáno buďto:

    • standardním chováním metody compareTo vkládaných objektů — pokud implementují rozhraní Comparable

    • nebo je možné uspořádání definovat pomocí tzv. komparátoru (objektu impl. rozhraní Comparator) poskytnutých při vytvoření množiny.

Výkonnostní srovnání množin

  • Výsledek spuštění dema CollectionsDemo.java

    Set implemented as java.util.HashSet test done:
       add 100000 elements took 27 ms
       remove all elements from 100000 to 0 took 14 ms
       add 100000 elements took 9 ms
       remove all elements from 0 took 18 ms
       add 100000 random elements took 30 ms
       remove 100000 random elements took 17 ms
    Set implemented as java.util.TreeSet test done:
       add 100000 elements took 67 ms
       remove all elements from 100000 to 0 took 50 ms
       add 100000 elements took 58 ms
       remove all elements from 0 took 41 ms
       add 100000 random elements took 84 ms
       remove 100000 random elements took 68 ms

Comparable

  • Jednoduché rozhraní Comparable slouží k definování uspořádání na třídě objektů.

  • Předepisuje jedinou metodu int compareTo(T t)

  • Implementuje-li třída toto rozhraní, znamená to, že její objekty jsou vzájemně uspořádané.

  • Lze tedy o nich říci, který je ve struktuře umístěn dříve a který později pomocí o1.compareTo(o2), která vrací celé číslo, kde rozhoduje jeho znaménko.

  • Toto rozhraní definuje tzv. přirozené uspořádání (natural ordering).

  • Využívá se zejména u uspořádaných kontejnerů (množin a asociativních polí).

  • Chování by mělo být konzistentní s equals, tzn. pro si rovné objekty by compareTo měla vrátit 0.

Comparator

  • Jednoduché rozhraní Comparator slouží k definování uspořádání na třídě objektů zvnějšku, tzn. uspořádání pomocí objektu jiné třídy.

  • Předepisuje jedinou metodu int compare(T o1, T o2)

  • Implementuje-li třída toto rozhraní, znamená to, že její objekty umějí definovat uspořádání na objektech typu T obdobně jako Comparable vracením celého čísla se znaménkem.

  • Toto rozhraní funguje jako alternativa tam, kde nevyhovuje přirozené uspořádání (Comparable).

  • Využívá se zejména u uspořádaných kontejnerů (množin a asociativních polí), do nichž se dá komparátor nastavit při konstrukci těchto kontejnerů.

Cyklus for-each

  • Je rozšířenou syntaxí cyklu for.

  • Umožňuje procházení všech prvků polí a dalších iterovatelných struktur, např. seznamů a množin.

Příklad:

Set<String> strings = ...
for(String s: strings) {
   System.out.println(s);
}

Mapy

  • Mapy (asociativní pole) fungují v podstatě na stejných principech a požadavcích jako Set:

  • Ukládají ovšem dvojice (klíč, hodnota) a umožnují rychlé vyhledání dvojice podle hodnoty klíče.

  • Základními metodami jsou:

    • dotazy na přítomnost klíče v mapě (containsKey),

    • výběr hodnoty odpovídající zadanému klíči (get),

    • možnost získat zvlášt množiny klíčů, hodnot nebo dvojic (klíč, hodnota).

    • možnost iterace po těchto dvojicích (forEach)

Mapy — implementace

Mapy mají podobné implementace jako množiny, tj.:

hašovací tabulky

třída HashMap

  • potenciálně rychlejší,

  • ale neuspořádané klíče

černobílé stromy

třída TreeMap

  • uspořádané klíče,

  • s garantovanou logaritmickou složitostí

Mapy — složitosti

  • Složitost základních operací (put, remove, containsKey):

    • Mapy implementované jako stromy mají nejvýš logaritmickou složitost základních operací.

    • U map implementovaných hašovací tabulkou složitost v praxi závisí na kvalitě hašovací funkce (metody hashCode) na ukládáných objektech,

    • teoreticky se blíží složitosti konstantní.

Uspořádané mapy

  • Implementují rozhraní SortedMap

  • Dvojice (klíč, hodnota) jsou v nich uspořádané podle hodnot klíče.

  • Existuje vestavěná implementace třídou TreeMap

  • Uspořádání lze ovlivnit naprosto stejným postupem jako u uspořádané množiny.

Získání nemodifikovatelných kontejnerů

  • Kontejnery jsou standardně modifikovatelné (read/write).

  • Nemodifikovatelné kontejnery se často používají při vracení hodnot z metod.

  • Dají se získat pomocí volání příslušné metody třídy Collections.

Souběžný přístup

  • Moderní kontejnery jsou nesynchronizované, nepřipouštějí souběžný přístup z více vláken.

  • Standardní, nesynchronizovaný, kontejner lze však zabalit synchronizovanou obálkou.

Pomocná třída Collections

  • Java Core API nabízí třídu Collections.

  • Je to tzv. utility class, nabízí jen statické metody a proměnné, nelze od ní vytvářet instance.

  • Nabízí škálu užitečných metod pro práci s kontejnery, jako např.:

    • binární vyhledávání v kontejneru

    • vracení nemodifikovatelných kopií

    • rychlé vracení prázdných kontejnerů

    • agregační funkce (maximální, mininmální prvek)

    • vyplňování seznamu

    • obracení (reverse) nebo prohazování (shuffle) pořadí

    • uspořádání

Kontejnery a výjimky

  • Při práci s kontejnery může vzniknout řada výjimek, např. IllegalStateException apod.

  • Většina má charakter výjimek běhových, tudíž není povinností je odchytávat, pokud věříme, že nevzniknou.

Starší typy kontejnerů

  • Existují tyto starší typy kontejnerů (za → uvádíme náhradu):

    • Hashtable → HashMap, HashSet (podle účelu)

    • Vector → List

    • Stack → List nebo lépe Queue či Deque

Enumeration

  • Roli iterátoru plnil dříve výčet (Enumeration) se dvěma metodami:

    • boolean hasMoreElements()

    • Object nextElement()

Srovnání implementací kontejnerů

Seznamy
  • na bázi pole (ArrayList) - rychlý přímý přístup (přes index)

  • na bázi lineárního zřetězeného seznamu (LinkedList) — rychlý sekvenční přístup (přes iterátor)

  • téměř vždy se používá ArrayList — stejně rychlý a paměťově efektivnější

Množiny a mapy
  • na bázi hašovacích tabulek (HashMap, HashSet) — rychlejší, ale neuspořádané (lze získat iterátor procházející klíče uspořádaně)

  • na bázi vyhledávacích stromů (TreeMap, TreeSet) - pomalejší, ale uspořádané

  • spojení výhod obou — LinkedHashSet, LinkedHashMap

Odkazy