Listy

Transkrypt

Listy
Aplikacje w Javie – wykład 7
Kolekcje (listy, zbiory)
Treści prezentowane w wykładzie zostały oparte o:
● Barteczko, JAVA Programowanie praktyczne od podstaw, PWN, 2014
● http://docs.oracle.com/javase/8/docs/
● C. S. Horstmann, G. Cornell, Java. Podstawy, Helion, Gliwice 2008
1
KOLEKCJE
●
Kolekcja jest obiektem, który grupuje elementy danych (inne obiekty) i
pozwala traktować je jak jeden zestaw danych, umożliwiając jednocześnie
wykonywanie operacji na zestawie danych np. dodawania i usuwania oraz
przeglądania elementów zestawu.
●
Uwaga: W niektórych językach programowania kolekcje są nazywane
kontenerami. W Javie kontenery są specjalnymi „kolekcjami”, które grupują
komponenty graficznego interfejsu użytkownika (GUI).
●
Naturalną realizacją koncepcji kolekcji są tablice.
●
W języku Java tablice nie są jednak wygodnym sposobem tworzenia kolekcji
ponieważ:
●
–
nie posiadają dedykowanych metod do obsługi kolekcji,
–
rozmiar tablicy jest stały.
Dlatego w Javie, w pakiecie java.util, zdefiniowano narzędzia, służące
do tworzenia i posługiwania się różnymi rodzajami kolekcji.
2
Architektura kolekcji (Collections Framework)
●
●
●
●
Abstrakcyjne właściwości struktur danych opisywane są przez interfejsy
kolekcyjne, a konkretne realizacje - inaczej implementacje kolekcji - tych
właściwości znajdujemy w konkretnych klasach.
Zunifikowana architektura, służąca do reprezentacji kolekcji i operowania na
nich, składa się z:
– interfejsów,
– implementacji,
– algorytmów.
Architektura kolekcji w Javie to: Java Collections Framework (JCF). Mamy
tam do dyspozycji wiele gotowych, efektywnych klas i metod, pozwalających
łatwo rozwiązywać wiele problemów związanych z reprezentacją w
programie bardziej zaawansowanych struktur danych i operowaniem na nich.
Klasy JCF dostarczają środków do posługiwania się następującymi rodzajami
kolekcji:
– listy – i ich szczególne przypadki:
●
stosy,
●
kolejki,
●
kolejki podwójne.
– zbiory i zbiory uporządkowane,
– mapy – tablice asocjacyjne (słowniki).
3
JCF- hierarchia interfejsów
4
Podstawowe interfejsy JCF
●
Collection - dowolna kolekcja nie będącą mapą
– List - zestaw elementów, z których każdy znajduje się na określonej pozycji;
do listy można wielokrotnie dodać ten sam element i można sięgnąć po
element na dowolnej pozycji.
Queue - kolejka, czyli sekwencja elementów, do której dodawanie i sięganie po
elementy odbywa się na pozycjach, określonych przez zadany porządek
(najczęsciej FIFO - first in first out). Nie ma bezpośredniego dostępu do
dowolnej pozycji.
●
Deque - rozszerza Queue: kolejka podwójna, do której dodawanie i sięganie
może odbywać się na obu końcach (np. dodaj na początku, dodaj na końcu,
usuń z początku, usuń z końca).
– Set - zestaw niepowtarzających się elementów, pozycje elementów są
nieokreślone.
●
SortedSet - rozszerza Set: zbiór uporządkowany
– NavigableSet - rozszerza SortedSet: zbiór uporządkowany, dla
którego możliwe są operacje uzyskiwania elementów "bliskich" danemu.
Map – mapa (tablica asocjacyjna, słownik) - zestaw par: klucz-wartość, przy czym
odwzorowanie kluczy w wartości jest jednoznaczne
– SortedMap - mapa z uporządkowanymi kluczami (typu SortedSet)
– NavigableMap - mapa, w której klucze są typu NavigableSet
–
●
5
Wybrane podstawowe konkretne implementacje
Implementowany Implementujące
interfejs
klasy
Sposób realizacji właściwości określanych przez
interfejs
List
ArrayList
Dynamicznie rozszerzalna tablica (szybki bezpośredni
dostęp po indeksach)
List, Queue,
Deque
LinkedList
Lista liniowa z podwójnymi dowiązaniami (szybkie
wpisywanie i usuwanie elementów poza końcem listy; wolny
dostęp bezpośredni; implementacja operacji na kolejkach)
Queue, Deque
ArrayDeque
Kolejka podwójna (zrealizowana jako rozszerzalna tablica;
szybki dostęp do obu końców, brak dostępu do dowolnej
pozycji)
Queue
PriorityQueue
Kolejka z priorytetami (kolejka, w której pierwszy i ostatni
elment jest określany na podstawie ustalonego porządku
(porównania elmentów wg kryteriów) )
Set
HashSet
Tablica haszująca (mieszania) (szybkie wpisywanie (add) i
odnajdywanie elementów(contains); kolejność elementów
nieokreślona)
Set,
SortedSet,
NavigableSet
Set
TreeSet
Drzewo czerwono-czarne, szybkie wstawianie,
(uporządkowanie elementów; dostęp i wyszukiwanie
wolniejsze niż w implementacji mieszającej)
LinkedHashSet
Tablica mieszania i lista liniowa (jak w implememntacji
mieszającej, z zachowaniem porządku wpisywania
elementów)
6
Ogólne operacje na kolekcjach- interfejs Collection
Interfejs Collection definiuje wspólne właściwości i funkcjonalność wszystkich
kolekcji (tzn. list, zbiorów i innych) poza mapami.
Typ Collection jest zatem swoistym "najmniejszym wspólnym mianownikiem"
wszystkich rodzajów kolekcji i używamy go (szczególnie przy przekazywaniu
argumentów) wtedy, gdy potrzebna jest największa ogólność działań.
Podstawowe operacje na kolekcjach:
●
int size() - zwraca liczbę elementów zawartych w kolekcji
●
boolean isEmpty() - sprawdza, czy kolekcja jest pusta
●
●
●
boolean contains(Object o) - sprawdza, czy kolekcja zawiera podany
obiekt.
boolean add(Object o) - dodaje obiekt o do kolekcji. (Uwaga: operacja
opcjonalna – nie jest dozwolona dla kolekcji niemodyfikowalnych)
boolean remove(Object o) - usuwa obiekt o z kolekcji. (Uwaga: operacja
opcjonalna – nie jest dozwolona dla kolekcji niemodyfikowalnych)
Kolekcja jest modyfikowalna, jeśli można dodawać do niej elementy, usuwać z
niej elementy oraz zmieniać wartości elementów. Niemodyfikowalne kolekcje nie
pozwalają na dodawanie, usuwanie i zmianę wartości elementów.
7
Ogólne operacje na kolekcjach
●
●
●
Operacje opcjonalne. Konkretne klasy kolekcyjne mogą dopuszczać lub nie
wykonanie takich operacji. Np. klasa definiująca jakąś kolekcję niemodyfikowalną
nie może pozwolić na wykonanie operacji dodawania i usuwania elementów. Ale
ponieważ każda klasa kolekcyjna musi implementować interfejs Collection, to
musi też zdefiniować metody add i remove. Przyjęto zasadę, że jeśli operacja
opcjonalna dla danej implementacji nie jest dopuszczalna, to odpowiednia metoda
zglasza wyjątek UnsupportedOperationException:
public boolean add(T o) {
throw new UnsupportedOperationException();
}
public boolean remove(T o) {
throw new UnsupportedOperationException();
}
Uwaga: ponieważ kolekcje są sparametryzowane, T oznacza parametr typu.
Przy dodawaniu elementów do kolekcji mogą powstać i inne wyjątki, zależne od
implementacji kolekcji. Implementacje, które nie dopuszczają dodawania
elementów o pewnych właściwościach (np. elementów null) będą zgłaszać
wyjątek IllegalArgumentException. Te zaś, które np. czasowo nie
dopuszczają dodania elementu (np. w tym momecie jest przekroczony
maksymalny limit wielkości kolekcji) zgłaszają wyjątek
IllegalStateException.
8
Operacje grupowe na kolekcjach
Operacje grupowe na kolekcjach polegają na wykonywaniu „za jednym razem”
pewnych operacji na całych kolekcjach. Należą do nich metody:
boolean addAll(Collection<? extends E> c) - dodanie do
dowolnej kolekcji wszystkich elementów kolekcji przekazanej przez
parametr c
●
boolean removeAll(Collection<?> c) - usunięcie z kolekcji
wszystkich elementów, które są zawarte w kolekcji przekazanej przez
parametr c
●
boolean retainAll(Collection<?> c)- pozostawienie w kolekcji
tylko tych elementów, które są zawarte w kolekcji przekazanej przez
parametr c
●
void clear()- usunięcie wszystkich elementów kolekcji.
●
Object [] toArray()- zwraca tablicę obiektów zawartych w kolekcji.
Ponieważ niektóre z tych operacji mogą modyfikować kolekcje (a o tym czy
naprawdę nastąpiła modyfikacja - świadczą wyniki zwracane przez metody true albo false), to - oczywiście - są one operacjami opcjonalnymi.
●
UWAGA! Operacje, które wymagają porównywania elementów np.
contains(jakis_obiekt), containsAll(Collection},
removeAll(..), retainAll(...) używają do tego metody equals()
zdefiniowanej w klasach obiektów.
9
Przekształcanie kolekcji w inne kolekcje
●
Niejako kontynuacją operacji grupowych jest możliwość przekształcenia
kolekcji danego rodzaju w dowolną inną kolekcję dowolnego innego rodzaju.
Np. listy w zbiór uporządkowany. We wszystkich konkretnych
implementacjach kolekcji dostarczono konstruktorów, mających jako
parametr dowolną inną kolekcję (czyli parametr typu Collection).
●
Jeśli zatem mamy listę, a chcemy z niej zrobić zbiór uporządkowany (w
konkretnej implementacji - np. drzewa zrównoważonego), to wystarczy użyć
konstruktora odpowiedniej klasy (tu: TreeSet):
List lista;
//utworzenie listy w konkretnej implementacji
//np.ArrayList lub LinkedList
Set tset = new TreeSet(lista);
●
W ten sposób uzyskamy zbiór (a więc bez powtórzeń elementów),
uporządkowany (w naturalnym porządku elementów), którego elementy
będą pobrane z dostarczonej listy.
●
Oczywiście, jeśli nie stosujemy typów surowych (jak wyżej), ale kolekcje
sparametryzowane, kompilator zabroni nam dokonywania pewnych
przekształceń (np. uzyskania listy napisów List<String> ze zbioru liczb
Set<Integer>).
10
Iteratory
●
●
●
●
Bardzo ważną metodą interfejsu Collection (tak naprawdę dziedziczoną z
interfejsu Iterable) jest metoda iterator(), która zwraca iterator.
Iterator jest obiektem klasy implementującej interfejs Iterator i służy do
przeglądania elementów kolekcji oraz ew. usuwania ich przy przeglądaniu
Metody interfejsu Iterator:
–
T next() - zwraca kolejny element kolekcji lub sygnalizuje wyjątek
NoSuchElementException, jeśli osiągnięto koniec kolekcji. T jest typem
elementów kolekcji.
–
void remove() - usuwa element kolekcji, zwrócony przez ostatnie
odwołanie do next(). Operacja opcjonalna.
–
boolean hasNext()- zwraca true, jeśli możliwe jest odwołanie do
next() zwracające kolejny element kolekcji.
Klasy iteratorów są definiowane w klasach kolekcyjnych jako klasy wewnętrzne,
implementujące interfejs Iterator<T>. Implementacja metody iterator() z
interfejsu Collection zwraca obiekt takiej klasy. Dzięki temu od każdej kolekcji
możemy uzyskać iterator za pomocą odwołania:
Iterator<T> iter = c.iterator();
gdzie: c - dowolna klasa implementująca interfejs Collection, T - typ elmentów
kolekcji.
11
Iteratory
●
●
●
●
●
●
Dla tych kolekcji, w których elementy nie zajmują ściśle określonych pozycji
iteratory są jedynym sposobem na "przebieganie" po kolekcji.
Dla kolekcji listowych iteratory są efektywniejszym narzędziem iterowania od
pętli iteracyjnych pobierających elementy z pozycji wyznaczanych przez podane
indeksy.
O iteratorze należy myśleć jako o wskaźniku ustawianym nie na elemencie
kolekcji, ale pomiędzy elementami. Na początku iterator ustawiony jest przed
pierwszym elementem. Odwołanie next() jednocześnie przesuwa iterator za
element i zwraca ten element.
Metoda iteratora remove() najczęściej stosowana jest do usuwania z kolekcji
elementów, które spełniają (nie spełniają) jakichś warunków. Szablon użycia
remove() w trakcie iteracji:
Iterator<T> iter = c.iterator(); // c - dowolna kolekcja
//typu Collection, T - typ elementów kolekcji
while (iter.hasNext()) {
T element = iter.next();
if (warunek_usunięcia(element)) iter.remove();
}
Metoda remove() może usunąć tylko element zwrócony przez next() i wobec
tego może być zastosowana tylko raz dla każdego next().
W trakcie iteracji za pomocą iteratora nie wolno modyfikować kolekcji innymi
sposobami niż użycie metody remove() na rzecz iteratora!
12
Listy (interfejs List)
LISTA - zestaw elementów, z których każdy znajduje się na określonej pozycji
w zestawie. Różne elementy listy mogą zawierać takie same dane.
JDK posiada kilka implementacji interfejsu List, różniących się sposobem
przechowywania elementów.
●
W klasie ArrayList stosuje się tablicę, która jest dynamicznie zwiększana
w momencie przekroczenia jej maksymalnego rozmiaru. Elementy listy są
zapisywane jako elementy takiej tablicy. Ponieważ tablice w Javie mają
określone (niezmienne po utworzeniu) rozmiary utworzenie listy tablicowej
wymaga alokacji tablicy z jakimś zadanym rozmiarem. Jest on specyfikowany
przez initialCapacity (domyślnie 10), który to parametr możemy podać
w konstruktorze ArrayList. Przy dodawaniu elementów do listy sprawdzane
jest czy pojemność tablicy jest wystarczająca, jeśli nie to rozmiar tablicy jest
zwiększany. Służy temu metoda ensureCapacity(minCapacity), którą
zresztą możemy wywołać sami, aby w trakcie działania programu zapewnić
podaną jako minCapacity pojemność listy.
13
Listy
●
Klasa LinkedList jest klasyczną listą łączoną, w której każdy element
posiada referencję do poprzedniego i następnego elementu listy.
Zatem elementy listy, które z punktu widzenia programisty są elementami
umieszczanych danych (np. nazwisk lub jakichś innych obiektów),
technicznie są "linkami", zawierającymi nie tylko dane, ale również
wskaźniki na następny i poprzedni element na liście. Początek listy
dowiązaniowej, zwany głową lub wartownikiem zawiera wskazanie na
pierwszy element listy (null, jeśli lista jest pusta).
●
Klasa Vector jest przepisaną na nowo wersją znaną z JDK 1.0.
Jest obecna w bibliotece Java Collections tylko ze względu na potrzebę
wstecznej zgodności.
14
Listy – porównanie klas
Te dwie implementacje charakteryzują się różną wydajnością, ale można stosować je
zamiennie:
●
LinkedList powinniśmy wybierać wtedy, gdy na liście będą wykonywane częste
operacje wstawiania i/lub usuwania elementów w środku listy (poza końcem listy).
Istotnie na liście typu LinkedList takie operacje polegają na zmianie dwóch
dowiązań (prowadzącego do poprzedniego i do następnego elementu) - są więc
bardzo szybkie, zaś w implementacji tablicowej (ArrayList) wiążą się z
przepisywaniem elementów tablicy, co (zwykle) zabiera więcej czasu.
●
Operacje bezpośredniego dostępu do elementów listy są w implementacji
tablicowej ArrayList natychmiastowe (polegają na indeksowaniu tablicy),
natomiast w implementacji LinkedList są bardzo nieefektywne, gdyż technicznie
wymagają przebiegania po elementach listy od samego jej początku lub od końca w
kierunku początku (ten ostatni przypadek jest jedyną optymalizacją dostępu w klasie
LinkedList, dokonywaną wtedy, gdy indeks znajduje się "w drugiej połowie" listy).
●
Na listach LinkedList należy unikać operacji get(int index) i set(int
index, Object value)
●
Wyszukiwanie elementów na listach (czy to klasy ArrayList czy LinkedList) za
pomocą ogólnych metod interfejsu Collection (contains(Object)) oraz
interfejsu List indexOf(Object)) nie jest efektywne. Powinniśmy albo
zastosować inny rodzaj kolekcji (np. zbiory w implementacji tablic mieszania), albo
posortować listę (metoda sort) i zastosować wyszukiwanie binarne (metoda
binarySearch) (są to metody klasy Collections).
15
Operacje na listach
●
●
●
●
●
●
●
●
●
●
●
wszystkie metody interfejsu Collection
boolean add(int p, T elt) - dodaje element typu T na pozycji p. Zwraca
true jeśli lista została zmodyfikowana.
boolean addAll(int p, Collection c)- dodaje wszystkie elementy
kolekcji c do listy poczynając od pozycji p. Zwraca true jeśli lista została
zmodyfikwoana.
T get(int p)- zwraca element na pozycji p (T jest typem elementu)
int indexOf(T elt)- zwraca pozycję (indeks) pierwszego wystąpienia
elementu elt
int lastIndexOf(T elt)- zwraca indeks ostatniego wystąpienia elementu elt
ListIterator<T> listIterator()- zwraca iterator listowy, ustawiony na
początku listy (przed pierwszym elementem).
ListIterator<T> listIterator(int p)- zwraca iterator listowy ustawiony
przed elementem o indeksie p.
boolean remove(int p)- usuwa element na pozycji p. Zwraca true jeśli lista
została zmodyfikowana.
T set(int p, T val)- zastępuje element na pozycji p podanym elementem
elt. Zwraca poprzednio znajdujący się na liście element.
List<T> subList(int f, int l) - zwraca podlistę zawierającą elementy listy
od pozycji f włącznie do pozycji l (wyłącznie).
16
ListIterator
●
Operacje na liście rozszerzają możliwości operowania na kolekcjach o
operacje pozycyjne - takie, które uzwględniają pozycję elementów.
●
Ze względu na znajomość pozycji elementów w kolekcji możliwe staje się
iterowanie po kolekcji w obie strony: od początku i od końca. Można też
ustawić iterator w taki sposób, by iteracje rozpoczynały się od podanej
pozycji, a znając pozycję elementu zwracanego przez iterator można nie
tylko go usunąć, ale zamienić lub dodać nowy element na pozycji
wyznaczanej przez stan iteratora.
●
Dlatego właśnie oprócz zwykłego (ogólnego dla wszystkich kolekcji)
iteratora, listy udostępniają iteratory listowe, które są obiektami klas
implementujących interfejs ListIterator. Ten ostatni jest rozszerzeniem
interfejsu Iterator
●
Metody iteratora listowego :
boolean hasNext(), boolean hasPrevious(),
Object next(), Object previous(),
int nextIndex(), int previousIndex(),
void add(T o), void remove(), void set(T o)
17
ArrayList - przykład
Przykład: Rozważmy program, który tworzy listę firm, dodaje do niej dowolną liczbę
elementów (nazw firm zapisanych w kolejnych wierszach pliku), po czym
wyprowadza zawartość listy na konsolę. W programie możemy przedstawić ją jako
tablicę, ale nie wiadomo jaki ma mieć rozmiar, dlatego skorzystamy z listy.
import java.util.*;
import java.io.*;
class Intro1 {
public static void main(String args[]) throws IOException {
Scanner scan = new Scanner(new File("firms.txt"));
// Utworzenie obiektu klasy ArrayList
ArrayList list = new ArrayList();
while (scan.hasNextLine()) {
String firm = scan.nextLine();
// dodanie kolejnego elementu do listy
list.add(firm);
}
// wyprowadzenie zawartości listy
for (int i = 0; i < list.size(); i++)
System.out.println(list.get(i));
}
}
18
Listy – Iterator
●
Lepszym sposobem przeglądania elementów listy jest skorzystanie z iteratora.
●
W naszym przykładzie listy firm użycie iteratora może wyglądać następująco:
for (Iterator iter = list.iterator();iter.hasNext(); )
System.out.println(iter.next());
●
●
Najlepiej jednak jest używać rozszerzonego for (for-each):
for (Typ id : kol) instr
co oznacza, że w każdym kroku iteracji z kolekcji kol pobierany jest (za pomocą
jej iteratora) następny element i podstawiany pod zmienną id, która może być
następnie użyta w instrukcji instr.
Typ natomiast zależy od tego czy używamy kolekcji sparametryzowanych typami
czy też kolekcji surowych.
●
Surowe kolekcje moga zawierać referencje do dowolnych obiektów (ich
elementy są formalnie typu Object). Metoda next() iteratorów takich kolekcji
ma typ wyniku Object.
●
W przedstawionym przykładzie listy mamy właśnie do czynienia z taką surową
kolekcją. Zarówno iterator, jak i metoda get() zwracają wyniki typu Object.
19
Listy - bez parametryzacji
●
Powinniśmy więc napisać:
for (Object elt : list) System.out.println(elt);
Zauważmy, że przekazanie metodzie println argumentu typu Object
powoduje wyprowadzenie napisu zwróconego przez metodę toString() z
klasy argumentu. Nie mieliśmy więc kłopotu z faktycznym typem (którym był
String).
●
Jednak gdyby w powyższym przykładzie chcieć wywołać na rzecz zmiennej
elt np. metodę length() z klasy String, to kompilator zgłosiłby błąd
(statyczna ścisła kontrola typów: istotnie w klasie Object - a takiego typu
jest elt - nie ma metody length()!).
Musielibyśmy więc dokonywać referencyjnej konwersji zawężającej :
// Wypisuje długości napisów z kolekcji list
for (Object elt : list)
System.out.println( ((String) elt).length());
20
Listy sparametryzowane
●
Użycie kolekcji sparametryzowanych polega na podaniu typu jej elementów
w nawiasach kątowych np.
ArrayList<String> list = new ArrayList<>();
Do tak zdefiniowanej kolekcji, korzystając ze zmiennej list, nie będzie można
dodać elementu innego typu niż String, a także wszelkie metody zwracające
elementy tej kolekcji (m.in. get() oraz next() iteratora) będą miały typ
wyniku String.
W tym przypadku typ zmiennej w rozszerzonym for może być String i
wobec tego możemy pisać tak:
ArrayList<String> list = new ArrayList<>();
// ....
// Wypisuje długości napisów z kolekcji list
for (String elt : list)
System.out.println(elt.length());
21
Listy sparametryzowane
Uwaga.
Z powodu słabości typów generycznych, dla sparametryzowanej kolekcji:
List<String> list = new ArrayList<>();
dokonując przypisania
List hackedList = list;
możemy do powyższej listy obiektów typu String dodawać obiekty również
innych typów, np.:
hackedList.add(10); //Integer
hackedList.add(new Date()); //Date
gdyż metoda add na hackedList oczekuje parametru typu Object, a nie
String (wynika to z wewnętrznej reprezentacji RAW typów generycznych).
Wówczas użycie obiektu z takiej kolekcji zgodnie z jej typem, np.
String s = list.get(0);
skompiluje się, ale podczas uruchomienia wyrzuci wyjątek
ClassCastException informując, że próbuje się rzutować Integer na
String.
22
Listy sparametryzowane
Aby zabezpieczyć kolekcję przed dopisywaniem "nieuprawnionych" obiektów
możemy wykorzystać specjalnie do tego celu stworzone klasy opakowujące,
których instancje otrzymujemy poprzez uruchomienie wybranych metod klasy
Collections. Np. dla listy:
List<String> list = Collections.checkedList(
new ArrayList<>(), String.class);
Metoda checkedList bierze dwa parametry – opakowywaną listę oraz klasę
elementów tej listy, a zwraca nowy obiekt – listę opakowującą (view), która
sprawdza każdy dopisywany do niej obiekt (czyli właściwie do wyjściowej listy)
pod kątem jego typu.
W klasie Collections są zdefiniowane również analogiczne metody
dotyczące innych typów kolekcji.
23
ListIterator przykład
import java.util.*;
class ListIter1 {
static <T> void state(ListIterator<T> it) {
int pi = it.previousIndex(),ni = it.nextIndex();
System.out.println("Iterator jest pomiędzy indeksami: "
+ pi + " " + ni);
}
public static void main(String args[]) {
List<String> list = new LinkedList<String>(Arrays.asList(
new String[] { "E0","E1", "E2", "E3" }));
ListIterator<String> it = list.listIterator();
it.next();
state(it);//Iterator jest pomiędzy indeksami: 0 1
it.add("nowy1");
System.out.println(list);//[E0, nowy1, E1, E2, E3]
it.next();
it.next();
it.previous();
state(it);//Iterator jest pomiędzy indeksami: 2 3
it.add("nowy2");
System.out.println(list);//[E0, nowy1, E1, nowy2, E2, E3]
it.previous();
it.previous();
state(it);//Iterator jest pomiędzy indeksami: 1 2
it.remove();
System.out.println(list); //[E0, nowy1, nowy2, E2, E3]
}
}
24
Zbiory (interfejs Set)
●
Zbiór jest kolekcją, reprezentującą zestaw niepowtarzających się
elementów.
●
W zbiorze elementy nie mają pozycji. Nie jest możliwy dostęp do elementów
zbioru „po indeksach”. O elemencie zbioru można zatem powiedzieć
jedynie, czy należy do zbioru (w jednym egzemplarzu), czy nie.
●
Zatem zbiór posiada, odmienną niż lista, semantykę: nie zachowuje
kolejności elementów, natomiast wyklucza istnienie duplikatów.
Interfejs Set nie definiuje żadnych nowych metod w porównaniu do
interfejsu Collection. Jedyne uszczegółowienie polega na tym, że w
przypadku zbiorów, metody dodające elementy do zbiorów zwracają wartość
false, jeśli dodawane elementy już w zbiorze występują.
●
Podobnie, jak w przypadku listy, zbiór posiada w JDK kilka gotowych
implementacji. Jedną z nich jest HashSet, w którym unikatowość
elementów jest zapewniona przez zastosowanie tablicy haszującej
(mieszającej); natomiast w przypadku klasy TreeSet poprzez wyszukiwanie
binarne z użyciem drzewa czerwono-czarnego (drzewo dwukolorowe).
25
Zbiory - HashSet
●
Przykład. Zauważmy, że jeśli w pliku firm dwa razy powtórzono tę samą
nazwę, to co zrobić, jeśli chcemy mieć wynikowy zestaw firm bez powtórzeń
nazw? Oczywiście, można własnoręcznie oprogramować sprawdzanie
elementów zestawu i usuwać z niego duplikaty. Ale po co, jeśli istnieje
prostszy sposób - zastosowanie kolekcji typu zbiór.
Możemy np. użyć konkretnej klasy realizującej koncepcję zbioru
nieuporządkowanego - klasy HashSet.
●
Zwróćmy uwagę, że:
–
w zbiorze elementy nie mają pozycji. Przy przeglądaniu możemy zatem
zastosować wyłącznie iterator (dostęp "po indeksach" nie jest możliwy).
–
porządek iterowania (przeglądania) zbioru nie jest określony (kolejność
wyprowadzonych wyników może być inna niż kolejność firm w pliku).
26
Zbiory - HashSet - przykład
class Intro2 {
public static void main(String args[]) throws IOException {
Scanner scan = new Scanner(new File("firms.txt"));
// Utworzenie obiektu klasy HashSet
// z parametrem typu <String>
HashSet<String> set = new HashSet<String>();
while (scan.hasNextLine()) {
String firm = scan.nextLine();
// dodanie kolejnego elementu do zbioru
set.add(firm);
}
// wyprowadzenie zawartości zbioru
for (String elt : set) System.out.println(elt);
}
}
27
Zbiory - TreeSet
Przykład. Co zrobić, jeśli od naszego programu wymagane jest wyprowadzenie
uporządkowanego zestawu firm np. w alfabetycznym porządku? Możemy zastosować
kolekcję stanowiącą zbiór uporządkowany. W zbiorze uporządkowanym kolejność
przeglądania jego elementów za pomocą iteratora jest określona (np. w rosnącym
porządku alfabetycznym nazw firm, będących elementami zbioru). Konkretną
realizacją zbioru uporządkowanego jest w Javie klasa TreeSet.
import java.util.*;
import java.io.*;
class Intro3 {
public static void main(String args[]) throws IOException {
Scanner scan = new Scanner(new File("firms.txt"));
TreeSet<String> set = new TreeSet<>();
while (scan.hasNextLine()){
set.add(scan.nextLine());
}
for (String elt : set) System.out.println(elt);
}
}
28
Zbiory - HashSet
●
Tablica mieszająca (haszująca - ang. hashtable) jest strukturą danych specjalnie
przystosowaną do szybkiego odnajdywania elementów. Dla każdego elementu
danych wyliczany jest kod numeryczny (liczba całkowita) nazywany kodem
mieszania (hashcode), na podstawie którego obliczany jest indeks w tablicy, pod
którym będzie umieszczony dany element. Może się zdarzyć, że kilka elementów
otrzyma ten sam indeks, zatem elementy tablicy mieszającej stanowią listy, na
których znajdują się elementy danych o takim samym indeksie, wyliczonym na
podstawie ich kodów mieszania.
●
Każda lista - element tablicy mieszania nazywa się kubełkiem (bucket).
●
Aby umieścić nowy element w tablicy mieszania, wyliczany jest jego hashcode, po
czym na podstawie jego wartości obliczany jest indeks w tablicy mieszania. Indeks
taki stanowi resztę z dzielenia wartości kodu mieszania przez liczbę kubełków.
Element umieszczany jest w kubełku pod tym indeksem.
●
Istotne jest w tej procedurze, by:
–
wyliczanie kodu mieszania było szybkie,
–
kody mieszania dla rożnych elementów zależały tylko od ich wartości i były
(możliwie) różne dla różnych wartości elementów,
–
wynikowe indeksy dawały możliwie równomierną dystrybucję elementów
danych po elementach tablicy mieszania.
29
Zbiory - hashtable
●
W Javie można wyliczyć kod mieszania dla każdego obiektu za pomocą
zastosowania metody hashCode(). Metoda ta, zdefiniowana w klasie Object,
daje - ogólnie - jako wynik adresy obiektów. To, oczywiście, nie jest zbyt
użyteczne, bowiem nie bierze pod uwagę zawartości obiektów (wszystkie obiekty
mają rożne kody mieszania). Ale w standardowych klasach Javy metoda
hashCode() została przedefiniowana, tak, by zwracała kod na podstawie "treści"
obiektu, np. napisu stanowiącego "zawartość" obiektu klasy String. Dwa takie
same napisy będą miały te same kody mieszania.
●
Wyszukanie elementu w tablicy mieszania jest bardzo efektywne. Wystarczy
obliczyć indeks tablicy na podstawie kodu mieszania szukanego elementu i jeżeli
w danym kubełku jest tylko jeden element, to - nawet nie wykonując żadnych
porównań - zwrócić ten element (lub wartość true - że jest). Jeżeli w kubełku jest
kilka elementów, to musimy wykonać ich porównanie z szukanym elementem
(equals(...)), ale i tak liczba porównań będzie zwykle bardzo niewielka w
stosunku do liniowego czy nawet binarnego wyszukiwania. Klasa HashSet
wykonuje to np. przy wywołaniu metod add(obiekt), remove(obiekt).
●
Oprócz wyliczania kodów mieszania do prawidłowego dodawania i odnajdywania
elementów potrzebne jest odpowiednie zdefiniowanie metody equals(),
porównującej "treść" dwóch obiektów.
30
Zbiory HashSet
●
●
●
●
●
●
Jeżeli prawdziwe jest a.equals(b), to musi być spełniony warunek
a.hashCode() == b.hashCode().
Zarówno hashCode() jak i equals() są dobrze zdefiniowane w
standardowych klasach Javy, natomiast tworząc własne klasy musimy sami
zadbać o właściwe ich zdefiniowanie.
Należy pamiętać, że przedefiniowujemy metodę equals z klasy Object. A tam
jej parametrem jest Object. Użycie innego typu parametru prowadzi do
przeciążenia (a nie przedefiniowania) metody i uniemożliwia polimorficzne do niej
odwołania.
Metodę hashCode definiujemy w klasie jako:
public int hashCode() {
//obliczenie kodu mieszania na podstawie wartości pól klasy
//dla pól obiektowych użyjemy metody hashCode() z ich klas
}
Środowiska uruchomieniowe IDE (m.in. NetBeans) dają możliwość automatycznej
generacji kodów metod hashCode() i equals() poprzez wybór opcji z menu.
Porządek elementów kolekcji HashSet nie jest określony. Klasa
LinkedHashSet, dziedzicząc klasę HashSet udostępnia wygodną często
właściwość: zachowania porządku, w jakim elementy dodane były do zbioru.
31
Zbiory TreeSet
●
●
●
●
Inna podstawowa implementacja koncepcji zbioru - klasa TreeSet, opiera
się na strukturze danych zwanej drzewem czerwono-czarnym. Dla potrzeb
korzystania z klasy TreeSet wystarczy wiedzieć, że zapewnia ona
szczególne uporządkowanie elementów zbioru, które pozwala szybko
odnajdywać w nim podany element.
Klasa TreeSet realizuje nie tylko koncepję zbioru "w ogóle", ale również
zbioru uporządkowanego, implementując interfejs SortedSet (ściślej
NavigableSet), który rozszerza interfejs Set.
Widzieliśmy już jak iterowanie po TreeSet zwraca elementy w porządku
rosnącym. Np. poniższy fragment:
String[] s = {"ala", "pies", "kot"};
Set set = new TreeSet();
for (int i=0; i < s.length; i++) set.add(s[i]);
System.out.println(set.toString());
wypisze: [ala, kot, pies]
Do zbiorów należy dodawać tylko referencje do obiektów
niemodyfikowalnych, w przeciwnym razie zbiór może stracić spójność po
ewentualnej zmianie zawartości obiektu (może zawierać identyczne obiekty)
32
Kolekcje – przykład – proste sortowanie
TreeSet zapewnia uporządkowanie elementów kolekcji, ale usuwa duplikaty. Co
zrobić, jeśli chcemy duplikaty zachować i posortować kolekcję? Zachowanie
duplikatów zapewnia lista (np. ArrayList). Możemy wobec niej zastosowac gotowy
algorytm sortowania zapisany w postaci statycznej metody klasy Collections
(klasa ta zawiera metody realizujące rózne algorytmy działania na kolekcjach).
import java.util.*;
import java.io.*;
class Intro4 {
public static void main(String args[]) throws IOException {
Scanner scan = new Scanner(new File("firms.txt"));
ArrayList<String> list = new ArrayList<>();
while (scan.hasNextLine()){
list.add(scan.nextLine());
}
Collections.sort(list);
for (String firm : list) {
System.out.println(firm);
}
}
}//wyprowadzi firmy w rosnącym alfabetycznym porządku ich nazw
33
Przetwarzanie list i zbiorów - przykłady
●
Zmiana wartości elementów listy. Do każdego elementu listy firm (zmienna list),
dodać przyrostek "-Polska".
ArrayList<String> list = new ArrayList<>(
Arrays.asList(new String[]{"Opel", "Fiat", "Audi"}));
for (int i=0; i<list.size(); i++)
list.set(i, list.get(i) + " - Polska");
System.out.println(list);
lub za pomocą iteratora listowego
for (ListIterator<String> it = list.listIterator(); it.hasNext(); )
it.set(it.next() + " - Polska");
System.out.println(list);
●
Usunięcie podanego obiektu z kolekcji. Usuniemy Opel z listy firm list.
System.out.println("Przed: " + list);
list.remove("Opel");
System.out.println("Po: " + list);
–
remove(ref) usuwa tylko pierwszy element, który został znaleziony
–
ten sposób działa dla dowolnych modyfikowalnych kolekcji
–
Można też usuwać element o podanym indeksie np. list.remove(3);
W przypadku błędnego indeksu - IndexOutOfBoundsException
34
Przetwarzanie list i zbiorów - przykłady
●
Usuwanie za pomocą metody remove() iteratora
System.out.println("Przed: " +
list);
for (Iterator<String> it = list.iterator(); it.hasNext();){
if(it.next().equals("Opel")) it.remove();
}
System.out.println("Po: " + list);
Ten sposób działa dla dowolnych modyfikowalnych kolekcji
●
Operacje grupowe (bulk-operations)
Metody addAll(), removeAll(), retainAll(), to operacje grupowe
(jednocześnie są wykonywane działania, które musielibyśmy programować za
pomocą iteracji i metod contains(), remove(), add()).
Zmienna list1 zawiera listę firm wczytanych z pliku, a list2 – listę firm z
innego pliku. Należy zmienić listę list1 tak, by zawierała dodatkowo, wszystkie
te firmy z drugiego pliku, których nie było w pierwszym pliku.
Przykładowe rozwiązanie:
35
Przetwarzanie list i zbiorów - przykłady
System.out.println("Na poczatku list1 : " +
list1);
System.out.println("Na poczatku list2 : " +
list2);
System.out.println("Usuwamy z list2, te które są na list1");
list2.removeAll(list1);
System.out.println("Teraz list2 : " +
list2);
System.out.println("Dodamy list2 do list1" );
list1.addAll(list2);
System.out.println("Na koncu list1 : " +
●
list1);
Gdyby chodziło tylko o podanie nazw firm bez powtórzeń:
System.out.println("Dane z dwóch plików: ");
System.out.println("list1 : " +
list1);
System.out.println("list2 : " +
list2);
HashSet<String> set = new HashSet<>(list1);
set.addAll(list2);
System.out.println("Wynikowy zbiór : " +
set);
36
Przetwarzanie list i zbiorów - przykłady
●
●
●
W klasie Collections dostępne są metody statyczne, ułatwiające wykonywanie
różnych operacji na kolekcjach. Oprócz wspomnianych wcześniej metod dostępne
są dodatkowo metody odwracania list Collections.reverse(lista),
wypełniania kolekcji i wiele innych.
W trakcie przeglądania kolekcji za pomocą iteratora nie wolno jej modyfikować
innymi środkami niż metody iteratora, stosowanego właśnie do iterowania. Zatem
// coll – dowolna kolekcja napisów
Iterator<String> it1=coll.iterator();
Iterator<String> it2=coll.iterator();
while(it1.hasNext()){
it1.next();
it2.next();
it2.remove(); // BŁĄD FAZY WYKONANIA – wyjątek
// ConcurrentModificationException
}
Dotyczy to również pętli for-each dla kolekcji (stosowany jest w niej "pod
spodem" iterator)
for (String e : coll)
coll.add(e+"1");//spowoduje ConcurrentModificationException
37