Pobierz prezentację
Pobieranie prezentacji. Proszę czekać
OpublikowałStefan Jabłoński Został zmieniony 9 lat temu
1
Klasy kolekcji Tomasz Wilczyński
2
Kolekcja Schemat kolekcji Javy zawiera sześć interfejsów oraz kilka innych implementacji tych interfejsów do celów ogólnych. W referacie poznamy podstawowe właściwości głównych interfejsów. Potem pokażę jak programy używają tych interfejsów. Kolekcja (ang. collection) jest to obiekt reprezentujący grupę innych obiektów. Kolekcje zwykle reprezentują dane, które tworzą naturalną grupę, jak katalog poczty (kolekcja listów) lub katalog telefonów (kolekcja przyporządkowań nazwisk do numerów telefonów). Kolekcja definiuje grupę obiektów zwanych elementami. Na grupie można wykonać następujące operacje: Dodanie nowego elementu do kolekcji Usunięcie elementu z kolekcji Wyszukiwanie elementu w kolekcji Zwracanie rozmiaru kolekcji Przechowywanie elementów jakiejkolwiek kolekcji używa klasy object. To oznacza, że wymagana jest rzutowanie kiedy pobierany jest element z kolekcji.
3
Kolekcje API Java Software Development Kit (SDK) dostarcza nam kolekcje API, czyli ujednolicony schemat dla reprezentacji i manipulowania kolekcjami. Ten schemat dostarcza wcześniej już napisane klasy do tworzenia typowych struktur danych. Jest wiele zalet używania wcześniej już stworzonych kolekcji. Są to m.in..: Redukcja wysiłku przy próbach samodzielnej implementacji algorytmów i struktur danych, gdyż są one już gotowe i sprawdzone Zwiększenie wydajności, gdyż dostarczone nam kolekcje są niezwykle wydajnymi implementacjami użytecznych algorytmów i struktur danych
4
Interfejsy Na rysunku widzimy sześć interfejsów, które tworzą rdzeń schematu kolekcji. Każdy z nich definiuje unikalną grupę atrybutów i metod, które z kolei definiują specyficzne zachowania dla każdego typu obiektu.
5
Interfejs Collection Interfejs Collection jest interfejsem bazowym dla interfejsów List oraz Set. Interfejs ten zawiera wszystkie operacje wspólne dla obu pochodnych interfejsów. SDK nie dostarcza bezpośredniej implementacji interfejsu Collection, a jedynie implementacje bardziej szczegółowych podinterfejsów ( ang. subinterface ) jak Set, List. Interfejs Collection jest zazwyczaj używany do przekazywania kolekcji i manipulowania nimi kiedy potrzebna jest maksymalnie duża ogólność opisu kolekcji.
6
Ogólne implementacje interfejsów Collection
7
Konstruktory kolekcji Wszystkie implementacje klas Collection ogólnego użytku (które zazwyczaj implementują Collection pośrednio poprzez któryś z jej podinterfejsów) powinny dostarczać dwóch „standardowych” konstruktorów: W rezultacie ten drugi konstruktor pozwala użytkownikowi na kopiowanie każdej kolekcji produkując kolekcję typu pożądanej implementacji. Nie ma jak wymusić stosowania tej konwencji (ponieważ interfejsy nie mogą zawierać konstruktorów) ale wszystkie implementacje klas Collection ogólnego użytku w bibliotekach platformy Java stosują się do niej.
8
Operacje nieobsługiwane Metody „destrukcyjne” zawarte w tym interfejsie tj. metody które modyfikują kolekcje na której działają, są określone tak aby wyrzucały UnsupportedOperationException, jeśli kolekcja nie obsługuje tej operacji. Jeśli tak jest, te metody mogą, ale nie muszą wyrzucać tego błędu, jeśli wywołanie nie miałoby żadnego wpływu na kolekcję. Dla przykładu, wywołanie metody addAll(Collection) na rzecz kolekcji niemodyfikowalnej może, ale nie musi wyrzucać wyjątku jeżeli dodawana kolekcja jest pusta. Niektóre implementacje kolekcji mają restrykcje na elementy które mogą zawierać. Dla przykładu niektóre implementacje zabraniają elementów null, a niektóre mają ograniczenia na typy swych elementów. Próba dodania niestosownego elementu wyrzuca niesprawdzany wyjątek, typowo NullPointerException albo ClassCastException. Próba sprawdzania obecności niestosownego elementu może wyrzucać wyjątek albo po prostu zwracać false. Różne implementacje będą zachowywały się w jeden lub w drugi sposób. Ogólniej, próba wykonania operacji na niestosownym elemencie, której wykonanie nie skończyłoby się włączeniem tego niestosownego elementu do kolekcji może wyrzucać wyjątek, lub skończyć się powodzeniem – zależnie od implementacji. Takie wyjątki są oznaczone jako opcjonalne w specyfikacji tego interfejsu.
9
Metody w Collection
10
Interfejs List Najważniejszą właściwością implementacji interfejsu List jest kolejność. List reprezentuje bowiem uporządkowaną listę elementów. Interfejs ten dopuszcza powtarzające się elementy. Obiekty List zapewniają kontrolę nad miejscem dołączenia nowego elementu. Do każdego elementu mamy dostęp poprzez indeks. Istnieją dwie główne implementacje interfejsu List: klasy ArrayList i LinkedList.
11
Klasa ArrayList Klasa ArrayList jest to implementacja interfejsu List jako tablicy, u żywana jest więc do tworzenia dynamicznych tablic. W przeciwieństwie do zwykłych tablic, które mają ustaloną wielkość, klasa ArrayList rośnie w miarę dodawania elementów do obiektu array. Implementuje wszystkie opcjonalne operacje listy, i dopuszcza wszystkie elementy, także null. Oprócz możliwości interfejsu List klasa ta dostarcza metody do manipulowania rozmiarem tablicy, która jest używana wewnętrznie do przechowywania listy. Operacje size, isEmpty, get, set, iterator, oraz listIterator przebiegają w stałym czasie. Operacja add przebiega w (ang.) amortized constant time, czyli dodanie n elementów zajmuje O(n) czasu. Czas wykonania pozostałych metod jest liniowy. Składnik stały jest mały w porównaniu do tego w implementacji LinkedList. Klasa ArrayList pozwala na szybki dostęp swobodny do elementów, ale jest wolniejsza przy wstawianiu i usuwaniu elementów z wnętrza listy.
12
Pojemność ArrayList Każda instancja ArrayList posiada pojemność. Jest to rozmiar tablicy użytej do przechowania elementów listy. Ten rozmiar jest co najmniej taki jak rozmiar danej listy. Gdy dodajemy jakiś element do listy, jej pojemność automatycznie rośnie. Szczegóły tego wzrostu nie są wyspecyfikowane poza tym, że dodanie elementu posiada stały koszt amortized time. Aplikacja może zwiększyć pojemność instancji ArrayList przed dodaniem dużej liczby elementów dzięki operacji ensureCapacity. To może zmniejszyć liczbę dodatkowych alokacji.
13
Synchronizacja Należy zauważyć, że implementacja nie jest zsynchronizowana. Jeżeli wiele wątków wejdzie do ArrayList współbieżnie, i chociaż jeden z nich zmodyfikuje strukturę listy, to ArrayList musi być zsynchronizowana zewnętrznie. (Strukturalna modyfikacja to taka, która dodaje lub kasuje jeden lub więcej elementów, lub wyraźnie zmienia rozmiar związanej z nią tablicy; jedynie ustawienie wartości elementu nie jest modyfikacją strukturalną) Jest to typowo osiągane przez synchronizację na obiekcie który naturalnie opakowuje listę. Jeżeli żaden taki obiekt nie istnieje, lista powinna zostać „zawinięta” używając metody Collections.synchronizedList(new ArrayList(...)). Najlepiej robić to w czasie tworzenia, co zapobiega przypadkowemu niezsynchronizowanemu dostępowi do listy: List list = Collections.synchronizedList(new ArrayList(... )).
14
Przykład użycia ArrayList [1] import java.util.*; public class ReadArr{ public static void main(String[] args){ List myList = new ArrayList(); // Wypełnij ArrayList for (int i = 0; i < args.length ; i++){ myList.add(args[i]); } // Wypisz na ekran ArrayList for (int i = 0; i < myList.size(); i++){ System.out.println("Element "+ i +":"+myList.get(i)); } Wynik działania programu $ java ReadArr referat z klas kolekcji prowadzi Tomek Wilczyński Element 0: referat Element 1: z Element 2: klas Element 3: kolekcji Element 4: prowadzi Element 5: Tomek Element 6: Wilczyński
15
Przykład użycia ArrayList [2] Warto zauważyć, że zmienna myList została zdefiniowana używając typu interfejsu List ale została utworzona używając klasy ArrayList. Jest to bardzo zalecany zwyczaj w pracy z kolekcjami. Dzięki definiowaniu obiektu używając typu interfejsu, nasz kod staje się niezależny od implementacji kolekcji. To pozwala na zmianę implementacji kolekcji przez zmianę jedynie konstruktora obiektu. Należy też zauważyć iż konstruktor ArrayList() tworzy klasę bez definiowania jej rozmiaru. Domyślnie tworzonych jest 10 elementów listy. Można określić początkowy rozmiar klasy ArrayList. Dla przykładu: List myList = new ArrayList(5); List myList = new ArrayList(7); Ten przykład tworzy listy odpowiednio pięcio i siedmio elementowe. Gdy tablica osiąga swoją pojemność, rozmiar tablicy automatycznie rośnie.
16
Klasa LinkedList LinkedList jest implementacją interfejsu List. Implementuje wszystkie opcjonalne operacje listy, i dopuszcza wszystkie elementy (także null). Oprócz możliwości interfejsu List klasa ta dostarcza jednolicie nazwane metody get, remove i insert (pobierz, usuń, dodaj) element na początek i na koniec listy. Te operacje umożliwiają LinkedList być używanej jako stos (stack), kolejka (queue), czy kolejka dwukierunkowa (deque). Wszystkie operacje na stosie/kolejce/kolejce dwukierunkowej mogą być łatwo odtworzone przy pomocy standardowych operacji na listach. Znajdują się one tu głównie dla wygody, choć mogą pracować nieco szybciej niż te same operacje zwykłej listy. Wszystkie te operacje działają jak można by się spodziewać dla doubly-linked- list. Operacje odwołujące się do elementu listy będą przemierzać ją od początku lub od końca, w zależności z którego końca jest bliżej do danego elementu. Zapewnia optymalny dostęp sekwencyjny wraz z tanim usuwaniem i wstawianiem na środek listy. Jest stosunkowo powolna w przypadku dostępu swobodnego (lepiej użyć ArrayList ). Podobnie jak w ArrayList, implementacja nie jest zsynchronizowana.
17
Interfejs iterator Jedną z cech kolekcji jest wbudowana metoda przechodzenia przez zawartość kolekcji. W poprzednim przykładzie pętla for jest używana do wypisywania zawartości klasy ArrayList. W kolekcji możemy zamiast tego użyć interfejsów Iterator oraz ListIterator.
18
Przykład użycia iteratora import java.util.*; public class ReadArrIt{ public static void main(String[] args){ List myList = new ArrayList(); // Wypelnij ArrayList for (int i = 0; i < args.length ; i++){ myList.add(args[i]); } //Wypisz na ekran ArrayList String temp = ""; Iterator i = myList.iterator(); while(i.hasNext()){ temp = (String) i.next(); System.out.println("Element: "+ temp); } Wywołanie metody iterator() tworzy obiekt iterator i. Metoda hasNext() zwraca true jeżeli istnieją jeszcze elementy do których można przejść. Metoda next() zwraca element wskazywany w danej chwili, i przesuwa wskaźnik do następnego obiektu. Należy pamiętać, iż elementy kolekcji są przechowywane używając klasy Object więc aby otrzymać element z kolekcji musimy użyć rzutowania
19
Wyniki działania programu $ java ReadArrIt referat z klas kolekcji prowadzi Tomek Wilczyński Element: referat Element: z Element: klas Element: kolekcji Element: prowadzi Element: Tomek Element: Wilczyński Pomimo tego że każdy element z kolekcji jest dostarczony, nie mamy dostępu do indeks ó w element ó w. Ponadto, program Iterator może przemierzać kolekcję tylko w jednym kierunku.
20
Inerfejs Map Interfejs Map reprezentuje grupę par wartość / klucz. Każdy obiekt przechowywany w interfejsie Map posiada klucz, który jednoznacznie wskazuje obiekt z nim powiązany. Każda wartość przechowuje obiekt, dozwolone są powtórzenia. Ponieważ interfejs Map przechowuje parę wartość i klucz, a nie poszczególne obiekty, nie dziedziczy on z interfejsu Collection. Istnieją dwie główne implementacje interfejsu Map : HashMap i TreeMap. Omówimy także HashTable.
21
Klasa Hashtable [1] Klasa ta implementuje tablice haszujące, które przyporządkowują klucze do wartości. Jako klucz i wartość może występować dowolny obiekt nie będący null. Aby efektywnie przechowywać i dostarczać obiekty z tablicy haszującej, obiekty użyte jako klucze muszą implementować metodę hashCode oraz equals. Instancja HashTable ma dwa parametry wpływające na jej wydajność: początkowa pojemność (ang. initial capacity) oraz czynnik wypełnienia (ang. load factor). Pojemność jest to ilość kubełków w tablicy haszującej, zaś początkowa pojemność to po prostu pojemność tablicy haszującej w chwili jej stworzenia. Czynnik wypełnienia jest miarą tego jak bardzo może zostać wypełniona tablica haszująca, zanim jej pojemność zostanie automatycznie zwiększona. Gdy współczynnik ten zostanie osiągnięty, kontener automatycznie zwiększy swoją pojemność (liczbę kubełków) poprzez podwojenie, a następnie ponownie przydzieli przechowywane obiekty do nowych kubełków – jest to nazywane powtórnym haszowaniem (ang. rehash).
22
Klasa Hashtable [2] Generalnie, domyślny współczynnik wypełnienia (.75) zapewnia dobry bilans pomiędzy kosztami czasu i pamięci. Większe wartości zmniejszają zapas miejsca, lecz zwiększają czas potrzebny do znalezienia danej pozycji. Jest to widoczne przy większości operacji Hashtable, w tym get i put. Początkowa pojemność kontroluje bilans pomiędzy zmarnowaną pamięcią a potrzebą operacji powtórnego haszowania, które zabierają dużo czasu. Operacja powtórnego haszowania nie nastąpi nigdy, jeżeli początkowa pojemność jest większa niż maksymalna liczba pozycji tablicy haszującej podzielonej przez jej współczynnik wypełnienia. Jednak ustalenie zbyt wysokiej początkowej pojemności może być marnowaniem pamięci. Jeżeli ma zostać stworzonych wiele pozycji tablicy haszującej, to tworzenie jej z wystarczająco dużą pojemnością może pozwolić na wstawienie pozycji bardziej wydajnie niż gdybyśmy pozwolili jej automatycznie używać powtórnego haszowania w miarę wzrostu tablicy. Implementacja ta jest zsynchronizowana.
23
Przykład użycia Hashtable
24
Interfejs HashMap Klasa HashMap dostarcza implementację tablicy haszującej interfejsu Map. Implementuje ona wszystkie opcjonalne operacje klasy Map, oraz w przeciwieństwie do Hashtable dopuszcza null dla klucza i wartości. Ponadto, różnica pomiędzy klasami Hastable i HashMap polega na tym, że HashMap nie jest zsynchronizowana, podczas gdy Hashtable jest. Klasa HashMap nie gwarantuje zachowania ustalonego porządku elementów. Implementacja ta zapewnia stały czas wykonania podstawowych operacji get oraz put, zakładając że funkcje haszujące prawidłowo rozkładają elementy w kubełkach. Iteracja po elementach wymaga czasu proporcjonalnego do pojemności instancji HashMap (liczby kubełków) plus jej rozmiar (liczba przyporządkowań (mapowań) klucz – wartość). Jest więc bardzo ważne aby nie ustalać początkowej pojemności zbyt wysokiej (ani czynnika wypełnienia zbyt niskiego) jeżeli zależy nam na wydajności iteracji. Instancja HashMap, tak jak i Hashtable, posiada dwa parametry: pojemność początkową oraz współczynnik wypełnienia omówione już wcześniej.
25
Przykład użycia HashMap Poniższy przykład pokazuje jak stworzyć i wypisać na ekran klasę HashMap. Program czyta słowa podane w linii poleceń, i używa interfejsu Map do zliczenia liczby pojawień się każdego słowa. import java.util.*; public class ReadHashMap{ public static void main(String[] args){ Map myMap = new HashMap(); Integer count; String key; // Wypełnij Map i zlicz słowa for (int i = 0; i < args.length ; i++){ key = args[i]; if (myMap.get(key) == null){ myMap.put(key, new Integer(1)); } else { count = (Integer) myMap.get(key); count = new Integer(count.intValue()+1); myMap.put(key, count); } //Wypisz pary nazwa / wartość Set keys = myMap.keySet(); Iterator i = keys.iterator(); while(i.hasNext()){ key = (String) i.next(); System.out.println("Słowo: '" + key + "' Wartość: " + myMap.get(key)); }
26
Wyniki działania programu $ java ReadHashMap nie zrozumiesz kolekcji jeżeli nie zrozumiesz dziedziczenia Słowo: "jeżeli" Wartość: 1 Slowo: "kolekcji" Wartość: 1 Słowo: "zrozumiesz" Wartość: 2 Słowo: "dziedziczenia" Wartość: 1 Słowo: "nie" Wartość: 2 Klasa HashMap jest stworzona i przypisana do interfejsu Map. Zwróćmy uwagę na dodanie elementu do interfejsu Map : wywołana zostaje metoda get na rzecz obiektu myMap. Jeżeli klucz i wartość istnieją dla danej wartości klucza, zwracana jest wartość. Jeżeli klucz nie istnieje lub żadna wartość nie została przypisana do klucza, zwracany jest null. Jeżeli wartością jest null, wtedy tworzony jest nowy element używając słowa przypisanego zmiennej klucz. Metoda put ustala tą wartość na 1 gdyż jest to pierwsze pojawienie się słowa. Jeżeli metoda get zwraca wartość inną niż null, wtedy wykonywany jest blok instrukcji po else. Wartość jest pobierana używając metody get i bieżącego słowa jako klucza. Następnie wartość klucza jest zwiększana o 1. W kolejnej linii metoda put wkłada nową wartość z powrotem do map. Iterowanie przez map do drukowania kluczy i ich wartości jest nieco inne. Na początku tworzony jest set z kluczy dla obiektu myMap. Od tego set tworzony jest iterator. Klucz jest dostarczony od iteratora a następnie użyty do sprawdzenia wartości. Należy zauważyć że wypisane elementy nie są w żaden sposób uporządkowane.
27
Klasa TreeMap Implementacja interfejsu SortedMap oparta na drzewie czerwono – czarnym. Klucze w poprzednim przykładzie nie są przechowywane w żadnym szczególnym porządku, ponieważ do przechowania ich używa się funkcji haszującej. Jeżeli potrzebne są klucze i przyporządkowane im wartości w porządku rosnącym, należy użyć implementacji klasy TreeMap. Jest to główna zaleta TreeMap ; klasa ta posiada też metodę submap, która pozwala uzyskać fragment drzewa.
28
Przykład użycia TreeMap Jeżeli w poprzednim przykładzie zamiast Map myMap = new HashMap(); użyjemy Map myMap = new TreeMap(); otrzymujemy klucze uporządkowane, więc wynik działania programu będzie następujący : $ java ReadTreeMap nie zrozumiesz kolekcji jeżeli nie zrozumiesz dziedziczenia Słowo: "dziedziczenia " Wartość: 1 Słowo: "jeżeli " Wartość: 1 Słowo: "kolekcji " Wartość: 1 Słowo: "nie " Wartość: 2 Słowo: "zrozumiesz" Wartość: 2 Klucze są przechowywane w porządku rosnącym używając drzewa. Stąd klucze i ich wartości są pobierane w porządku rosnącym. Implementacja ta nie jest zsynchronizowana. Implementacja ta zapewnia koszt czasu log(n) dla operacji containsKey, get, put, remove.
29
Interfejs Set Interfejs Set ma dokładnie ten sam interfejs co Collection, dlatego nie posiada żadnych dodatkowych funkcji, jak to było w przypadku dwóch różnych odmian List. Mimo iż zbiór Set jest dokładnie interfejsem Collection, zachowuje się inaczej (jest to idealne użycie dziedziczenia i polimorfizmu – wyrażenie różnego zachowania). Zbiór nie pozwala na przechowywanie więcej niż jednego egzemplarza wartości każdego z obiektów. Tak więc każdy element w grupie jest unikalny; unikalność takiego elementu jest określona przez metodę equals Istnieją dwie podstawowe implementacje interfejsu Set – klasy HashSet i TreeSet.
30
Klasa HashSet Jest to implementacja interfejsu Set używana dla zbiorów w których istotny jest krótki czas lokalizacji elementu. HashSet używa do tego celu funkcji haszującej.
31
Przykład użycia HashSet Poniższy przykład implementuje klasę HashSet. Drukuje on komunikat przy próbie dodania obiektu już istniejącego: import java.util.*; public class ReadSet{ public static void main(String[] args){ Set mySet = new HashSet(); // Wypełnij Set boolean added; for (int i = 0; i < args.length ; i++){ added = mySet.add(args[i]); if (!added){ System.out.println("Wielokrotny element: " +args[i]); } //Wydrukuj Set na standardowe wyjście Iterator i = mySet.iterator(); while(i.hasNext()){ System.out.println("Element: " + i.next()); }
32
Wynik działania programu $ java ReadSet nie zrozumiesz kolekcji jeżeli nie zrozumiesz dziedziczenia Wielokrotny element: nie Wielokrotny element: zrozumiesz Element: jeżeli Element: kolekcji Element: zrozumiesz Element: dziedziczenia Element: nie Zmienna mySet została zainicjalizowana podobnie jak w przypadku interfejsu listy. Jednak dodawanie elementu do interfejsu Set jest inne. Metoda add zwraca wartość boolean. Jeżeli duplikat obiektu nie istnieje, obiekt zostaje dodany i metoda add zwraca true. Jeżeli duplikat istnieje, obiekt nie zostaje dodany i metoda zwraca false. Zmienna added zwraca rezultat metody add. Jeżeli obiekt nie jest dodany, zostaje wypisany komunikat. Należy zwrócić uwagę na wynik działania programu: Klasa HashSet nie trzyma elementów w żadnym ustalonym porządku. Gdy program wypisuje klasę HashSet, elementy nie są drukowane w porządku alfabetycznym, ani też w kolejności w jakiej zostały dodane. Jeżeli potrzebujemy mieć elementy ułożone w porządku, musimy użyć implementacji klasy TreeSet.
33
Klasa TreeSet Klasa TreeSet jest implementacją inerfejsu SortedSet – zbiór uporządkowany na podstawie drzewa. Dzięki niemu można pobierać uporządkowany ciąg elementów. Elementy przechowywane w TreeSet są porządkowane używając interfejsu Comparable. Tak więc każdy element przechowywany w set musi być implementacją interfejsu Comparable
34
Przykład użycia TreeSet Poniższy program jest modyfikacją poprzedniego. Używamy tutaj TreeSet zamiast HashSet import java.util.*; public class ReadTreeSet{ public static void main(String[] args){ Set mySet = new TreeSet(); // Wypełnij Set boolean added; for (int i = 0; i < args.length ; i++){ added = mySet.add(args[i]); if (!added){ System.out.println("Wielokrotny element: " + args[i]); } //Wypisz na ekran Set Iterator i = mySet.iterator(); while(i.hasNext()){ System.out.println("Element: " + i.next()); }
35
Wynik działania programu $ java ReadTreeSet nie zrozumiesz kolekcji jeżeli nie zrozumiesz dziedziczenia Wielokrotny element: nie Wielokrotny element: zrozumiesz Element: jeżeli Element: kolekcji Element: zrozumiesz Element: dziedziczenia Element: nie Widzimy iż wypisane elementy są uporządkowane, co jest główną zaletą TreeSet.
36
Bibliografia
Podobne prezentacje
© 2024 SlidePlayer.pl Inc.
All rights reserved.