Professional Documents
Culture Documents
Ako ne znate koliko ete objekata koristiti da biste reili neki problem ili postoji
mogunost da e im broj biti promenljiv, onda postoji i problem njihovog smetanja
u memoriju. U programskom jeziku Java reenje tog problema predstavljaju
kolekcije - objekti koji se koriste za smetanje i manipulaciju nedefinisanim brojem
objekata.
Sve to je potrebno, s gledita dizajna korisnikog programa, jeste niz objekata koji
ima promenljivu veliinu i metode koje omoguavaju da se manipulie tim
objektima. Ali ima dobrih razloga za postojanje vie tipova kolekcija. Kao prvo,
razliite vrste kolekcija treba da imaju razliite naine na koji se moe manipulisati
podacima koje sadre. Kao drugo, razliite vrste kolekcija imaju razliite nivoe
efikasnosti odraivanja razliitih operacija. (Na primer, nee se na isti nain
umetnuti novi element u sortiranu ili obinu listu.)
Razraeni objektno orijentisani programski jezici sadre paket klasa koji slue za
reenje problema kolekcija. U jeziku C++ to je STL (Standard Template Library).
Poto je Java nastala kao jezik koji je trebalo da zameni C++ u odreenoj klasi
problema, pa joj je jezik C++ bio poetni uzor (a ona je kasnije narasla do
sadanjih mogunosti), i u njoj se nalazi grupa takvih klasa. Java je podravala
kolekcije ve od verzije 1.0; ta je podrka unekoliko bila poboljana u verziji 1.1,
ali se tek u verziji 2.0 dolo do obuhvatnije implementacije kolekcija.
Osnovne klase koje se koriste za kreiranje kolekcija tretiraju sve objekte koje mogu
da sadre kao da nemaju specifikovan tip, tanije, bazirane su na pretpostavci da
sadre objekte tipa Object. Poto sve klase u Javi imaju na poetku svoje
hijerarhije nasleivanja 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 reenje ima dovoljnu irinu. Problem nastaje
pri proveri tipa objekta koji neka kolekcija moe da sadri. Ako kreirate neki niz ili
matricu promenljivih imate mogunost provere tipova podataka koje pokuavate da
upiete. To znai da za vreme pisanja programa kompajler moe proveriti da li ste
pogreili prilikom pisanja programskog koda. U sluaju kolekcija to nije mogue, pa
ostaje samo neprijatna mogunost da korisnik vaeg programa tokom rada dobije
poruku o neuspenom pokuaju izvrenja nelegalne instrukcije od strane JVM-a
(Java Virtual Machine), ime se prekida i rad programa.
U praksi, piui 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 znai da ga tretira kao da je primerak neke optije klase
(upcasting), pa se moe rei da on gubi svoj identitet. Kasnije koristite te iste
objekte, ali prilikom preuzimanja iz kolekcije ne moete sa sigurnou tvrditi kog su
tipa ti objekti. Pre korienja ovih objekata ponovo ih vraate u prvobitni tip
(downcasting) kako bi ste mogli da koristite metode svojstvene toj klasi objekata.
Ako je oblik (klasa) u koji vraate objekat pogrean, dobijate poruku o greci:
Downcasting nije potreban samo za objekte tipa String, jer kadgod kompajler
oekuje objekat tipa String, a ne dobije takav, on automatski koristi metod
toString(), koji je definisan u klasi Object (i moe se redefinisati), a koji vraa naziv
klase iji je primerak taj objekat, praen znakom '@' i heksadecimalnim oblikom
hash koda tog objekta. U sluaju da elite da iskoristite neto od te kombinacije:
Interfejsi
Postoje dva osnovna interfejsa za kolekcije: Collection i Map. Ti interfejsi definiu
kakav treba da bude interfejs svake kolekcije i time definiu koje osnovne funkcije
treba da postoje.
Interfejsi kolekcija
Interfejs Collection navodi (izmeu ostalih) osnovne metode kojima se zahteva da se
definiu (implementiraju) osnovne operacije koje treba da postoje nad kolekcijom:
dodavanje elementa, provera da li neki element ve postoji u kolekciji, brisanje
elementa iz kolekcije, nain na koji se moe redom pristupiti svim elementima
kolekcije, kao i da se moe 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 proirenja interfejsa Collection postoje kolekcije List i Set, koje postavljaju
dodatne zahteve.
Interfejs List uvodi ureenu kolekciju, pa samim tim postoje i dodatni zahtevi da se
element moe uneti na odgovarajuu poziciju unutar postojee liste, preuzeti na
osnovu indeksa u toj listi ili obrisati element koji ima dati indeks:
void add(int index, Object element)
Osnovne klase
Da bi se programerima olakao rad, postoji pet apstraktnih klasa koje implementiraju
one metode koje predstavljaju samu osnovu:
AbstractCollection
AbstractList
AbstractSequentialList
AbstractSet
AbstractMap
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 nain prilaze problemu kolekcija, pa su, sa nekim
modifikacijama, ukljuene i u ovaj napredniji sistem:
Vector
Stack
Hashtable
Properties
Iteratori
Sve kolekcije imaju neki nain da prime neki objekat ili da predaju svoj sadraj.
Uvek postoje metode push ili add, ili neke druge slinog naziva. U skladu s tim da
uvek postoji mogunost da se u toku projektovanja programa doe do nekog reenja
koje zahteva promenu vrste kolekcije, kreiran je i univerzalan nain pristupa
elementima kolekcije - iterator. To je objekt ija je namena omoguavanje
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, pa je
zbog novih potreba dodat i nov iterator, Iterator, koji ima dodatne osobine (mogue
je brisanje elemenata preko iteratora) i krae 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 sadrala podatke za
sve zaposlene u jednom preduzeu, preuzimanje tih podataka bi izgledalo kao:
Iterator i = zaposleni.iterator();
while (i.hasNext())
{
Zaposleni z = (Zaposleni)i.next();
...
}
Povremeno ete naii na neki metod koji vue korene jo iz verzije 1.0, a koji
oekuje, kao parametar, enumerator. Statika 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)
);
Moete 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());
java.util.Iterator
boolean hasNext()
Object next()
void remove()
Korienje listi
List
ArrayList
Svaki put kada se pristupa sledeem elementu (po indeksu) potraga poinje od poetka
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 vei od polovine broja elemenata u listi, pretraga poinje s kraja liste.
???
java.util.Vector
Enumeration elements()
java.util.List
ListIterator listIterator()
ListIterator listIterator(int index)
Object remove(int i)
java.util.ListIterator
void add(Object element)
void set(Object element)
boolean hasPrevious()
Object previous()
int nextIndex()
int previousIndex()
java.util.LinkedList
LinkedList()
LinkedList(Collection elements)
void addFirst(Object element)
void addLast(Object element)
Object getFirst()
Object getLast()
Object removeFirst()
Object removeLast()
Atila Rafai
2
3
ta? element?
ta? element?
He tabele
Liste omoguavaju programeru da odredi u kom redosledu e se nalaziti objekti unutar
tih kolekcija. Ali ako se trai element kome se ne zna tana pozicija, moraju se
redom pregledavati elementi dok se ne naie ba taj element. Ako je kolekcija
velika, to moe 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
izraunava celobrojnu vrednost zvanu he kd, koju pridruuje pojedinanom elementu
kolekcije. Za he kd je bitno da se moe brzo izraunati 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 ulananih lista zvanih bucket (u bukvalnom
prevodu 'vedro'). Da bi se naao objekat u he tabeli, potrebno je izraunati njegov
he kod, i korienjem ukupnog broja bucketa se, preko modula, nalazi indeks bucketa
u kome se nalazi taj element. Kada se, tim brzim putem, "zahvati" vei broj
elemenata (kao voda vedrom), naredna operacija je redno pregledavanje elemenata u
listi da bi se pronaao odgovarajui 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.
Moe se desiti da je u bucketu samo taj element, ali se moe desiti i da je tamo jo
neki; tada se kae da je dolo do he kolizije. U tom sluaju mora se pristupiti
poreenju sa moda 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 izvri
samo nekoliko poreenja, pa je samim tim potrebno malo vremena da se doe do
eljenog elementa tabele. Ako je pak tabela vie popunjena, poveava se broj
kolizija, a samim tim se usporava rad nad he tabelom.
Ako je poznato koliko e biti otprilike elemenata u tabeli, moe se specifikovati
inicijalni kapacitet. Ta vrednost bi trebalo da bude otprilike 150% broja elemenata.
Poto se po nekim ispitivanjima pokazalo da je za inicijalni broj bucketa poeljno
odabrati prost broj (onaj koji je deljiv samo sa samim sobom), ako imamo potrebu za
oko 100 ulaza, inicijalna veliina bi trebalo da bude 151 bucket.
Ako se eli vea kontrola nad performansama he tabele, dolazi do problema u sluaju
kada inicijalni kapacitet nije mogue odrediti. Tada se, ako se poetni kapacitet
postavi na nedovoljnu vrednost, javlja potreba za tzv. rehashing operacijom, tj.
kreiranjem nove tabele sa odgovarajuom veliinom, njenim punjenjem objektimavrednostima iz 'stare' tabele i brisanjem iz memorije stare tabele. To je vremenski
zahtevna operacija, pa se mora uvesti kompromisno reenje: specifikuje se load
factor. U programskom jeziku Java to je vrednost kojom se odreuje kolika treba da
bude iskorienost bucketa da bi se pristupilo reheiranju. Na primer, ako load
factor ima vrednost 0.75 (to je u Javi podrazumevana vrednost), tabela se
automatski reheira na duplo veu tabelu kada ukupni broj iskorienih bucketa pree
75% inicijalne vrednosti.
HashSet
HashSet je klasa iz Java biblioteke kolekcija koja implementira set pomou 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 pokuaj upisa novog objekta. Metod contains() redefinisan je
tako da na postojanje odreenog elementa proverava samo odgovarajui bucket.
Iterator he seta postoji za prolazak kroz celu tabelu.
Navedeni program, koji je primer korienja he seta, uitava sve rei sa svog
ulaza, upisuje ih u he tabelu i, na kraju, ispisuje niz preuzetih rei te ukupan
broj naenih rei:
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 rauna he kd za niz znakova. U sluaju objekta String to je integer
koji je izveden iz samih znakova.
U sluaju ostalih klasa, kompajler nee prijaviti greku 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 to to
ona vrednost tipa integer (za koju je dozvoljeno da je negativna) izvodi iz
memorijske adrese objekta. To znai da u optem sluaju ova metoda za svaki objekat
daje razliit he kod, nezavisno od toga da li su vrednosti neka dva objekta
identine. Zbog toga je potrebno redefinisati metod za svaku klasu koja bi mogla
biti ikada ubaena u he tabelu. Pri tome treba voditi rauna da to odslikava
sadraj objekta. Na primer, u sluaju 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 matini broj.
Osim redefinisanja metode hashCode() potrebno je redefinisati i metodu equals(). Ta
metoda je takoe 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 slina HashSet klasi, osim to ima dodato jedno poboljanje: objekti su
sortirani. Svaki put kada se neki element doda u ovu kolekciju, postavlja se na
odgovarajue mesto. Podrazumeva se da objekat koji se postavlja u kolekciju ima
implementiran interfejs Comparable. Taj interfejs deklarie 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 nainu sortiranja, a pozitivnu vrednost u
suprotnom sluaju. Tane vrednosti nisu bitne, bitan je samo znak vrednosti koja se
vraa 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 azbunom rasporedu slova.
}
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.");
Interfejs Map
Konceptualno, ovaj interfejs definie kolekciju koja je slina vektoru, ali se kolekcija pretrauje, umesto po indeksima, po
nekom objektu. Na primer, ako imate renik koji treba da sadri strune izraze, kreirate mapu i ona za svaku re, koja je
objekat, sadri takoe neki odgovarajui objekat. Pri tome vai pravilo da se svi struni izrazi pojavljuju samo jednom u
reniku i da oni predstavljaju klju po kome se nalazi objekat koji predstavlja neku vrednost, to bi u ovom sluaju bilo
objanjenje izraza.
Za reenje ovakvog problema koristi se ba klasa izvedena iz interfejsa Map. Ovaj interfejs podrava postojanje parova
objekata klju-vrednost, pri emu ne moe postojati duplikat kljueva. Metode koje deklarie Map jesu: size() daje
informaciju o broju elemenata u mapi, isEmpty() vraa true ako mapa nema niti jedan element, put(Object key,
Object value) dodaje specificiranu vrednost u mapu pod specifikovanim kljuem, get(Object key) vraa vrednost
pridruenu datom kljuu, remove(Object key) uklanja par klju-vrednost za datu vrednost kljua key. Interfejs Map
takoe zahteva dva iteratora: keys() za kljueve 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 odreeni klju u mapi. U sluaju da postoji, zamenjuje staru vrednost novom,
dok staru vrednost vraa kao povratnu vrednost. Time se spreava da se preko neke ve postojee vrednosti upie nova, a da
se stara vrednost izgubi. Ovaj metod se moe modifikovati tako da spreava zamenu postojee vrednosti, to bi moglo
odgovarati ako se korienjem ovog primera implementira renik.
Metod remove() takoe vraa objekat-vrednost (u sluaju uspenog brisanja, tako da se moe skratiti postupak preuzimanja
objekata-vrednosti iz mape nije potrebno prvo preuzeti neku vrednost korienjem metode get(), pa je naknadno brisati,
ve za to moe direktno posluiti metod remove()).
java.util.Map (interesantniji metodi):
Object get(Object key)
Object put(Object key, Object
value)
Set keySet()
Collection values()
HashMap
Standardna Java biblioteka klasa sadri dva razliita tipa mapa, HashMap i TreeMap. Obe klase imaju isti interfejs, ali su
razliite po pitanju efikasnosti. Ako je potrebno esto koristiti metod get(), klasa HashMap omoguava brz pristup
elementima, jer koristi he tabelu. Na taj nain, brzim proraunom he koda moe se u veini sluajeva direktno pristupiti
traenom elementu. Ako se taj nain realizacije metoda get() uporedi sa implementacijom istoimene metode u klasi
ArrayList, razlika u brzini e biti vrlo oigledna.
Primer korienja 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 sluajnih brojeva koji se dobijaju korienjem
statike metode random() klase Math. Kao parametre, ovaj primer prima dva broja: broj iteracija i raspon u kome e se
proveravati distribucija brojeva. Na kraju modifikacije objekta klase HashMap dobija se i izvetaj o vremenskom trajanju
radnog dela programa (proteklo vreme u milisekundama). Jedan od test rezultata imao je sledee 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 odgovarajue definisane i usklaene
metode hashCode() i equals(). One se koriste za proveru jednakosti prilikom upisa u kolekciju. U navedenom primeru to
nije bio sluaj, jer su se koristili objekti tipa Integer. U sluaju da logika programa zahteva korisniki definisane klase za
klju ili vrednost, ovaj zahtev se mora potovati, jer bi u suprotnom program koristio istoimene metode klase Object, koje
koriste adresu objekta kao he kod, zbog ega je svaki objekat identian jedino samome sebi.
java.util.HashMap (interesaniji metodi):
HashMap()
HashMap(Map entries)
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
TreeMap
TreeMap predstavlja implementaciju interfejsa Map, koja sadri sortirane parove klju-vrednost. A kako e ti parovi biti
sortirani, to moe biti odreeno prilikom kreiranja objekta klase TreeMap, ako se kao parametar preda objekat tipa
Comparator. Ako se ne koristi taj konstruktor, sortirae se prema prirodnom redosledu, to bi za objekte tipa String
znailo sortiranje po metodu compareTo() iz interfejsa Comparable.
TreeMap predstavlja jedinu klasu koja ima implementiran metod subMap(), koji vraa deo sortirane mape. Ipak, uz sve to,
postoji i loa strana smanjena brzina pristupa elementima. Prethodni primer, ako se umesto HashMap klase koristi klasa
TreeMap, ima sledee 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 sledeim redosledom:
5
50
500
5000
TreeMap
2740
4230
6320
10490
HashMap
2310
2360
2360
3350
Atila Rafai