1. Wprowadzenie do kontenerów Wykład ósmy Kontenery s ą obiektami, które potrafi ą przechowywa ć inne obiekty w określony sposób. O kontenerach można myśle ć jako o gotowych do użycia strukturach danych. W języku Java istniej ą kontenery pozwalające np.: przechowywa ć obiekty w postaci listy wiązanej lub w postaci drzewa. Mechanizm działania takich obiektów jest ukryty przed ich użytkownikami (czyli równie ż przed nami). Jedyne czym dysponuje programista korzystający z kontenerów jest ich interfejs i dokumentacja do niego dostarczona wraz ze środowiskiem Javy. Stosując kontenery należy pamięta ć, że nie przechowuj ą one wprost obiektów, tylko referencje do nich. Referencje te nie s ą takiej klasy jak umieszczane w kontenerze obiekty, ale klasy Object. Oznacza to, ze umieszczając obiekt w kontenerze tracimy informacje o jego klasie. Wszystkie kontenery w języku Java dziel ą si ę na dwa rodzaje opisane dwoma różnymi interfejsami. Pierwszym rodzajem kontenerów s ą kolekcje. Ich typ jest określony interfejsem Collection. Kontenery te zazwyczaj przechowuj ą obiekty wedle jakiej ś określonej reguły, np.: zbiory przechowuj ą tylko różne elementy, przy czym można określi ć któr ą cech ą maj ą si ę one różni ć, a listy przechowuj ą obiekty w określonym porządku. Drugi rodzaj stanowi ą odwzorowania, określone interfejsem Map. Odwzorowania przechowuj ą pary obiektów, postaci klucz warto ść. Zapoznajmy si ę na początek z metodami, jakie s ą zadeklarowane w interfejsie Collection: boolean add(object) jest metod ą opcjonaln ą, służy do dodawania obiektu do kontenera, zwraca warto ść false jeśli ta operacja się nie powiedzie, boolean addall(collection) równie ż jest metod ą opcjonaln ą, jej działanie polega na dodaniu do kontenera wszystkich elementów, znajdujących si ę w kolekcji przekazanej jej przez parametr, zwraca warto ść false jeśli żaden z obiektów nie zostanie dodany, void clear() - podobnie jak poprzedniczki jest opcjonalna, usuwa wszystkie obiekty z kontenera, boolean contains(object) zwraca warto ść true jeśli w kontenerze znajduje si ę obiekt do którego została przekazana jej referencja, boolean containsall(collection) podobnie jak poprzedniczka, ale sprawdza nie pojedynczy obiekt, a wszystkie obiekty należące do podanej jej przez parametr kolekcji, boolean isempty() - sprawdza, czy kontener jest pusty, jeśli tak to zwraca warto ść true, Iterator iterator() - zwraca referencj ę do obiektu iteratora, pozwalającego porusza ć si ę po obiektach wchodzących w skład kontenera, boolean remove(object) jeśli obiekt, którego referencja została tej metodzie przekazana przez parametr, znajduje si ę w w kontenerze, to jest on usuwany i zwracana jest warto ść true, jest metod ą opcjonaln ą, boolean removeall(collection) usuwa wszystkie obiekty, z kontenera, które nale żą do przekazanej jej przez parametr kolekcji, jest metod ą opcjonaln ą, boolean retainall(collection) zatrzymuje w kontenerze te obiekty, które nale żą równie ż od kolekcji przekazanej jej przez parametr, jest metod ą opcjonaln ą, int size() - zwraca liczb ę obiektów umieszczonych w kontenerze, Object[] toarray() - zwraca tablic ę referencji do wszystkich obiektów z kontenera, Object[] toarray(object[] a) zwraca tablic ę referencji do wszystkich obiektów umieszczonych w kontenerze, typ referencji jest taki sam jak typ elementów tablic przekazanej przez parametr. Interfejs Collection jest rozszerzany przez dwa inne interfejsy: List i Set. Interfejs List określa metody pozwalające na przechowywanie obiektów w kontenerze w określonym porządku. Ten interfejs jest implementowany przez dwie klasy: ArrayList, która pozwala na szybki dostęp do przechowywanych obiektów, ale operacje wstawiania i usuwania obiektów dla tego kontenera s ą stosunkowo wolne. Klasa LinkedList pozwala stosunkowo szybko wstawia ć i usuwa ć elementy z kontenera, natomiast przeglądanie zgromadzonych w niej obiektów jest stosunkowo wolne. Na bazie LinkedList można budowa ć stosu i kolejki. Oto przykład ilustrujący użycie tych dwóch klas: class Element { private int value; return new Integer(value).toString(); public Element(int x) { value=x; W kontenerach klasy LinkedList i ArrayList będziemy umieszczać obiekty klasy Element. Klasa ta posiada pole prywatne o nazwie value, konstruktor z parametrem, który inicjalizuje to pole oraz nadpisuje metod ę tostring(). W metodzie main klasy publicznej Listy tworzymy dwa obiekty będące kontenerami, pierwszy klasy ArrayList, a drugi klasy LinkedList. Tworzymy te ż referencj ę, której typ jest określony interfejsem Iterator. Obiekty, których klasy implementuj ą taki interfejs s ą obiektami lekkimi (koszt ich tworzenia jest mały) i pozwalaj ą w uniwersalny sposób porusza ć si ę po obiektach umieszczonych w dowolnym kontenerze. Iterator podstawowy posiada trzy metody: next() - zwraca adres kolejnego obiektu w kontenerze, hasnext() - sprawdza, czy s ą jeszcze jakieś nieodwiedzone obiekty w kontenerze oraz remove() - usuwa obiekt z kontenera. Istniej ą bardziej specjalizowane kontenery dla niektórych rodzajów kontenerów. Pierwsz ą czynności ą wykonywan ą w programie jest dodanie do kontenera klasy ArrayList 20 obiektów klasy Element, które będ ą przechowywa ć wartości całkowite z przedziału od 0 do 19. Następnie zawarto ść całego kontenera jest wypisywana na ekran. Java posiada przeciążone wersje metod print() i 1
public class Listy { ArrayList a = new ArrayList(); LinkedList l = new LinkedList(); Iterator it; a.add(new Element((int)(Math.random()*20))); System.out.println(a); for(int i=0;i<a.size();i++) System.out.print(a.get(i)+" "); it = a.iterator(); while(it.hasnext()) System.out.print(it.next()+" "); it=a.iterator(); while(it.hasnext()) { l.addfirst(it.next()); it.remove(); System.out.println(l); while(!l.isempty()) System.out.print(l.removeLast()+" "); println(), które pozwalaj ą bezpośrednio wypisa ć obiekty należące do kontenera (dla każdego takiego obiektu wywoływana jest metoda tostring(), dlatego została ona przeciążona w klasie Element). W kolejnej pętli for zawarto ść kontenera jest ponownie wypisywana na ekran. Tym razem jednak pobieramy z kontenera klasy ArrayList adres każdego z przechowywanych w niej obiektów, za pomoc ą określonej w tej klasie metody get(indeks) i dopiero po jego uzyskaniu przekazujemy ten obiekt do metody println(). Kolejne wiersze programu pokazuj ą w jaki sposób możemy posługiwa ć si ę iteratorem. Najpierw musimy uzyska ć taki iterator wywołując metod ę iterator() kontenera ArrayList. Następnie w pętli while przechodzimy przez kolejne elementy należące do tego kontenera i wypisujemy je na ekran. Następnie ponownie uzyskujemy iterator, przechodzimy nim po obiektach zgromadzonych w kontenerze klasy ArrayList, wstawiamy je za pomoc ą metody addfirst() do kontenera klasy LinkedList i usuwamy z kontenera klasy ArrayList. 1 Na zakończenie wykorzystując wykorzystujemy metody isempty() i removelast() do wypisania na ekran i usunięcia obiektów z kontenera LinkedList. Interfejs Set pozwala przechowywa ć w kontenerze tylko obiekty unikatowe, przy czym unikatowo ść jest określana za pomoc ą metody equals(), która domyślnie (tzn. wtedy, kiedy jej nie przeciążymy) porównuje adresy obiektów. Interfejs Set implementuj ą dwie klasy HashSet i TreeSet. Pierwsza pozwala szybko znale źć obiekt w kontenerze, bowiem wykorzystuje technik ę haszowania. Decydujące znaczenie dla tej techniki ma metoda hashcode(), która jest definiowana w klasie Object. Jeśli chcemy wpłyn ąć na wyszukiwanie obiektów przechowywanych w kontenerze typu HashSet, to możemy w klasie tych obiektów nadpisa ć t ę metod ę. Kontener TreeSet przechowuje elementy w sposób uporządkowany hierarchicznie. Jego działanie opiera si ę o struktur ę drzewa. Oto przykład ilustrujący użycie takich kontenerów: Podobnie jak w poprzednim przykładzie klasa Element jest klasą obiektów, które będ ą przechowywane w kontenerze, jednakże tym razem jest ona inaczej zdefiniowana. Pierwsz ą rzecz ą, jaką można zauważy ć jest to, że implementuje ona interfejs class Element implements Comparable { Comparable. W rzeczywistości klasy wszystkich obiektów przechowywanych w kontenerach typu Set muszą private int value; implementowa ć taki interfejs, gdy ż implementacja tego interfejsu pozwala na uporządkowanie elementów wewnątrz tego zbioru. Z interfejsu Comparable pochodzi metoda compareto(). Zadaniem tej metody jest zwrócenie wartości -1 jeśli obiekt z którym porównuje si ę obiekt bieżący jest mniejszy, 0 jeśli return new Integer(value).toString(); obiekty s ą równe i 1 jeśli obiekt porównywany jest większy od obiektu bieżącego. W przypadku klasy Element czynnikiem decydującym o wartości tej relacji jest zawarto ść pola value. We wspomnianej klasie nadpisywana jest te ż metoda equals(), która 1 Należy wiedzie ć, że iteratory s ą jednorazowego użytku, tzn. po przejściu przez wszystkie obiekty zgromadzone w kontenerze nie możemy zawróci ć i ponownie przej ść przez te obiekty za pomoc ą tego samego iteratora, nawet jeśli tych obiektów nie usuwaliśmy. Jeśli nadal chcemy używa ć iteratora, to musimy uzyska ć z kontenera kolejny iterator. 2
public Element(int x) { value=x; public boolean equals(object obj) { return (obj instanceof Element) && (value == ((Element)obj).getValue()); public int hashcode() {return getvalue(); public int compareto(object obj) { int i = ((Element)obj).getValue(); return (i<value?-1:(i==value?0:1)); pobiera referencj ę do obiektu porównywanego, sprawdza, czy jest on klasy Equals za pomocą instrukcji instanceof 2, a następnie porównuje pola value bieżącego i przekazanego obiektu. Została równie ż zmieniona metoda hashcode(), która jako unikatow ą warto ść skrótu zwraca zawarto ść pola value. Oznacza to, że nie będzie można doda ć do kontenera dwóch obiektów o takiej samej wartości pola value. Klasa Element zawiera równie ż metod ę getvalue(), która zwraca zawarto ść pola value. M metodzie main klasy publicznej Zbiory stworzono dwa obiekty, jeden klasy HashSet, a drugi klasy TreeSet. W pętlach for dodawanych jest 20 obiektów klasy Element najpierw do pierwszego kontenera, następnie do drugiego. Na zakończenie zawarto ść tych kontenerów jest wypisywana na ekran za pomoc ą przeciążonej wersji metody println. private int getvalue() { return value; public class Zbiory { HashSet h = new HashSet(); TreeSet t = new TreeSet(); h.add(new Element((int)(Math.random()*20))); t.add(new Element((int)(Math.random()*20))); System.out.println(h); System.out.println(t); Interfejs Map deklaruje między innymi następujące metody: put(object key, Object value) umieszcza w kontenerze par ę obiektów klucz ( key) warto ść ( value), Object get(object key) zwraca na podstawie obiektu klucza obiekt warto ść, boolean containskey(object key) sprawdza, czy dany obiekt klucz zosta ł umieszczony w kontenerze, boolean containsvalue(object value) sprawdza, czy dany obiekt warto ść zosta ł umieszczony w kontenerze. Z opisu powyższych metod łatwo wywnioskowa ć, że kontenery implementujące interfejs Map działaj ą na zasadzie struktury asocjacyjnej, która kojarzy obiekt z innym obiektem, dlatego te ż te kontenery nazywane s ą odwzorowaniami. Interfejs Map jest implementowany przez dwie klasy: HashMap i TreeMap. Kontenery pierwszej klasy zapewniaj ą wstawianie i znajdywanie par obiektów w stałym czasie. Działanie kontenerów klasy TreeMap opiera si ę o struktur ę drzewa czerwono czarnego i przechowuje obiekty w sposób uporządkowany. Obiekty umieszczone w tym ostatnim kontenerze musz ą implementowa ć interfejs Comparable lub Comparator. Oto przykład ilustrujący użycie kontenera klasy HashMap: 2 Ta instrukcja jest części ą mechanizmu RTTI Javy, który pozwala określi ć rzeczywist ą klas ę obiektu w czasie wykonania programu. 3
class Klucz { private int klucz; public Klucz(int k) { klucz=k; public int getklucz() { return klucz; return "Klucz: " + new Integer(klucz); class Wartosc { private int wartosc; public Wartosc(int w) { wartosc=w; public int getwartosc() { return wartosc; return "Warto ść: " + new Integer(wartosc).toString(); public class Odwzorowanie { HashMap hm = new HashMap(); W programie mamy zdefiniowane trzy klasy. Klasa Klucz posiada pole prywatne klucz, konstruktor z parametrem, metod ę getklucz, która zwraca warto ść pola klucz oraz nadpisan ą metodę tostring(). Klasa Wartosc jest zbudowana podobnie (inne są tylko nazwy metoda i pól, ale działanie ich jest takie samo). W klasie publicznej Odwzorowanie, w metodzie main tworzony jest obiekt klasy HashMap, a następnie w pętli for umieszczanych jest w nim pi ęć par obiektów, przy pomocy metody put(). W kolejnych wierszach wypisywana jest na ekran zawarto ść tego kontenera przy pomocy przeciążonej wersji metody println(). Na koniec zwracany jest zbiór obiektów kluczy i jest wypisywany na ekran przy pomocy iteratora. for(int i=0; i<5; i++) hm.put(new Klucz((int)(Math.random()*25)), new Wartosc((int)(Math.random()*25))); System.out.println(hm); Set ks; ks = hm.keyset(); Iterator it = ks.iterator(); while(it.hasnext()) System.out.print(hm.get(it.next())+", "); System.out.println("\n"); System.out.println(ks); 4
2. Uwagi końcowe Stosując kontenery należy pamięta ć o tym, że mog ą one przechowywa ć tylko obiekty. Zmienne typów prostych należy opakowa ć w obiekty odpowiadające klas ą ich typowi, np.: zmienn ą typu int należy opakowa ć w obiekt klasy Integer 3. Korzystając z kontenerów należy pamięta ć, że ich klasy znajduj ą si ę w pakiecie java.util i że ten pakiet trzeba zaimportowa ć. 3 W Javie 5 ten problem zosta ł troch ę inaczej rozwiązany. 5