List<E> subList(int fromIndex, int toIndex)
Tomáš Pitner, Radek Ošlejšek, Marek Šabo, Jakub Čecháček pitner@muni.cz
Collection
Podívejme se na potomky rozhraní Collection
, konkrétně:
List
má implementace
ArrayList
- seznam na bázi pole, rychlý při přímém přístupu, nejběžnější
LinkedList
- spojovaný seznam, méně používaný
Set
má implementace
HashSet
- množina na bázi hašovací tabulky, nejčastěji používaná
List
něco jako dynamické pole
každý uložený prvek má svou pozici — číselný index
index je celočíselný, nezáporný, typu int
umožňuje procházení seznamu dopředně i zpětně - indexem či iterátorem
lze pracovat i s podseznamy, něco jako řezy (slices) v Pythonu:
List<E> subList(int fromIndex, int toIndex)
ArrayList
nejpoužívanější implementace seznamu
využívá pole pro uchování prvků
při zvětšování/zmenšování se vytváří nové pole a prvky se musejí přesouvat
rychlý přístup k prvkům dle indexu
pomalé 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)
LinkedList
druhá nejpoužívanější implementace seznamu
využívá 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
Kontejnery ukladají pouze jeden typ, v tomto případě String . |
List implementation: | ArrayList | LinkedList ------------------------------------------------------------------ add 100000 elements | 12 ms | 8 ms remove all elements from last to first | 5 ms | 9 ms add 100000 elements at 0th position | 1025 ms | 18 ms remove all elements from 0th position | 1014 ms | 10 ms add 100000 elements at random position | 483 ms | 34504 ms remove all elements from random position | 462 ms | 36867 ms
Nevhodné případy užití tedy jsou ArrayList pro přidávání/odebírání zkraje seznamu
a pro dlouhé seznamy `LinkedList`jakékoli přístupy uprostřed. |
new ArrayList<>()
vytvoří prázdný seznam (s kapacitou 10 prvků)
new ArrayList<>(int initialCapacity)
vytvoří prázdný seznam s danou kapacitou
new ArrayList<>(Collection<? extends E> c)
vytvoří seznam a naplní ho prvky kolekce c
Kapacita reprezentuje interní kapacitu, neznamená to počet null prvků v nové kolekci! |
of
Kromě new
a konstruktoru lze přes statické tovární metody, kolekce jsou ale pak neměnné:
List.of(elem1, elem2, …)
vytvoří seznam a naplní ho danými prvky
vrátí nemodifikovatelnou kolekci
Set.of
, Map.of
analogicky
new
Chceme-li kolekci modifikovatelnou, musíme vytvořit novou new
:
List<String> modifiableList = new ArrayList<>(List.of("Y", "N"));
Jelikož v množině nejsou duplicity, Set.of si hlídá,
abychom prvek nepřidali vícekrát:
proto selže například Set.of("foo", "bar", "baz", "foo"); |
List
kolekci Collection
?v podstatě nedělám nic - seznam už je kolekcí
// change the type, it is its superclass
Collection<Long> collection = list;
Collection
seznam List
?musíme vytvořit nový seznam a prvky tam zkopírovat
// create new list
List<Long> l = new ArrayList<>(collection);
List
IRozhraní List
dědí od Collection
.
Kromě metod v Collection
obsahuje další metody:
E get(int index)
vrátí prvek na daném indexu
IndexOutOfBoundsException
je-li mimo rozsah
E set(int index, E element)
nahradí prvek s indexem index
prvkem element
vrátí předešlý prvek
List
IIvoid add(int index, E element)
přidá prvek na daný index (prvky za ním posune)
E remove(int index)
odstraní prvek na daném indexu (prvky za ním posune)
vrátí odstraněný prvek
int indexOf(Object o)
vrátí index prvního výskytu o
jestli kolekce prvek neobsahuje, vrátí -1
int lastIndexOf(Object o)
totéž, ale vrátí index posledního výskytu
List<String> list = new ArrayList<>();
list.add("A");
list.add("C");
list.add(1, "B");
// ["A", "B", "C"]
list.get(2); // "C"
list.set(1, "D"); // "B"
list.indexOf("D"); // 1
Set
odpovídá matematické představě množiny
prvek lze do množiny vložit nejvýš jedenkrát
při porovnávání rozhoduje rovnost podle výsledku volání equals
umožňuje rychlé dotazování na přítomnost prvku
provádí rychle atomické operace - se složitostí O(1), nejhůře O(log(n)):
vkládání prvku — add
odebírání prvku — remove
dotaz na přítomnost prvku — contains
Množiny jsou primárně bez pořadí, bez uspořádání, existuje však i množina s uspořádáním. |
equals
zjistí, jestli jsou objekty obsahově stejné (porovnání atributů).
hashCode
vrací pro obsahově stejné objekty stejné číslo, haš.
jakési falešné ID — pro různé objekty může hashCode
vracet stejný haš.
HashSet
Ukladá objekty do hašovací tabulky podle haše
Ideálně konstantní operace (tj. sub-logaritmická složitost)
Když má více prvků stejný haš, existuje více způsobů řešení
Pro (ne úplně ideální) hashCode
x + y vypadá tabulka následovně:
haš | objekt 0 | [0,0] 1 | [1,0] [0,1] 2 | [1,1] [0,2] [2,0] 3 | [2,1] [1,2] [0,3] [3,0]
HashSet
pod lupouboolean contains(Object o)
vypočte haš tázaného prvku o
v tabulce najde objekt uložený pod stejným hašem
objekt porovná s o
pomocí equals
Množinové operace budou velmi, velmi pomalé.
Chtěná složitost O(1), O(log n) zdegeneruje na lineární O(n).
Porušíme kontrakt (předpis) metody hashCode
.
Množina HashSet
přestane fungovat, bude obsahovat duplicity nebo vložený prvek už nenajdeme!
LinkedHashSet
Další implementací množiny je LinkedHashSet
= HashSet + LinkedList.
Zachová pořadí prvků dle jejich vkládání, což jinak u HashSet
neplatí.
třída Stack
, struktura LIFO
třída Queue
, struktura FIFO
fronta může být také prioritní — PriorityQueue
třída Deque
(čteme "deck")
slučuje vlastnosti zásobníku a fronty
nabízí operace příslušné oběma typům - vkládání i odběr z obou stran
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
Kontejnery ukládájí pouze odkazy na objekty, neukládají primitivní typy.
Proto používame jejich objektové protějšky — Integer, Char, Boolean, Double
…
Java automaticky dělá zabalení, tzv. autoboxing — konverzi primitivního typu na objekt "wrapper".
Pro zpětnou konverzi se analogicky dělá tzv. unboxing, vybalení.
List<Integer> list = new ArrayList<>();
list.add(new Integer(1));
list.add(1); // autoboxing
int primitiveType = list.get(0); // unboxing
Základní typy:
jednoduché, intuitivní
nepoužitelné pro modifikace samotné kolekce
náročnější, ale flexibilnější
modifikace povolena, např. mazání prvku pod iterátoem
forEach
například list.forEach(System.out::println)
Je rozšířenou syntaxí cyklu for
.
Umožňuje procházení kolekcí i polí.
List<Integer> numbers = List.of(1, 1, 2, 3, 5);
for(Integer i: list) {
System.out.println(i);
}
For-each neumožňuje modifikace kolekce.
Jestli kolekci změníme, nemůžeme pokračovat v iterování — dojde k vyhození ConcurrentModificationException
.
Odstranění prvku a vyskočení z cyklu však funguje:
Set<String> set = new HashSet<>(Set.of("Donald Trump", "Barrack Obama"));
for(String s: set) {
if (s.equals("Donald Trump")) {
set.remove(s);
break;
}
}
Sekvenční procházení prvků kolekce v neurčeném pořadí nebo uspořádání (u uspořádaných kolekcí)
Každý iterátor musí implementovat velmi jednoduché rozhraní
Iterator<E>
while
Běžné použití pomocí while
:
Set<Integer> set = Set.of(1, 2, 3);
Iterator<Integer> iterator = set.iterator();
while(iterator.hasNext()) {
Integer element = iterator.next();
...
}
E next()
vrátí následující prvek
NoSuchElementException
jestli iterace nemá žádné zbývající prvky
boolean hasNext()
true
jestli iterace obsahuje nějaký prvek
void remove()
odstraní prvek z kolekce
maximálně jednou mezi jednotlivými voláními next()
Pro procházení iterátorem se dá použít i for
cyklus:
Set<String> set = new HashSet<>(Set.of("Donald Trump", "Barrack Obama", "Hillary Clinton"));
for (Iterator<String> iter = set.iterator(); iter.hasNext();) {
String element = iter.next();
if (!element.equals("Barrack Obama")) iter.remove();
}
Roli iterátoru plnil dříve výčet
(Enumeration ) — nepoužívat. |
Kromě kontejnerů obsahujících již koncové hodnoty, jsou často používané i kontejnery obsahující další kontejnery:
Například List<Set<Integer>>
bude obsahovat seznam množin celých čísel,
tedy třeba [{1, 4, -2}, {0}, {-1, 3}]
.
Nebo mapa, která studentům přiřazuje seznam známek Map<Student, List<Integer>>
.