Programowanie Obiektowe (Java) Wyk ad ósmy ł 1. Wprowadzenie

Transkrypt

Programowanie Obiektowe (Java) Wyk ad ósmy ł 1. Wprowadzenie
Programowanie Obiektowe (Java)
Wykład ósmy
1.
Wprowadzenie do kontenerów
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)
nie powiedzie,
jest metodą opcjonalną, służy do dodawania obiektu do kontenera, zwraca wartość false jeśli ta operacja się
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
kontenera,
iteratora, pozwalającego „poruszać się” po obiektach wchodzących w skład
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)
metodą opcjonalną,
usuwa wszystkie obiekty, z kontenera, które należą do przekazanej jej przez parametr kolekcji, jest
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:
import java.util.*;
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
class Element {
private int value;
public String toString() {
return new Integer(value).toString();
}
public Element(int x) {
value=x;
1
Programowanie Obiektowe (Java)
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.
}
}
public class Listy {
public static void main(String[] args) {
ArrayList a = new ArrayList();
LinkedList l = new LinkedList();
Iterator it;
for(int i=0;i<20;i++)
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)+" ");
System.out.println();
it = a.iterator();
while(it.hasNext())
System.out.print(it.next()+" ");
System.out.println();
it=a.iterator();
while(it.hasNext()) {
l.addFirst(it.next());
it.remove();
}
System.out.println(l);
while(!l.isEmpty())
System.out.print(l.removeLast()+" ");
System.out.println();
}
}
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ą
import java.util.*;
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().
public String toString() {
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
Programowanie Obiektowe (Java)
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();
pobiera referencję do obiektu porównywanego,
sprawdza, czy jest on klasy Equals za pomocą
instrukcji instanceof2, 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.
return (i<value?-1:(i==value?0:1));
}
private int getValue() {
return value;
}
}
public class Zbiory {
public static void main(String[] args) {
HashSet h = new HashSet();
TreeSet t = new TreeSet();
for(int i=0;i<20;i++)
h.add(new Element((int)(Math.random()*20)));
for(int i=0;i<20;i++)
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)
Object get(Object key)
umieszcza w kontenerze parę obiektów klucz (key)
zwraca na podstawie obiektu
boolean containsKey(Object key)
klucza obiekt
sprawdza, czy dany obiekt
boolean containsValue(Object value)
wartość (value),
wartość,
klucz został umieszczony w kontenerze,
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
Programowanie Obiektowe (Java)
import java.util.*;
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.
class Klucz {
private int klucz;
public Klucz(int k) {
klucz=k;
}
public int getKlucz() {
return klucz;
}
public String toString() {
return "Klucz: " + new Integer(klucz);
}
}
class Wartosc {
private int wartosc;
public Wartosc(int w) {
wartosc=w;
}
public int getWartosc() {
return wartosc;
}
public String toString() {
return "Wartość: " + new Integer(wartosc).toString();
}
}
public class Odwzorowanie {
public static void main(String[] args) {
HashMap hm = new HashMap();
for(int i=0; i<5; i++)
hm.put(new Klucz((int)(Math.random()*25)), new Wartosc((int)(Math.random()*25)));
System.out.println();
System.out.println(hm);
System.out.println();
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
Programowanie Obiektowe (Java)
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 Integer3. 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