Java-Kolekcije i iteratori (1) Pripremio Atila Rafai
Ako ne znate koliko ćete objekata koristiti da biste rešili neki problem ili postoji mogućnost da će im broj biti promenljiv, onda postoji i problem njihovog smeštanja u memoriju. U programskom jeziku Java rešenje tog problema predstavljaju kolekcije - objekti koji se koriste za smeštanje i manipulaciju nedefinisanim brojem objekata. Sve što je potrebno, s gledišta dizajna korisni čkog programa, jeste niz objekata koji ima promenljivu veličinu i metode koje omogućavaju da se manipuliše tim objektima. Ali ima dobrih razloga za postojanje više tipova kolekcija. Kao prvo, različite vrste kolekcija treba da imaju različite načine na koji se može manipulisati podacima koje sadrže. Kao drugo, različite vrste kolekcija imaju različite nivoe efikasnosti odrađivanja različitih operacija. (Na primer, ne će se na isti na čin umetnuti novi element u sortiranu ili običnu listu.) Razrađeni objektno orijentisani programski jezici sadrže paket klasa koji služe za rešenje problema kolekcija. U jeziku C++ to je STL (Standard Template Library). Pošto je Java nastala kao jezik koji je trebalo da zameni C++ u odre đenoj klasi problema, pa joj je jezik C++ bio po četni uzor (a ona je kasnije narasla do sadašnjih mogućnosti), i u njoj se nalazi grupa takvih klasa. Java je podržavala kolekcije već od verzije 1.0; ta je podrška unekoliko bila poboljšana u verziji 1.1, ali se tek u verziji 2.0 došlo do obuhvatnije implementacije kolekcija. Osnovne klase koje se koriste za kreiranje kolekcija tretiraju sve objekte koje mogu da sadrže kao da nemaju specifikovan tip, ta čnije, bazirane su na pretpostavci da sadrže objekte tipa Object. Pošto sve klase u Javi imaju na po četku svoje hijerarhije nasleđivanja klasu Object, to i ne predstavlja neki problem. Za primitivne tipove podataka (byte, char, int, float, ...) postoje klase koje se mogu koristiti umesto njih, tako da ovakvo rešenje ima dovoljnu širinu. Problem nastaje pri proveri tipa objekta koji neka kolekcija može da sadrži. Ako kreirate neki niz ili matricu promenljivih imate mogućnost provere tipova podataka koje pokušavate da upišete. To znači da za vreme pisanja programa kompajler može proveriti da li ste pogrešili prilikom pisanja programskog koda. U slučaju kolekcija to nije moguće, pa ostaje samo neprijatna mogu ćnost da korisnik vašeg programa tokom rada dobije poruku o neuspešnom pokušaju izvršenja nelegalne instrukcije od strane JVM-a (Java Virtual Machine), čime se prekida i rad programa. U praksi, pišući program vi kreirate svoju kolekciju i punite je po svojim potrebama nekim svojim objektima. Prilikom upisa objekta u kolekciju ona prima vaš objekat kao da je tipa Object, što zna či da ga tretira kao da je primerak neke opštije klase (upcasting), pa se može reći da on gubi svoj identitet. Kasnije koristite te iste objekte, ali prilikom preuzimanja iz kolekcije ne možete sa sigurnoš ću tvrditi kog su tipa ti objekti. Pre korišćenja ovih objekata ponovo ih vraćate u prvobitni tip (downcasting) kako bi ste mogli da koristite kor istite metode svojstvene toj klasi objekata. Ako je oblik (klasa) u koji vra ćate objekat pogrešan, dobijate poruku o grešci:
U ovom primeru paprika ne predstavlja problem u slu čaju kreiranja torte od banana. Problem se javlja tek prilikom korišćenja torte, jer Java prijavljuje grešku:
Downcasting nije potreban samo za objekte tipa String, jer kadgod kompajler očekuje objekat tipa String, a ne dobije takav, on automatski koristi metod toString(), koji je definisan u klasi Object (i može se redefinisati), a koji vra ća naziv klase čiji je primerak taj objekat, pra ćen znakom '@' i heksadecimalnim oblikom hash koda tog objekta. U slu čaju da želite da iskoristite nešto od te kombinacije:
Kolekcije i iteratori (2) U Javi se framework za kolekcije (skup klasa koje omogućavaju osnovu za dodavanje naprednije funkcionalnosti) funkcionalnosti) sastoji od više interfejsa i apstraktnih klasa koje su kreirane za implementaciju osnovnih metoda i proširivanje osnovnih tehnika, kao što je tehnika korišćenja iteratora.
Interfejsi Postoje dva osnovna interfejsa za kolekcije: Collection i Map. Ti interfejsi definišu kakav treba da bude interfejs svake kolekcije i time definišu koje osnovne funkcije treba da postoje.
Interfejsi kolekcija Interfejs Collection navodi (između ostalih) osnovne metode kojima se zahteva da se definišu (implementiraju) osnovne operacije koje treba da postoje nad kolekcijom: dodavanje elementa, provera da li neki element već postoji u kolekciji, brisanje elementa iz kolekcije, način na koji se može redom pristupiti svim elementima kolekcije, kao i da se može proveriti koliko trenutno ima elemenata u nekoj kolekciji: boolean add(Object element) boolean contains(Object element) boolean remove(Object element) Iterator iterator() int size()
Kao proširenja interfejsa Collection postoje kolekcije List i Set, koje postavljaju dodatne zahteve. Interfejs List uvodi uređenu kolekciju, pa samim tim postoje i dodatni zahtevi da se element može uneti na odgovarajuću poziciju unutar postojeće liste, preuzeti na osnovu indeksa u toj listi ili obrisati element koji ima dati indeks: void add(int index, Object element)
1
Object get(int index) void remove(int index)
Za kretanje napred/nazad kroz listu ovaj interfejs koristi drugi interfejs, ListIterator, koji predstavlja proširenje interfejsa Iterator. Interfejs Set je veoma sličan interfejsu Collections; on navodi samo jedno dodatno
ograničenje: ne može biti duplikata unutar seta objekata. Zbog toga se moraju definisati dve dodatne metode, equals i hashCode, koje treba da omoguće testiranje dva objekta na jednakost i kreiranje heš koda koji će omogućiti da objekti koji imaju istu vrednost ne budu proglašeni za različite, što je osnovna postavka još iz klase Object (tu se koristi adresa objekta kao heš kod). Kod interfejsa Map postoji nešto drugačiji zahtev: mora postojati mogućnost kreiranja "rečnika" koji će imati parove ključ/vrednost. Preciznije rečeno, mora postojati mogućnost da se neki unos u mapu može kraće opisati, čime se omogućava brža pretraga mape: Object put(Object key, Object value) boolean containsKey(Object key) boolean containsValue(Object value) Object get(Object key) Object remove(Object key) int size()
Na primer, možete kreirati adresar koji će sadržati detaljne podatke o osobama s kojima ste u kontaktu, ali ćete taj adresar pretraživati samo na osnovu prezimena i imena, umesto da gubite vreme u pretraživanju daleko veće količine podataka.
Osnovne klase Da bi se programerima olakšao rad, postoji pet apstraktnih klasa koje implementiraju one metode koje predstavljaju samu osnovu: AbstractCollection AbstractList AbstractSequentialList AbstractSet AbstractMap
Ako želite da implementirate svoju kolekciju, verovatno ćete iskoristiti neku od ovih klasa. Pored te mogućnosti, Java u biblioteci ima šest konkretnih klasa: LinkedList ArrayList HashSet TreeSet HashMap TreeMap
2
Ako smatrate da vam to nije dovoljno, postoje i klase koje su postojale u Java biblioteci i pre nastanka ovog naprednijeg sistema interfejsa i klasa (engl. legacy container classes), a koje na svoj način prilaze problemu kolekcija, pa su, sa nekim modifikacijama, modifikacijama, uključene i u ovaj napredniji sistem: Vector Stack Hashtable Properties
Iteratori Sve kolekcije imaju neki način da prime neki objekat ili da predaju svoj sadržaj. Uvek postoje metode push ili add, ili neke druge sličnog naziva. U skladu s tim da uvek postoji mogućnost da se u toku projektovanja programa dođe do nekog rešenja koje zahteva promenu vrste kolekcije, kreiran je i univerzalan način pristupa elementima kolekcije - iterator. To je objekt čija je namena omogućavanje sekvencijalnog pristupa elementima. Na samom startu Java je imala standardni iterator, Enumeration, koji su koristile sve klase kolekcije. U verziji 2 Jave dodata je kompleksnija biblioteka kolekcija, kolekcija, pa je zbog novih potreba dodat i nov iterator, Iterator, koji ima dodatne osobine (moguće je brisanje elemenata preko iteratora) i kraće nazive za metode koje se koriste za pristup elementima. Iterator interfejs ima dva metoda koji su analogni onima u interfejsu Enumerator; to su hasNext (za stariju verziju hasMoreElements) i next (analogno sa nextElement u Enumeratoru). Na primer, ukoliko bi imali neku kolekciju koja bi sadržala podatke za sve zaposlene u jednom preduzeću, preuzimanje tih podataka bi izgledalo kao: Iterator i = zaposleni.iterator(); while (i.hasNext()) { Zaposleni z = (Zaposleni)i.next(); ... }
Povremeno ćete naići na neki metod koji vuče korene još iz verzije 1.0, a koji očekuje, kao parametar, enumerator. Statička metoda Collections.enumeration koristi se za kreiranje enumerator objekta za datu kolekciju. Na primer: // niz ulaznih tokova ArrayList streams = ...; // konstruktor SequenceInputStream objekta kao parametar prima enumerator SequenceInputStream in = new SequenceInputStream( Collections.enumeration(streams) );
Kao primer korišćenja iteratora uzećemo prepravljen primer iz prethodnog dela: // program torta.java import java.util.*; class Banana { } public class torta { public static void main(String[] args)
3
{ ArrayList tortaOdBanana = new ArrayList(); for (int i = 0; i < 7; i++) tortaOdBanana.add(new Banana()); Iterator i = tortaOdBanana.iterator(); while (i.hasNext()) System.out.println(((Banana)i.next()).toString()); } }
Možete videti da se jedina bitna izmena nalazi samo u nekoliko poslednjih linija. Umesto da koristite liniju for (int i = 0; i < tortaOdBanana.size(); i++) System.out.println(((Banana)tortaOdBanana.get(i)).toString());
koristi se Iterator za prolazak kroz niz elemenata Iterator i = tortaOdBanana.iterator(); while(i.hasNext()) System.out.println(((Banana)i.next()).toString());
Korišćenjem iteratora programer ne mora da vodi računa o broju elemenata u kolekciji. To se postiže metodama hasNext() i next().
java.util.Enumeration boolean hasMoreElements() Object nextElement()
Vraća true ako ima još nepregledanih elemenata u kolekciji. Vraća sledeći element kolekcije. Greška je ako se pozove ovaj metod, a da je pre toga hasMoreElements() vratio false.
java.util.Iterator boolean hasNext() Object next() void remove()
Vraća true ako ima još nepregledanih elemenata u kolekciji. Vraća sledeći element kolekcije. Uklanja iz kolekcije poslednji član koji je bio vraćen metodom next().
Korišćenje listi List
ArrayList
Redosled je najvažnija osobina interfejsa List; osigurava se održavanje elemenata u određenom redosledu. List dodaje nekoliko metoda interfejsu Collection, koje omogućavaju umetanje i brisanje elemenata iz sredine liste. (Ovo se preporučuje samo za klasu LinkedList) Interfejs List omogućava kreiranje ListIterator objekta koji omogućava kretanje kroz listu u oba smera, kao i umetanje i brisanje elemenata iz sredine liste (preporučuje se samo za LinkedList). Implementacija interfejsa List koja sadrži niz Object[] koji dinamički realocira. Koristi se umesto prethodne varijante (Vector) kao kolekcija za opšte potrebe. Omogućava brz pristup elementima, ali je sporo umetanje
4
LinkedList
i brisanje elemenata iz sredine liste. ListIterator koristi se samo za prolazak kroz listu, ali ne i za umetanje i brisanje elemenata, jer je za to pogodnije koristiti drugu kolekciju - LinkedList. Pruža optimalan sekvencijalni pristup i najoptimalnija umetanja i brisanja iz sredine liste. Relativno spora implementacija za direktni pristup nekom elementu liste koji nije naredni, za šta je pogodnije koristiti ArrayList. Takođe poseduje metode addFirst, addLast, getFirst, getLast, removeFirst, i removeLast (koje nisu definisane ni u jednom interfejsu ili osnovnoj klasi), što omogućava korišćenje objekta tipa LinkedList, kao da je stek ili red.
ArrayList i Vector Klase ArrayList i Vector koriste se kao kolekcije opšte namene. Obe klase imaju neki svoj inicijalni kapacitet koji se povećava svaki put kada treba dodati novi element u već popunjenu kolekciju. Operacija povećanja kapaciteta vrlo je zahtevna, jer se kreira nova kolekcija koja sadrži sve elemente koji već postoje u datoj kolekciji, s tim da se ostavlja prazan prostor za eventualne nove elemente liste, a postojeća kolekcija se potom briše iz memorije. Kod klase Vector programer ima veću slobodu prilikom rada, jer može specifikovati inkrement veličine kolekcije u slučaju popunjenosti liste. Sve metode klase Vector su sinhronizovane, što znači da možete pristupiti objektu tipa Vector iz dve ili više niti. To znači mnogo kad je u pitanju sigurnost podataka, ali u većini slučajeva je ta mogućnost potpuno nepotrebna, a u domenu brzine izvršavanja programa predstavlja samo usporenje. Suprotno metodama te klase, ArrayList metode nisu sinhronizovane, pa su samim tim i brže. Korišćenje klase ArrayList umesto klase Vector vrlo je jednostavno, jer je jedino potrebno koristiti metode kraćih naziva, get i set, umesto metoda elementAt i setElementAt klase Vector.
LinkedList Za razliku od ArrayList klase, klasa LinkedList, iako ima metod get, nema nikakav način da optimizuje vreme sekvencijalnog pristupa elementima tom metodom. Sledeći programski kôd je najsporiji način sekvencijalnog pristupa elementima LinkedList objekta: for (int i = 0; i < list.size(); i++) // uradi neku operaciju koristeci list.get(i);
Svaki put kada se pristupa sledećem elementu (po indeksu) potraga počinje od početka liste, jer LinkedList klasa nije kreirana s namerom da se zapamti trenutna pozicija unutar liste. Ono što jedino čini neku optimizaciju jeste činjenica da, ako je indeks veći od polovine broja elemenata u listi, pretraga počinje s kraja liste.
5
Korišćenjem iteratora ListIterator, razlika u brzini prolaska kroz listu je vrlo očigledna. Iterator vodi računa o trenutnoj poziciji. Tačnije rečeno, Java iteratori u suštini pokazuju između1 dva elementa: nextIndex metod vraća indeks elementa koji bi bio dostupan narednim pozivom metode next; metod previousIndex vraća indeks elementa koji bi bio dostupan narednim pozivom metode previous. Jedini razlog korišćenja povezane liste jeste minimizacija korišćenog vremena i resursa za operacije umetanja i brisanja elemenata iz sredine liste. U slučaju da je broj elemenata mali, bolje je koristiti objekat tipa ArrayList.
Dvostruko povezana lista
Dodavanje elementa u povezanu listu
1
???
6
Uklanjanje elementa iz povezane liste
java.util.Vector Enumeration elements()
Vraća Enumeration objekat za prolazak kroz elemente vektora.
java.util.List ListIterator listIterator() ListIterator listIterator(int index)
void add(int i, Object element) void addAll(int i, Collection elements)
Object remove(int i)
Object set(int i, Object element) int indexOf(Object element)
Vraća ListIterator za prolazak kroz listu. Vraća ListIterator za prolazak kroz listu, pri čemu će prvi poziv metode next dati element koji ima indeks kao onaj naveden kao parametar ove metode. Dodaje element na specifikovanu poziciju. Dodaje sve elemente iz kolekcije u listu, a indeks prvog elementa koji treba da se unese biće i (ostali slede iza). Briše element iz liste (sa navedenim indeksom) i daje taj element kao povratnu vrednost. Zamenjuje postojeći element sa datim indeksom novim elementom. Vraća indeks prvog objekta u listi, koji je jednak objektu datom kao parametar, a ako nema takvog elementa, vraća -1.
java.util.ListIterator void add(Object element) void set(Object element)
Dodaje element ispred trenutne pozicije. Zamenjuje poslednji element kome je bilo pristupljeno sa next ili previous. Kreira izuzetak IllegalStateException ako je struktura liste bila modifikovana po poslednjem pozivu
7
boolean hasPrevious() Object previous() int nextIndex() int previousIndex()
metoda next ili previous. Vraća true ako postoji makar još jedan element do kraja liste. Vraća prethodni element. Kreira izuzetak NoSuchElementException ako je dostignut početak liste. Vraća indeks elementa koji bi bio vraćen sledećim pozivom metode next. Vraća indeks elementa koji bi bio vraćen sledećim pozivom metode previous.
java.util.LinkedList LinkedList() LinkedList(Collection elements) void addFirst(Object element) void addLast(Object element) Object getFirst() Object getLast() Object removeFirst() Object removeLast()
Kreira praznu listu. Kreira listu i dodaje sve elemente iz kolekcije. Dodaje element na početak liste. Dodaje element na kraj liste. Vraća element sa početka liste. Vraća element sa kraja liste. Skida iz liste2 i vraća element koji je na početku liste. Skida iz liste3 i vraća element koji je na kraju liste.
Atila Rafai
2
šta? element? šta? element?
3
8
Kolekcije i iteratori (3) Korišćenje setova Interfejs Set ima iste metode kao i Collection, tačnije, ne dodaje nove metode, već je ponašanje nekih metoda malo drugačije: Set ne dozvoljava duplikate. Ako ste neki objekat postavili u Set, ne možete dodati još jedan objekat koji ima iste vrednosti.
Heš tabele Liste omogućavaju programeru da odredi u kom redosledu će se nalaziti objekti unutar tih kolekcija. Ali ako se traži element kome se ne zna tačna pozicija, moraju se redom pregledavati elementi dok se ne naiđe baš taj element. Ako je kolekcija velika, to može biti vremenski vrlo zahtevna operacija. S druge strane, ako nije bitno kako se zapisuju elementi u kolekciju, već je bitna samo brzina pristupa, tada se koriste druge vrste kolekcija - heš tabele. Heš tabela izračunava celobrojnu vrednost zvanu heš kôd , koju pridružuje pojedinačnom elementu kolekcije. Za heš kôd je bitno da se može brzo izračunati i da zavisi samo od elementa za koji se kreira, a ne i od ostalih elemenata u heš tabeli. Heš tabela je implementirana kao niz ulančanih lista zvanih bucket (u bukvalnom prevodu 'vedro'). Da bi se našao objekat u heš tabeli, potrebno je izračunati njegov heš kod, i korišćenjem ukupnog broja bucketa se, preko modula, nalazi indeks bucketa u kome se nalazi taj element. Kada se, tim brzim putem, "zahvati" veći broj elemenata (kao voda vedrom), naredna operacija je redno pregledavanje elemenata u listi da bi se pronašao odgovarajući element. Na primer, ako objekat ima heš kod 531, a ima 101 bucket, tada će on biti u 26-om, jer je ostatak od celobrojnog deljenja 531 sa 101 baš 26. Može se desiti da je u bucketu samo taj element, ali se može desiti i da je tamo još neki; tada se kaže da je došlo do heš kolizije . U tom slučaju mora se pristupiti poređenju sa možda i svim elementima koji se nalaze u bucketu. Ako je algoritam heš koda dovoljno dobar, a broj bucketa dovoljno veliki, tada je neophodno da se izvrši samo nekoliko poređenja, pa je samim tim potrebno malo vremena da se dođe do željenog elementa tabele. Ako je pak tabela više popunjena, povećava se broj kolizija, a samim tim se usporava rad nad heš tabelom. Ako je poznato koliko će biti otprilike elemenata u tabeli, može se specifikovati inicijalni kapacitet. Ta vrednost bi trebalo da bude otprilike 150% broja elemenata. Pošto se po nekim ispitivanjima pokazalo da je za inicijalni broj bucketa poželjno odabrati prost broj (onaj koji je deljiv samo sa samim sobom), ako imamo potrebu za oko 100 ulaza, inicijalna veličina bi trebalo da bude 151 bucket.
Ako se želi veća kontrola nad performansama heš tabele, dolazi do problema u slučaju kada inicijalni kapacitet nije moguće odrediti. Tada se, ako se početni kapacitet postavi na nedovoljnu vrednost, javlja potreba za tzv. rehashing operacijom, tj. kreiranjem nove tabele sa odgovarajućom veličinom, njenim punjenjem objektimavrednostima iz 'stare' tabele i brisanjem iz memorije stare tabele. To je vremenski zahtevna operacija, pa se mora uvesti kompromisno rešenje: specifikuje se load factor. U programskom jeziku Java to je vrednost kojom se određuje kolika treba da bude iskorišćenost bucketa da bi se pristupilo reheširanju. Na primer, ako load factor ima vrednost 0.75 (što je u Javi podrazumevana vrednost), tabela se automatski rehešira na duplo veću tabelu kada ukupni broj iskorišćenih bucketa pređe 75% inicijalne vrednosti.
HashSet HashSet je klasa iz Java biblioteke kolekcija koja implementira set pomoću heš
tabele. Njen podrazumevani konstruktor kreira heš tabelu koja ima inicijalno 101 bucket, a load factor je 0.75. Set je kolekcija elemenata bez duplikata, tako da se metod add() koristi za pokušaj upisa novog objekta. Metod contains() redefinisan je tako da na postojanje određenog elementa proverava samo odgovarajući bucket. Iterator heš seta postoji za prolazak kroz celu tabelu. Navedeni program, koji je primer korišćenja heš seta, učitava sve reči sa svog ulaza, upisuje ih u heš tabelu i, na kraju, ispisuje niz preuzetih reči te ukupan broj nađenih reči:
import java.util.*; import java.io.*; public class HashSetTest { public static void main(String[] args) { Set words = new HashSet(); try { BufferedReader in = new BufferedReader( new InputStreamReader( new FileInputStream( new File("HashSetTest.java") ))); String line; while ((line = in.readLine()) != null) { StringTokenizer tokenizer = new StringTokenizer(line); while (tokenizer.hasMoreTokens()) { String word = tokenizer.nextToken(); words.add(word); } } }
catch (IOException e) { System.out.println("Error " + e); } Iterator iter = words.iterator(); while (iter.hasNext()) System.out.println(iter.next()); System.out.println(words.size() + " razlicitih reci."); } }
U ovom primeru su se mogli koristiti objekti tipa String, jer ta klasa ima hashCode() metod koji računa heš kôd za niz znakova. U slučaju objekta String to je integer koji je izveden iz samih znakova. U slučaju ostalih klasa, kompajler neće prijaviti grešku ako upisujete neki objekat u heš tabelu, a da niste definisali metodu hashCode() , i to zato što je ta metoda definisana u klasi Object. Problem s ovom implementacijom metode predstavlja predstavlja to što ona vrednost tipa integer (za koju je dozvoljeno da je negativna) izvodi iz memorijske adrese objekta. To znači da u opštem slučaju ova metoda za svaki objekat daje različit heš kod, nezavisno od toga da li su vrednosti neka dva objekta identične. Zbog toga je potrebno redefinisati metod za svaku klasu koja bi mogla biti ikada ubačena u heš tabelu. Pri tome treba voditi računa da to odslikava sadržaj objekta. Na primer, u slučaju da se u heš tabelu ubacuju objekti koji predstavljaju zaposlene u nekoj firmi, treba koristiti ili njihovu šifru u bazi podataka te firme ili njihov matični broj. Osim redefinisanja metode hashCode() potrebno je redefinisati i metodu equals(). Ta metoda je takođe definisana u klasi Object, ali ni ta implementacija ne odgovara ovom zahtevu. Napomena: potrebno je uskladiti te dve metode — ako poziv metode x.equals(y) vrati true, tada i vrednosti koje se dobijaju pozivima metoda x.hashCode() i y.hashCode() moraju biti jednake.
TreeSet Ova klasa je slična HashSet klasi, osim što ima dodato jedno poboljšanje: objekti su sortirani. Svaki put kada se neki element doda u ovu kolekciju, postavlja se na odgovarajuće mesto. Podrazumeva se da objekat koji se postavlja u kolekciju ima implementiran interfejs Comparable . Taj interfejs deklariše samo jedan metod: int compareTo(Object other)
Poziv metode x.compareTo(y) mora vratiti 0 ako su dva objekta jednaka, negativnu vrednost ako je x pre objekta y u tom načinu sortiranja, a pozitivnu vrednost u suprotnom slučaju. Tačne vrednosti nisu bitne, bitan je samo znak vrednosti koja se vraća pozivom te metode. Java implementira tu metodu za neke svoje osnovne klase, tako da klasa String ima implementiranu metodu compareTo() , a za sortiranje se koristi takozvani leksikografski redosled, tj. redosled po azbučnom rasporedu slova.
Postoji jedan problem u implementiranju te metode. U slučaju da se neki objekti, shodno različitim situacijama, žele sortirati po različitim kriterijumima, ovakvim pristupom problemu to nije moguće. Zbog ovakvih problema, a i zbog verovatnoće da neka klasa nema implementiranu metodu compateTo(), moguće je koristiti konstruktor klase TreeSet koji kao parametar prima objekat tipa Comparator. Interfejs Comparator ima deklarisanu samo jednu metodu: int compare(Object x, Object y)
Kao i metoda compareTo() , ova metoda treba da vraća nulu, negativnu ili pozitivnu celobrojnu vrednost, u zavisnosti od vrednosti objekata koje prima kao parametre. Dodavanje nekog objekta u ovakvu kolekciju nešto je sporije od dodavanja u HashSet objekat, ali je još uvek brže od dodavanja elementa u sredinu nekog niza ili ulančane liste. Na primer, ako ovakva kolekcija ima n elemenata, tada je prosečno potrebno log2n poređenja da bi se našla odgovarajuća pozicija za novi element. Na primer, ako je u kolekciji 1.000 elemenata, za dodavanje novog elementa je potrebno oko 10 poređenja. Ovo je prerađen prethodni primer, tako da kao izlaz daje sortirani niz tokena koje prima kao ulaz. Ovde je prikazana implementacija implementacija korišćenja objekta tipa Comparator da bi se naveo svoj redosled sortiranja: import java.util.*; import java.io.*; public class TreeSetTest { public static void main(String[] args) { Set words = new TreeSet(new Comparator() { public int compare(Object x, Object y) { String sx = (String) x; String sy = (String) y; int z = sx.compareToIgnoreCase(sy); return -z; } } } ); try { BufferedReader in = new BufferedReader( new InputStreamReader( new FileInputStream( new File("TreeSetTest.java") ))); String line; while ((line = in.readLine()) != null) { StringTokenizer tokenizer = new StringTokenizer(line); while (tokenizer.hasMoreTokens()) { String word = tokenizer.nextToken(); words.add(word);
} } } catch (IOException e) { System.out.println("Error " + e); } Iterator iter = words.iterator(); while (iter.hasNext()) System.out.println(iter.next()); System.out.println(words.size() + " razlicitih reci."); } }
Pripremio Atila Rafai
Kolekcije i iteratori (4) Korišćenje mapa Apstraktna klasa Dictionary bila je prva implementacija (još iz JDK verzije 1.0) neke klase koja je imala parove objekata klju č-vrednost. Zbog te osobine je i dobila naziv re čnik (engl. dictionary). Klasa HashTable bila je jedina implementacija te klase, a dodavala je heš tabelu kao na čin zapisivanja parova klju č-vrednost. Od JDK verzije 1.1 po čelo se s poboljšanjima poboljšanjima podrške za kolekcije, ali je je tek od verzije 1.2 napravljen radikalan radikalan zaokret. Umesto klase Dictionary, koja je bila potpuno apstraktna, koriste se: interfejs Map, podinterfejs SortedMap, i klase koje implementiraju ovaj interfejs - AbstractMap, HashMap, Hashtable, RenderingHints, WeakHashMap, i Attributes. Klase HashMap i TreeMap (izvedena iz AbstractMap) predstavljaju klase opšte upotrebe.
Interfejs Map Konceptualno, ovaj interfejs definiše kolekciju koja je sli čna vektoru, ali se kolekcija pretražuje, umesto po indeksima, po nekom objektu. Na primer, ako imate re čnik koji treba da sadrži stru čne izraze, kreirate mapu i ona za svaku re č, koja je objekat, sadrži tako đe neki odgovaraju ći objekat. Pri tome važi pravilo da se svi stru čni izrazi pojavljuju samo jednom u rečniku i da oni predstavljaju klju č po kome se nalazi objekat koji predstavlja neku vrednost, što bi u ovom slu čaju bilo objašnjenje izraza. Za rešenje ovakvog problema koristi se baš klasa izvedena iz interfejsa Map. Ovaj interfejs podržava postojanje parova objekata klju č-vrednost, pri čemu ne može postojati duplikat klju čeva. Metode koje deklariše Map jesu: size() – daje informaciju o broju elemenata u mapi, isEmpty() – vraća true ako mapa nema niti jedan element, put(Object key, Object value) – dodaje specificiranu vrednost u mapu pod specifikovanim klju čem, get(Object key) – vraća vrednost pridruženu datom ključu, remove(Object key) – uklanja par klju č-vrednost za datu vrednost klju ča key. Interfejs Map takođe zahteva dva iteratora: keys() za ključeve i elements() za vrednosti. Evo primera kako treba da izgleda klasa koja se izvodi iz klase AbstractMap: import java.util.*; public class TestMap extends AbstractMap { private ArrayList keys = new ArrayList(); private ArrayList values = new ArrayList(); public int size() { return keys.size(); } public boolean isEmpty() { return keys.isEmpty(); } public Object put(Object key, Object value) { int index = keys.indexOf(key); if (index == -1) // nema ga u tabeli { keys.add(key); values.add(value); return null; } else // postoji - zameni ga { Object returnval = values.get(index); values.set(index, value); return returnval;
} } public Object get(Object key) { int index = keys.indexOf(key); if (index == -1) return null; return values.get(index); } public Object remove(Object key) { int index = keys.indexOf(key); if (index == -1) return null; keys.remove(index); Object returnval = values.get(index); values.remove(index); return returnval; } public Set keySet() { return new HashSet(keys); } public Collection values() { return values; } public Set entrySet() { return new HashSet(values); } }
Metod put() proverava prvo da li ve ć postoji odre đeni ključ u mapi. U slu čaju da postoji, zamenjuje staru vrednost novom, dok staru vrednost vra ća kao povratnu vrednost. Time se spre čava da se preko neke ve ć postoje će vrednosti upiše nova, a da se stara vrednost izgubi. Ovaj metod se može modifikovati tako da spre čava zamenu postoje će vrednosti, što bi moglo odgovarati ako se koriš ćenjem ovog primera implementira re čnik. Metod remove() takođe vraća objekat-vrednost (u slu čaju uspešnog brisanja, tako da se može skratiti postupak preuzimanja objekata-vrednosti iz mape – nije potrebno prvo preuzeti neku vrednost koriš ćenjem metode get(), pa je naknadno brisati, već za to može direktno poslužiti metod remove()). java.util.Map (interesantniji (interesantniji metodi): Object get(Object key)
Vraća vrednost pridruženu klju ču. U slučaju da klju č nije nađen u mapi vraća se null. Kao parametar može se dati vrednost null. Object put(Object key, Object Postavlja par klju č-vrednost u mapu. Ako je klju č već prisutan u mapi, novi value) objekat value zamenjuje onaj koji je prethodno bio povezan s tim ključem. Ovaj metod vra ća staru vrednost za dati klju č ili null ako klju č nije bio upisan u mapu. Kao parametre prima klju č koji može biti null i vrednost koja ne može biti null. void putAll(Map entries) Dodaje sve ulaze iz mape koja je predata kao parametar. boolean containsKey(Object key) Vraća true ako dati klju č postoji u mapi. boolean containsValue(Object value) Vraća true ako data vrednost postoji u mapi. Set entrySet() Vraća objekat tipa Set sastavljen od objekata tipa Map.Entry koji sadrže parove ključ-vrednost, ali uz neka ograni čenja: mogu se uklanjati parovi iz seta, i to se odražava na mapu, ali se ne mogu dodavati novi parovi klju čvrednost. Set keySet() Ima identičnu funkciju kao i prethodna metoda, samo što klju čeve prisutne u mapi1. Collection values() Vraća kolekciju koja sadrži samo vrednosti mape. I za ovu kolekciju važe ograničenja iz prethodna dva metoda.
1
šta „samo što ključeve prisutne u mapi“?
java.util.Map.Entry (interesaniji (interesaniji metodi): Object getKey() Object getValue() Object setValue(Object value)
Vraća ključ ovog para. Vraća vrednost ovog para. Menja postoje ću vrednost pridruženu ovom paru, a vra ća vrednost koja je pre ovoga bila u paru.
HashMap Standardna Java biblioteka klasa sadrži dva razli čita tipa mapa, HashMap i TreeMap. Obe klase imaju isti interfejs, ali su različite po pitanju efikasnosti. Ako je potrebno često koristiti metod get(), klasa HashMap omogućava brz pristup elementima, jer koristi heš tabelu. Na taj na čin, brzim prora čunom heš koda može se u ve ćini slučajeva direktno pristupiti traženom elementu. Ako se taj na čin realizacije metoda get() uporedi sa implementacijom istoimene metode u klasi ArrayList, razlika u brzini će biti vrlo o čigledna. Primer korišćenja klase HashMap: import java.util.*; class Counter { int i = 1; public String toString() { return Integer.toString(i); } } class HashMapTest { public static void main(String[] args) { int iterations = Integer.parseInt(args[0]); int distribution = Integer.parseInt(args[1]); HashMap hm = new HashMap(); long time = System.currentTimeMillis(); for (int i = 0; i < iterations; i++) { Integer key = new Integer((int)(Math.random() * distribution)); if (hm.containsKey(key)) ((Counter)hm.get(key)).i++; else hm.put(key, new Counter()); } System.out.print("Obrada trajala "); System.out.print(System.currentTimeMillis() - time); System.out.println(" milisekundi"); System.out.println(hm); } }
U ovom primeru klasa HashMap koristi se za proveru ravnomernosti rasporeda slu čajnih brojeva koji se dobijaju koriš ćenjem statičke metode random() klase Math. Kao parametre, ovaj primer prima dva broja: broj iteracija i raspon u kome će se proveravati distribucija distribucija brojeva. Na kraju modifikacije modifikacije objekta klase HashMap dobija se i izveštaj o vremenskom trajanju radnog dela programa (proteklo vreme u milisekundama). Jedan od test rezultata imao je slede će vrednosti: D:\Java apps>java HashMapTest 1000000 8 Obrada trajala 2090 milisekundi {7=125046, 6=124756, 5=125079, 4=124639, 3=125280, 2=125047, 1=124916, 0=125237}
Za upotrebu klase HashMap moraju se koristiti i za klju č i za vrednost objekti koji imaju odgovaraju će definisane i uskla đene metode hashCode() i equals(). One se koriste za proveru jednakosti prilikom upisa u kolekciju. U navedenom primeru to nije bio slu čaj, jer su se koristili objekti tipa Integer. U slu čaju da logika programa zahteva korisni čki definisane klase za klju č ili vrednost, ovaj zahtev se mora poštovati, jer bi u suprotnom program koristio istoimene metode klase Object, koje koriste adresu objekta kao heš kod, zbog čega je svaki objekat identi čan jedino samome sebi. java.util.HashMap (interesaniji (interesaniji metodi): HashMap() HashMap(Map entries) HashMap(int initialCapacity) HashMap(int initialCapacity, float loadFactor)
Konstruiše praznu mapu. Konstruiše mapu sa datim inicijalnim podacima. Konstruiše praznu mapu sa datim inicijalnim kapacitetom. Konstruiše praznu mapu sa datim inicijalnim kapacitetom i faktorom popunjenosti (podrazumevana vrednost iznosi 0.75).
TreeMap TreeMap predstavlja implementaciju interfejsa Map, koja sadrži sortirane parove klju č-vrednost. A kako će ti parovi biti sortirani, to može biti odre đeno prilikom kreiranja objekta klase TreeMap, ako se kao parametar preda objekat tipa Comparator. Ako se ne koristi taj konstruktor, sortira će se prema prirodnom redosledu, što bi za objekte tipa String
značilo sortiranje po metodu compareTo() iz interfejsa Comparable. TreeMap predstavlja jedinu klasu koja ima implementiran metod subMap(), koji vra ća deo sortirane mape. Ipak, uz sve to, postoji i loša strana strana – smanjena brzina pristupa pristupa elementima. Prethodni primer, ako se umesto umesto HashMap klase koristi klasa TreeMap, ima slede će rezultate: D:\Java apps>java TreeMapTest 1000000 8 Obrada trajala 2750 milisekundi {0=124642, 1=125603, 2=124796, 3=124942, 4=124985, 5=124780, 6=125052, 7=125200}
Evo nekih uporednih rezulta izlaza iz prethodnog primera, ako se za argument iterations odredi vrednost 1.000.000, dok se vrednost za argument distribution menja sledećim redosledom: 5 50 500 5000 TreeMap 2740 4230 6320 10490 HashMap 2310 2360 2360 3350
Atila Rafai