390 likes | 541 Views
„Klasy kolekcji”. Techniki i języki programowania. by Szymon Natanek. Kolekcje obiektów. Tablice: Klasa Arrays Listy: Klasy: ArrayList, LinkedList, Vector, Stack Zbiory: Klasy: TreeSet, HashSet, LinkedHashSet Mapy:
E N D
„Klasy kolekcji” Techniki i języki programowania by Szymon Natanek
Kolekcje obiektów • Tablice: • Klasa Arrays • Listy: • Klasy: ArrayList, LinkedList, Vector, Stack • Zbiory: • Klasy: TreeSet, HashSet, LinkedHashSet • Mapy: • Klasy: HashMap, LinkedHashMap, WeakHashMap, TreeMap, IdentityHashMap, HashTable
Tablice • Tablica w Javie jest ciągiem obiektów (a raczej referencji do obiektów). Jest prostą sekwencją liniową, pozwalającą na szybki dostęp do elementów. • Tablice posiadają ograniczenie rozmiaru. Można stworzyć tablicę o określonym rozmiarze, ale nie można tego rozmiaru zmienić w czasie życia obiektu. • Aby uniknąć powyższego problemu, po zapełnieniu tablicy, można utworzyć nową, o większym rozmiarze i skopiować zawartość poprzedniej do niej. Jest to jednakże rozwiązanie mało wygodne. • Inicjowanie tablic: MyObj[] tablica1; // niezainicjowana MyObj tablica2[]; // jak wyżej MyObj[] tablica3 = new MyObj[5] // zainicjowana tablica 5 elementowa MyObj[] tablica4 ={new MyObj(), new MyObj()} /* zainicjowana tablica 2 elementowa */ • Zaletą tablicy jest sprawdzanie zakresu, dlatego też nie możliwe jest wyjście poza ściśle określony zakres tablicy, jak miało to miejsce w C lub C++
Tablice - ciąg dalszy • Tablicy niezainicjowanej nie można użyć. Jeżeli nastąpi odwołanie do niezainicjowanej tablicy, kompilator powiadomi nas o błędzie: • int[] tablica1; // Tablica nie zainicjowana • int[] tablilca2 = new int[6]; • int length = tablica1.length + tablica2.length; // Błąd • Tablica posiada składową tylko do odczytu length, będącą długością tablicy (lecz nie obecnej ilości obiektów, tylko ilość obiektów jakie można umieścić). • W przeciwieństwie do klas kontenerowych, które przechowują referencje do obiektów, tablice mogą przechowywać również zmienne typu podstawowego. • Tablice mogą być zwracane przez funkcje. W C lub C++ można było zwrócić wskaźnik do tablicy jako wynik funkcji. Java natomiast daje możliwość zwracania całej tablicy. Zwolnieniem pamięci z takiej tablicy nie trzeba się martwić, ponieważ po zniknięciu wszystkich odwołań do niej, zostanie automatycznie usunięta.
Arrays • Klasa Arrays,zamieszczona w pakiecie java.util, posiada zestaw metod statycznych do wykonywania operacji na tablicach: equals(tablica1, tablica2); // Porównuje tablice pod względem równości fill(tablica, wartość); // Wypełnia tablicę określoną wartością sort(tablica, comparator); // Sortuje tablicę (comparator opcjonalny) binarySearch(tablica, szukany_element, comparator ); /*Wyszukuje element w tablicy posortowanej*/ • Powyższe metody są przeciążone dla każdego z podstawowych typów i klasy Object • Ponadto klasa Arrays posiada metodę toList(), która zmienia tablicę w kontener List • Do kopiowania tablicy służy statyczna metoda System.arraycopy(), pozwalająca na kopiowanie tablic, szybciej niż za pomocą własnej pętli for. Jest ona przeciążona tak, że obsługuje tablice wszystkich typów. • Do porównywania elementów tablicy służy metoda compareTo() zamieszczona w interfejsie java.lang.Comparable
Porównywanie elementów • Do porównywania elementów tablicy służy metoda compareTo() zamieszczona w interfejsie java.lang.Comparable. Metoda ta jest niezbędna do działania metody sort() • Przykład 1 – porównanie z wykorzystaniem compareTo(): • import java.util.*; • public class CompType implements Comparable { • int value; • ... • public int compareTo(Object obj) { • int val = ((CompType)obj).value; • return (value < val ? –1 : (value == val ? 0 : 1)); • }
Porównywanie elementów – ciąg dalszy Przykład 2 – porównanie przy użyciu compare(): • import java.util.*; • public class MyComp implements Comparable { • public int compare(Object o1, Object o2) { • int val = ((CompType)o1).value; • int val2 = ((CompType)o2).value; • return (val1 < val2 ? –1 : (val1 == val2 ? 0 : 1)); • }
Wyszukiwanie elementów • Metody porównań wykorzystywane są również do wyszukiwania elementów za pomocą binarySearch(). • Przeszukiwanie tablic, jak sama nazwa sugeruje, odbywa się stosując wyszukiwanie binarne. Należy pamiętać o tym, aby nie wyszukiwać elementów w tablicy nieuporządkowanej, gdyż wynik jest nieprzewidywalny (nieskończona pętla rekurencyjna, lub błędny wynik). • Jeżeli do sortowania tablicy stosujemy Comparator, to trzeba włączyć ten Comparator podczas szukania: • import java.util.*; • public class search { • ... • MyComparator comp = new MyComparator(); • Arrays.binarySearch(tablica, szukany_element, comp); • }
Kontenery (Containers) • Kontenery dzielą się na dwie grupy Kolekcje: • Lista (List) – przechowuje elementy w określonej kolejności, może przechowywać powtarzające się elementy • Zbiór (Set) – przechowuje elementy w dowolnej kolejności, nie może zawierać elementów zduplikowanych Odwzorowanie: • Odwzorowanie (Map) – grupa par obiektów typu klucz-wartość • W skład kontenerów wchodzą trzy komponenty i po kilka ich implementacji tj. ArrayList, LinkedList, Vector, Stack, TreeSet, HashSet, LinkedHashSet,HashMap, LinkedHashMap, WeakHashMap, TreeMap, IdentityHashMap, HashTable
Diagram kontenerów: Kontenery diagram I
Kontenery diagram II • Diagram kontenerów II: • Jak widać na diagramie, tak naprawdę są tam tylko trzy komponenty: Map, List i Set. Reszta to interfejsy, klasy abstrakcyjne i klasy pochodne.
Kontenery diagram III • Diagram kontenerów III: • Jeszcze jeden diagram, tym razem w większym uproszczeniu
Kontenery - wypełnianie • Wypełnianie kontenerów: • Dla kontenerów istnieje klasa towarzysząca Collections zawierająca statyczne metody usługowe, między nimi metodę fill(). Metoda ta zastępuje elementy, które już są na liście. Działa tylko dla klasy List, a nie działa dla Map i Set. • fill() ma tę samą wadę, co dla tablic – wypełnia całą listę referencją do jednego obiektu. Przez to fill() jest niezbyt użyteczne. • Ponadto elementy można dodawać do kontenerów za pomocą metod add() i put(). Będą one omówione dalej, przy okazji omówienia interfejsu Collection.
Kontenery – wypisywanie I • Wypisywanie zawartości kontenerów: • Zawartość kontenerów można wypisać funkcją println() pobierającą jako parametr referencję do kontenera. Przy odwołaniu, gdzie oczekiwany jest typ String domyślnie wywoływana jest metoda toString(). Metodę toString można przeciążać dla wszystkich typów obiektów. • Przykład: • import java.util.*; • public class ListPrint { • List list = new ArrayList(); • for(int i=0; i<10; i++) • list.add(”element listy”); • System.out.println(list); // Wywoła domyślnie toString() • }
Kontenery – wypisywanie II • Wypisywanie adresu elementów kontenerów: • Jeżeli do wypisania adresu obiektu w pamięci użyty zostanie this, spowoduje to powstanie rekursji i wygenerowana zostanie niekończąca się lista wyjątków. Aby tego uniknąć należy zastosować super zamiast this. • Przykład 1: Przykład 2: import java.util.*; public class APrint1 { public class MyObject { public String toString() { return ”adress:” + this; }} public static void main(String[] args) { List list = new ArrayList(); for(int c; c<10; c++) v.add(new MyObject); System.out.println(list); } • import java.util.*; • public class APrint1 { • public class MyObject { • public String toString() { • return ”adress” + super; • }} • public static void main(String[] args) { • List list = new ArrayList(); • for(int c; c<10; c++) • v.add(new MyObject); • System.out.println(list);} zamiast
Sposób wyświetlania: • Zawartość kontenerów, wypisana za pomocą polecenia println, jest wyświetlana w różny sposób, za zależności, jaki kontener jest wypisywany • List: • Zawartość kontenera jest wyświetlana w nawiasach kwadratowych: • ”[element1, element2, element1, element3]” • Set: • Zawartość kontenera jest wyświetlana jak przy List, z tą różnicą, że elementy się nie powtórzą (wynika to z właściwości zbioru Set): • ”[element1, element2, element3]” • Map: • Zawartość kontenera jest wyświetlana w nawiasach klamrowych, w parach klucz = wartość_skojarzona : • ”{klucz1=element1, klucz2=element2, klucz3=element3}” Kontenery – wypisywanie III
Wady kontenerów • Nieznany typ: • Wadą kontenerów jest brak informacji o przechowywanym typie. Kontener przechowuje odwołania do obiektów klasy Object, będącej klasą bazową wszystkich innych. • Konsekwencje: • Brak ograniczeń co do typu zamieszczanych obiektów. • Trzeba wykonywać rzutowanie do właściwego typu przed użyciem obiektu • Klasa ArrayList o określonym typie: • public class MyList { • private List list = new ArrayList(); // przesłanianie funkcji, a nie przeciążanie • public void add(MyObj o) { list.add(o); } // jak w przypadku dziedziczenia • public MyObj get(int index) { return (MyObj)list.get(index); } • public int size() { return list.size(); } • } Wymusza typ
Iteratory • Iterator jest obiektem służącym do przemieszczania się po ciągu elementów i wybieranie napotkanych obiektów. • Iterator może być pobrany dzięki metodzie iterator(), zwracającej iterator do danego kontenera. • Funkcje iteratora: • next() – uzyskuje następny obiekt z ciągu, a zwraca poprzedni obiekt • hasNext() – sprawdza, czy są następne obiekty • remove() – usuwa ostatni zwrócony przez iterator obiekt
Interfejs Collection – funkcje • Funkcje interfejsu Collection: • boolean add(Object) – dodaje argument (zwraca false, jeśli go nie doda) • boolean addAll(Collection) – umieszcza wszystkie elementy kontenera argumenu (zwraca true, Jeżeli został dodany jakiś obiekt) • void clear() – usuwa wszystkie elementy • boolean contains(Objest) – sprawdza, czy obiekt jest w kontenerze • boolean containsAll(Collection) – sprawdza, czy kontener zawiera wszystkie obiekty argumentu • boolean isEmpty() – sprawdza, czy kontener jest pusty • Iterator iterator() – zwraca iterator • boolean remove(Object) – usuwa obiekt • boolean removeAll(Collection) – usuwa wszystkie obiekty zawarte w argumencie
Interfejs Collection – funkcji ciąg dalszy • Funkcje interfejsu Collection ciąg dalszy: • boolean retainAll(Object) – pozostawia przecięcie, z teorii zbiorów, dwóch kontenerów • int size() – Zwraca liczbę elementów zawartych w kontenerze • Object[] toArray() – zamienia kontener na tablicę • Object[] toArray(Object[] a) – jak wyżej, typ tablicy przyjmuje typ argumentu • Interfejs Collection nie posiada funkcji get() pozwalającej na swobodny dostęp do elementów. Do kategorii Collection należą też zbiory Set, toteż swobodny dostęp do nich byłby bezsensowny. • W celu wydobycia elementu należy użyć iteratora.
Interfejs List • ArrayList: • Implementacja interfejsu List jako tablicy. Umożliwia szybszy dostęp do jej elementów, kosztem wolniejszego ich wstawiania i usuwania. • LinkedList: • Zapewnia optymalny dostęp sekwencyjny, usuwanie i wstawianie elementów do środka listy. Powolna w przypadku swobodnego dostępu. LinkedList pozwala na zaimplementowanie stosu oraz kolejki. • Funkcje interfejsu List: • add(Object) – wstawia element (addFirst(), addLast() dla LinkedList) • get() – pobiera element (getFirst(), getLast() dla LinkedList) • remove() – usuwa element (removeFirst(), removeLast() dla LinkedList) • iterator() – zwraca iterator • MyClass[] toArray(MyClass[] a) – jak wyżej, typ tablicy przyjmuje typ argumentu
Stos • Stos jest przedstawiany jako kontener typu last-in, first-out (LIFO). Można go stworzyć na podstawie LinkedList: • Główne metody stosu: • public class MyStack { • private LinkedList list = new LinkedList; • public void push(Object o) { list.addFirst(o); } • public Object top() { return list.getFirst(); } • public Object pop() { return list.removeFirst|(); } • ... • }
Kolejka • Kolejkę jest kontenerem typu first-in, first-out (FIFO). Można ją stworzyć na podstawie LinkedList: • Główne metody kolejki: • public class MyQueue { • private LinkedList list = new LinkedList; • public void put(Object o) { list.addFirst(o); } • public Object get() { return list.removeLast(); } • ... • }
Interfejs Set • Set ma ten sam interfejs co Collection, pomimo to zachowuje się inaczej. Set nie pozwala na przechowywanie więcej niż jednego egzemplarza wartości każdego z obiektów. Interfejs ten nie zapewnia utrzymania elementów w żadnym porządku. Elementy Object zamieszczane w Set muszą definiować metodę equals(), w celu ustalenia, czy element już przypadkiem nie należy do zbioru. • HashSet: • Zapewnia krótki czas lokalizacji elementu. Wymaga zdefiniowania metody hashCode() i equals() dla elementów klasy Object. • TreeSet: • Zbiór uporządkowany na podstawie drzewa. Dzięki niemu można pobierać uporządkowany ciąg elementów. Jako zbiór uporządkowany, dostarcza metody first() i last() zwracające najmniejszy i największy element.
Interfejs Set - ciąg dalszy • LinkedHashSet: • Cechuje się taką samą szybkością dostępu jak hashSet, z tym, że LinkedHashSet zachowuje oryginalną kolejność dodawania elementów. Klasa ta oparta jest na LinkedList. Wyniki pobierane są w kolejności jakiej były dodawane do kontenera. • Każdy typów zbioru Set przechowuje elementy w innej kolejności: • W zbiorach HashSet, TreeSet i LinkedHashSet zamieszczone zostały elementy od 0 do 9. Po wypisaniu tych zbiorów otrzymamy: • [ 2 , 4 , 9 , 8 , 6 , 1 , 3 , 7 , 5 , 0 ] – HashSet • [ 9 , 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 , 0 ] – TreeSet • [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] – LinkedHashSet • Przy tworzeniu własnego typu, należy pamiętać o zaimplementowaniu interfejsu Compareble i metody compareTo(), aby zbiór Set mógł określić przynależność jakiegoś elementu do zbioru, oraz aby mógł zbiór posortować (w przypadku TreeSet).
SortedSet • Jedyną dostępną implementacją SortedSet jest klasa TreeSet. W związku z powyższym, TreeSet dostarcza metody interfejsu SortedSet: • Comparator comparator(); /* Zwraca interfejs Comparator, lub null dla domyślnego comparatora */ • Object first() ; // Podaje najmniejszy element • Object last(); // Podaje największy element • SortedSet subSet(odElementu, doElementu); /* zwraca fragment zbioru obejmujący element odElementu, do elementu doElementu */ • SortedSet headSet(doElementu); /* Zwraca fragment zbioru o elementach mniejszych od elementu doElementu */ • SortedSet tailSet(odElementu); /* Zwraca fragment zbioru o elementach większych od elementu odElementu */
Interfejs Map • Kontener Map pozwala na przechowywanie par elementów {klucz, obiekt}. Interfejs Map dostarcza kilka podstawowych metod do obsługi tego rodzaju kontenerów: • put(Object klucz, Object wartość); // dodaje wartość i wiąże ją z kluczem • get(Object klucz); // zwraca wartość związaną z kluczem • containsKey(); // sprawdza czy odwzorowanie zawiera klucz • containsValue(); // sprawdza czy odwzorowanie zawiera wartość; • Odwzorowania Map do wyszukiwania elementów wykorzystują metodę hashCode() obiektu przechowywanego, przez co wyszukiwanie elementów w odwzorowaniach Map jest szybsze, niż gdyby zastosowana została metoda z ArrayList.
Interfejs Map - typy • W Javie dostępne są kilka typów odwzorowań Map: HashMap, TreeMap, LinkedhashMap, WeakhashMap oraz IdentityHashMap. • HashMap: • implementacja oparta na tablicy asocjacyjnej (dawniej HashTable). Zapewnia lokalizację i wstawianie par w czasie stałym. Zachowanie może być regulowane, poprzez ustawienie parametrów w konstruktorze. • LinkedHashMap (JDK 1.4): • Implementacja podobna do HashMap, lecz podczas przeglądania pary zwracane są w kolejności wstawiania. Działa trochę wolniej od HashMap. Wstawianie elementów odbywa się szybciej niż w HashMap, dzięki wykorzystaniu listy połączonej.
Interfejs Map – typy – ciąg dalszy • TreeSet: • Implementacja oparta na drzewach czerwono-czarnych. Pary po umieszczeniu są sortowane według porządku wyznaczonym przez interfejs Comparable. Dzieki posortowaniu dostępne są funkcje firstKey() i lastKey() zwracające najmniejszy i największy z kluczy. Ponadto posiada metodę subMap() pozwalającą uzyskać fragment drzewa. • WeakHashMap: • Odwzorowanie operujące na słabych klucza, umożliwiające usunięcie z pamięci obiektów przechowywanych w mapie. Obiekty bez odwołań są usuwane przez odśmiecacz pamięci, jeżeli w programie nie ma odwołań do nich. • IdentityHashMap (JDK 1.4): • Odwzorowanie hash’ujące określające równość kluczy przy wykorzystaniu operatora == zamiast metody equals(). Nie nadaje się do zastosowań ogólnych.
Interfejs Map – wydajność • Czynniki wydajności HashMap: • pojemność – liczba komórek tablicy • pojemność początkowa – liczba komórek tablicy po jej stworzeniu • rozmiar – liczba pozycji znajdujących się obecnie w tablicy • współczynnik zapełnienia – 0 oznacza pustą tablicę, 1 pełną (rozmiar/pojemność). Współczynnik ten oznacza, że gdy kontener osiągnie ten współczynnik zapełnienia, to automatycznie zwiększy swój rozmiar (pojemność). • Powyższe współczynniki można ustawiać w konstruktorze, w celu zmiany zachowania odwzorowania.
SortedMap • Jedyną dostępną implementacją SortedMap jest klasa TreeMap. W związku z powyższym, TreeMap dostarcza metody interfejsu SortedMap: • Comparator comparator(); /* Zwraca interfejs Comparator, lub null w przypadku domyślnego comparatora*/ • Object firstKey() ; // Podaje najmniejszy klucz • Object lastKey(); // Podaje największy klucz • SortedMap subMap(odKlucza, doklucza); /* zwraca fragment odwzorowania obejmujący klucze odKlucza, do klucza doKlucza */ • SortedMap headMap(doKlucza); /* Zwraca fragment odwzorowania o kluczach mniejszych od klucza doKlucza */ • SortedMap tailMap(odKlucza); /* Zwraca fragment odwzorowania o kluczach większych od klucza odKlucza */
Hash’owanie • Każdy obiekt domyślnie dziedziczy po klasie Object, jeżeli nie określimy klasy bazowej. Dlatego też każdy obiekt dziedziczy metodę hashCode() klasy Object, zwracającą adres danego obiektu. • Aby można było korzystać z odwzorowań dla własnych obiektów, należy zaimplementować metodę equals() i interfejs Comparable, a także metodę hashCode() (chyba, że chcemy skorzystać z domyślnej metody klasy Object). • Powinno się za każdym razem przesłaniać metodę hashCode(), w celu uzyskania poprawnego działania kontenerów opartych na działaniu tej metody. • Przykład: • public putInHash() { • Map map = new HashMap(); • int[] tab1 = {1, 2}; • int[] tab2 = {2, 1}; • map.put(tab1, new Object()); • map.put(tab2, new Object()); } Różne dla equals() Takie samo dla hashCode()
Hash’owanie przykład kolejny public putInHash() { public class MyKey { int field1; int field2; MyKey(int f1, int f2) { field1 = f1; field2 = f2;} Map map = new HashMap(); MyKey k1 = new MyKey(1, 2); MyKey k2 = new MyKey(2, 1); map.put(k1, new Object()); map.put(k2, new Object()); } Jak poprzednio: różne dla equals() Takie samo dla hashCode()
hashCode() - przykład public class TShirt { int ID; //number on tshirt int size; // size of tshirt String desc; // description public TShirt(int id, int s, String d) { ID = id; size = s; description = d; } public int hashCode() { return ID; }; public boolean equals(Object o) { return ( desc.equals(((TShirt) o).desc) && size == ((TShirt) o).size); } } public class PList{ String[] players = {”p1”, ”p2”}; TShirt ts1 = new TShirt(1, 7, ”player’s 1 shirt”); TShirt ts2 = new TShirt(2, 7, ”player’s 1 shirt”); public Plist() { Map map = new HashMap(); map.put(ts1, players[0]); map.put(ts2, players[1]); }; }
Dodatkowe usługi klasy Collections • max(Collection) – zwraca maksymalny element, stosuje normalną metodę porównania dla obiektów w strukturze • min(Collection) – jak wyżej, lecz zwraca element minimalny • max(Collection,Comparator) – j.w. stosuje Comparator do porównań • min(Collection,Comparator) – analogicznie do powyższego • indexOfSublist(List Source, List Target) – podaje indeks pierwszego miejsca, w którym Target występuje w Source • lastOfSublist(List Source, List Target) – analogicznie do powyższego • replaceAll(List list, Object old, Object new) – zamienia old na new w liscie • reverse(List) – odwracanie kolejności występowania • rotate(List list, int dist) – przesuwa elementy listy o dystans dist • copy(List target, List source) – kopiuje elementy z source do taget • swap(List list, int i, int j) – zamienia położenie elementów i i j
Dodatkowe usługi klasy Collections • nCopies(int n, Object o) – zwraca niemodyfikowalną listę rozmiaru n, której wszystkie odwołania wskazują na obiekt o • Enumeration(Collection) – zwraca obiekt Enumeration dla podanego argumentu • List(Enumeration e) – Zwraca obiekt ArrayList wygenerowany przy użyciu podanego obiektu Enumeration
Synchronizacja • Kolekcje mogą być obsługiwane przez kilka wątków jednocześnie, co z kolei wymaga, aby ta kolekcja była w jakiś sposób synchronizowana. • Klasa Collections udostępnia sposób automatycznej synchronizacji całego kontenera. Dzięki temu nie ma sposobności przypadkowego udostępnienia wersji niezsynchronizowanej. • Przykłady: • Set s = Collections.synchronizedSet(new HashSet()); • List l = Collections.synchronizedList(new ArrayList()); • Map m = Collections.synchronizedMap(new HashMap()); • W powyższych przypadkach, nowy kontener jest natychmiast przekazywany do odpowiedniej metody synchronizującej.
fail-fast • Kontenery w Javie posiadają również zabezpieczenie przed modyfikacją ich zawartości przez więcej niż jeden proces. • Mechanizm fail-fast wyszukuje wszystkie zmiany kontenera, nie pochodzące od danego procesu. Jeżeli inny proces modyfikuje kontener, spowoduje to pojawienie się wyjątku ConcurrentModificationException. • Przykład: • public class MyClass { • public static void main(String[] args) { • Collection c = new ArrayList(); • Iterator it = c.interator(); • c.add(”My object”); • } Spowoduje pojawienie się wyjątku ConcurrentModificationException
Koniec „To już jest koniec. Nie ma już nic. Jesteśmy wolni. (?) Możemy iść.” (???)