Professional Documents
Culture Documents
Core Java, Chapter 13, Collections.v.2
Core Java, Chapter 13, Collections.v.2
Cornell“
КОЛЕКЦИЈЕ
Структура података за коју се одлучимо може имати велики утицај на то колико ће наша
имплементација бити „природна“, као и на њене перформансе. Да ли је потребно брзо
претражити хиљаде или чак милионе сортираних елемената? Да ли је потребно брзо уметати или
уклањати елементе из средине сортиране секвенце? Да ли је потребно успоставити везе између
кључева и вредности?
У овом поглављу теорија је прескочена и акценат је на томе како користити колекцијске класе из
стандардне библиотеке.
Колекцијски интерфејси
У иницијалној верзији Јаве постојао је само мали скуп класа за најкорисније структуре података:
Vector, Stack, Hashtable, BitSet и интерфејс Enumeration који обезбеђује апстрактни
механизам за посећивање елемената произвољног контејнера.
Интерфејс за ред одређује да је могуће додавати елементе на крај реда, уклањати их са његовог
почетка и установити колико елемената се налази у реду. Ред користимо када је потребно да
објекте прикупљамо и дохватамо у маниру „first in first out“ (први пристигао - први обрађен).
interface Queue<E>{
// uproscena forma interfejsa iz standardne biblioteke
void add(E element);
E remove();
int size();
}
Интерфејс нам не говори ништа о томе како је ред имплементиран. Постоје две уобичајене
имплементације реда, једна користи „циркуларни низ“, а друга повезану листу:
1
Figure 13-2 Queue Implementations
2
Свака имплементација може се изразити класом која имплементира интерфејс Queue:
Када у свом програму користите ред, не морате да знате која имплементација се заправо користи
након што је колекција конструисана. Према томе, има смисла користити конкретну класу само
када се конструише објекат који представља колекцију, а чувати референцу на добијену колекцију
у променљивој типа интерфејса:
3
које ће наш програм прикупити, можда је ипак боље да користимо имплементацију са повезаном
листом.
У Јавиној библиотеци постоји и скуп класа чија имена почињу са „Abstract“. Нпр. AbstractQueue.
Те класе су намењене имплементаторима библиотеке. У мало вероватном случају да желимо да
имплементирамо своју класу која представља ред, вероватно нам је једноставније да изведемо
поткласу из класе AbstractQueue, него да имплементирамо све методе интерфејса Queue.
Осим ова два, постоји још неколико метода о којима ће више речи бити касније.
Метод add() додаје елемент у колекцију. Враћа true ако додавање елемента заиста измени
колекцију, а false уколико колекција остане непромењена. Нпр. ако покушамо додавање објекта
у скуп, а објекат већ постоји унутра, позив метода add() нема ефекта, јер скуп не прихвата
дупликате.
Метод iterator() враћа објекат класе која имплементира интерфејс Iterator. Објекат
итератор се може користити за посећивање објеката колекције, једног по једног.
Итератори
Узастопним позивима метода next() могуће је посетити све елементе колекције, један по један.
Међутим, ако смо дошли до краја колекције, метод next() избацује NoSuchElementException.
Према томе, пре позива метода next() потребно је позивати метод hasNext(). Овај метод
враћа true ако има још објеката које итератор није посетио. Ако желимо да обиђемо све
елементе колекције, тражимо итератор, а потом позивамо метод next() све док метод
hasNext() враћа true. Нпр.
Collection<String> c = …;
Iterator<String> iter = c.iterator();
while(iter.hasNext()){
String element = iter.next();
ради се нешто са елементом
}
4
Почев од Java SE 5.0, постоји елегантно скраћење за овакву петљу. То је тзв. „for each“ петља:
Компајлер просто „for each“ петљу преводи у петљу која користи итератор.
„For each“ петља ради са објектима произвољне класе која имплементира интерфејс Iterable.
Овај интерфејс има само један метод:
Интерфејс Collection изведен је из интерфејса Iterable. Према томе, могуће је користити „for
each“ петљу са (скоро) сваком колекцијом из стандардне библиотеке (колекције чија се имена
завршавају са „Map“ не имплементирају интерфејс Collection).
Међутим, ако идемо кроз елементе из HashSet, наилазићемо на њих у потпуно случајном
поретку. Можемо бити сигурни да ћемо итерирањем обићи све елементе колекције, али не
можемо правити никакве претпоставке о њиховом поретку. То обично не представља проблем.
5
Уклањање елемената
Метод remove() интерфејса Iterator уклања елемент враћен последњим позивом метода
next(). У многим ситуацијама ово има смисла - треба да видимо елемент пре него што одлучимо
да ли га треба уклонити. Међутим, уколико желимо да уклонимо елемент са задате позиције, ипак
треба да га прођемо. Нпр. ево како се уклања први елемент колекције стрингова c:
Iterator<String> it = c.iterator();
it.next(); // preskocimo prvi element
it.remove(); // sada ga uklonimo
Још је битније да постоји веза између позива метода next() и remove(). Није исправно позвати
метод remove() ако му није претходио позив метода next(). Уколико ипак пробамо, долази до
избацивања IllegalStateException.
it.remove();
it.remove(); // Greska!
Уместо тога, прво се мора позвати метод next() како би „скочио“ преко елемента који треба
уклонити:
it.remove();
it.next();
it.remove(); // OK
Како су интерфејси Collection и Iterator генерички, могуће је писати „utility“ методе који
оперишу произвољном врстом колекција. Нпр. следи генерички метод који тестира да ли
произвољна колекција садржи дати елемент:
Заправо, интерфејс Collection дефинише приличан број корисних метода које морају
обезбедити све класе које имплементирају овај интерфејс. Између осталих, то су методи:
int size()
boolean isEmpty()
boolean contains(Object obj)
boolean containsAll(Collection<?> c)
6
boolean equals(Object other)
boolean addAll(Collection<? extends E> from)
boolean remove(Object obj)
boolean removeAll(Collection<?> c)
void clear()
boolean retainAll(Collection<?> c)
Object[] toArray()
<T> T[] toArray(T[] arrayToFill)
Наравно, напорно је да свака класа која имплементира интерфејс Collection обезбеди толико
метода. Како би се имплементаторима живот учинио лакшим, библиотека поседује класу
AbstractCollection која оставља фундаменталне методе size() и iterator() апстрактним,
док их имплементације осталих метода користе. Нпр.
Конкретна колекцијска класа сада може да се изведе из класе AbstractCollection. Она треба
да обезбеди метод iterator(), док се за метод contains() побринула базна класа
AbstractCollection. Међутим, уколико поткласа има ефикаснији начин за имплементирање
метода contains(), слободна је да га употреби.
java.util.Collection<E>
Iterator<E> iterator()
враћа итератор који се може користити за посећивање елемената колекције.
int size()
враћа број елемената тренутно смештених у колекцију.
boolean isEmpty()
враћа true ако колекција не садржи елементе.
7
boolean add(Object obj)
додаје елемент у колекцију. Враћа true ако је колекција измењена овим позивом.
void clear()
уклања све елементе колекције.
Object[] toArray()
враћа низ објеката из колекције.
java.util.Iterator<E>
boolean hasNext()
враћа true ако има још непосећених елемената.
E next()
враћа следећи елемент који треба посетити. Избацује NoSuchElementException ако се стигло до
краја колекције.
void remove()
уклања последњи посећени објекат. Мора уследити непосредно након посете том елементу. Ако
је колекција измењена након последње посете, избацује IllegalStateException.
8
Конкретне колекције
У табели која следи набројане су колекције из Јавине библиотеке и кратак опис сваке од њих.
Повезане листе
Низ и објекат класе ArrayList имају велики недостатак: уклањање елемента из средине низа је
скупо јер сви елементи који следе морају да се помере ка почетку низа (видети наредну слику).
Исто важи и за уметање елемената у средину.
9
Добро позната структура података, повезана листа, решава овај проблем. Док низ чува референце
на објекте на узастопним меморијским локацијама, повезана листа чува сваки објекат у посебном
„линку“. Сваки линк такође чува референцу на наредни линк у секвенци. У Јави су све повезане
листе заправо двоструко повезане, тј. сваки линк такође чува и референцу на свог претходника :
Уклањање елемента из средине повезане листе није скупа операција - потребно је ажурирати
само линкове који окружују тај елемент.
10
Постоји битна разлика између повезаних листи и генеричких колекција. Повезана листа је уређена
колекција у којој је позиција елемената битна. Метод add() додаје елемент на крај листе.
Међутим, често желимо да додамо елемент негде у средину листе. За такав метод add() је
задужен итератор пошто итератори описују позиције у колекцијама. Коришћење итератора за
додавање елемената има смисла само за колекције које имају природно уређење. Нпр. скуп нема
никакво уређење елемената. Према томе, нема метода add() у интерфејсу Iterator, али постоји
подинтерфејс ListIterator, који садржи метод add():
Додатно, интерфејс ListIterator поседује два метода која се могу користити за обилажење
листе уназад:
E previous()
boolean hasPrevious()
Попут метода next(), метод previous()враћа објекат преко кога је итератор управо прешао.
Метод listIterator() класе LinkedList враћа итератор, објекат класе која имплементира
интерфејс ListIterator.
11
Metod add() додаје нови елемент испред позиције итератора. Нпр. следећи фрагмент кôда
пролази први елемент и умеће ”Juliet” испред другог:
Ако се метод add()позове више пута, елементи се просто додају тим редом, један за другим,
испред текуће позиције итератора.
12
Непосредно након позива next(), метод remove() уклања елемент лево од итератора. Међутим,
ако је управо позван метод previous(), уклања се елемент десно. Није могуће звати remove()
два пута узастопно.
Метод set() замењује последњи елемент враћен позивом метода next() или previous()
новим елементом. Нпр. следећи фрагмент кôда замењује први елемент листе новом вредношћу:
Као што се може претпоставити, ако један итератор обилази колекцију док је други модификује,
могу настати конфликтне ситуације. Нпр. претпоставимо да један итератор показује испред
елемента који је други итератор управо уклонио. Први итератор више није валидан и не би смео
даље да се користи. Итератори за повезане листе дизајнирани су тако да детектују такве
модификације. Ако итератор открије да је његова колекција измењена другим итератором или
позивом неког метода над колекцијом, он избацује ConcurrentModificationException. Нпр.
List<String> lista = …;
ListIterator<String> iter1 = lista.listIterator();
ListIterator<String> iter2 = lista.listIterator();
iter1.next();
iter1.remove();
iter2.next(); // izbacuje ConcurrentModificationException
Као што смо видели, ListIterator се може користити за обилажење листе у произвољном
смеру и за додавање и уклањање елемената.
13
Многи други корисни методи за оперисање повезаним листама декларисани су у интерфејсу
Collection. Они су, већим делом, имплементирани у суперкласи AbstractCollection класе
LinkedList. Нпр. метод toString() позива toString() за елементе и производи један дуг
стринг облика [A, B, C]. То је згодно приликом исправљања грешака. Метод contains() користи
се за утврђивање да ли је елемент присутан у повезаној листи. Нпр. позив
osoblje.contains("Pera") враћа true ако повезана листа већ садржи стринг једнак стрингу
"Pera".
Повезана листа не подржава брз случајан приступ. Ако желимо да видимо n-ти елемент, морамо
да пођемо од почетка и најпре пређемо преко првих n-1 елемената. Не постоји пречица. Из тог
разлога програмери обично не користе повезане листе у ситуацијама у којима је потребно
приступати елементима коришћењем целобројног индекса.
Без обзира на то, класа LinkedList поседује метод get()који омогућује приступ одређеном
елементу:
LinkedList<String> lista = … ;
String obj = lista.get(n);
Наравно, овај метод није ефикасан. Уколико затекнете себе како га користите, вероватно
користите структуру података неадекватну за Ваш проблем.
Никада не треба да користите овај илузорни метод случајног приступа за пролазак кроз повезану
листу. Кôд
je запањујуће неефикасан. Сваки пут када трагамо за наредним елементом, потрага почиње од
почетка листе. Објекат класе LinkedList не труди се да кешира информацију о позицији.
Метод get() има једну незнатну оптимизацију: ако је индекс бар size()/2, потрага за
елементом почиње од краја листе.
Интерфејс ListIterator такође поседује метод који враћа индекс текуће позиције. Заправо, како
итератори концептуално показују између елемената, постоје два таква метода: nextIndex(), који
враћа целобројни индекс елемента који ће бити враћен наредним позивом метода next(), и
previousIndex(), који враћа индекс елемента који ће бити враћен наредним позивом метода
previous(). Наравно, то је за један мање од nextIndex(). Ови методи су ефикасни - итератори
прате текућу позицију. Најзад, ако имамо целобројни индекс n, тада lista.listIterator(n)
враћа итератор који показује непосредно испред елемента са индексом n. Позив метода next()
враћа исти елемент као и lista.get(n). Добијање тог итератора је неефикасно.
Ако имамо повезану листу са малим бројем елемената, не треба да будемо превише параноични
око цене get() и set() метода. Али, на првом месту, због чега онда користити повезану листу?
Једини разлог за коришћење повезане листе је минимизација цене уметања и уклањања из
средине листе. Ако имате само неколико елемената, можете користити ArrayList.
14
Препорука је држати се подаље од свих метода који користе целобројни индекс за означавање
позиције у повезаној листи. Ако желите случајан приступ у колекцији, користите низ или
ArrayList, а не повезану листу.
Пример 1 (LinkedListTest): Просто креира две листе, учешљава их, потом уклања сваки други
елемент из друге листе и коначно тестира метод removeAll(). Препорука је пратити ток
извршавања програма и посебно обратити пажњу на итераторе. Може бити од помоћи цртање
дијаграма позиција итератора, попут:
|ACE |BDFG
A|CE B|DFG
AB|CE B|DFG
…
Приметити да позив
System.out.println(a);
штампа све елементе повезане листе позивајући метод toString() класе AbstractCollection.
java.util.List<E>
ListIterator<E> listIterator()
враћа list iterator за посећивање елемената листе.
E remove(int i)
уклања и враћа елемент са задате позиције.
E get(int i)
дохвата елемент са задате позиције.
E set(int i, E element)
замењује елемент на задатој позицији новим и враћа стари елемент.
15
java.util.ListIterator<E>
boolean hasPrevious()
враћа true ако постоји елемент који ће бити посећен итерирањем уназад кроз листу.
E previous()
враћа претходни објекат. Избацује NoSuchElementException ако је достигнут почетак листе.
int nextIndex()
враћа индекс елемента који ће бити враћен наредним позивом метода next().
int previousIndex()
враћа индекс елемента који ће бити враћен наредним позивом метода previous().
java.util.LinkedList<E>
LinkedList()
конструише празну повезану листу.
E getFirst()
E getLast()
враћа елемент са почетка, односно краја листе.
E removeFirst()
E removeLast()
уклања и враћа елемент са почетка, односно краја листе.
Array Lists
У претходној секцији видели смо интерфејс List и класу LinkedList која га имплементира.
Интерфејс List описује уређену колекцију у којој је позиција елемената битна. Постоје два
протокола за посећивање елемената: помоћу итератора и случајним приступом помоћу метода
get() и set(). Овај други није погодан за повезане листе, али, наравно, get() и set()имају
16
пуно смисла за низове. У библиотеци колекција је слична класа ArrayList, која такође
имплементира интерфејс List. Она енкапсулира низ објеката који се динамички реалоцира.
Јава програмери који су ветерани користе класу Vector кад год им је потребан динамички низ.
Зашто би требало користити ArrayList уместо Vector? Из једноставног разлога. Сви методи
класе Vector су синхронизовани (synchronized). Безбедно је приступити објекту класе Vector
из две нити. Али ако приступате вектору из само једне нити - што је најчешћи случај - Ваш кôд
траћи прилично времена на синхронизацију. Супротно томе, методи класе ArrayList нису
синхронизовани. Препорука је користити ArrayList уместо Vector кад год нам није потребна
синхронизација.
Hash Sets
Повезане листе и низови омогућавају задавање поретка којим ће елементи бити смештени.
Међутим, ако тражимо одређени елемент и не сећамо се његове позиције, морамо обилазити
редом све елементе док га не пронађемо. То може бити временски захтевно ако колекција
садржи много елемената. Ако нам поредак елемената није важан, постоје друге структуре
података које омогућују много бржу претрагу. Недостатак је што те структуре не пружају контролу
над редоследом којим ће се елементи појављивати. Оне организују елементе редом који је
подесан за њихову сопствену сврху.
Важно је да се хеш-кôд рачуна брзо и да израчунавање зависи само од стања објекта који се
хешира, а не и од других објеката у хеш-табели.
У Јави, хеш-табеле су имплементиране као низови повезаних листи. Свака листа се зове „bucket“
(слика испод). Да би се одредила позиција објекта у табели, израчунава се његов хеш-кôд и нађе
остатак при дељењу укупним бројем bucket-a. Тако добијени број је индекс bucket-a који садржи
тај елемент. Нпр. ако објекат има хеш-кôд 76268 и има 128 bucket-a, објекат се смешта у bucket
108 (јер је остатак 76268 % 128 једнак 108). Можда имамо среће и нема других елемената у том
bucket-у. Онда се елемент просто убаци у bucket. Наравно, неизбежно је да се понекад деси да је
bucket већ пуњен. Тада долази до тзв. колизије. У том случају, нови објекат се пореди са свим
објектима из bucket-a како би се видело да ли је већ присутан. Ако хеш-кôдови имају разумну
случајну дистрибуцију и број bucket-a је довољно велик, требало би да буде потребно свега
неколико поређења.
17
Ако желите већу контролу над перформансама хеш-табеле, можете задати иницијални број
bucket-a. Тај број представља број bucket-a који се користе за прикупљање објеката са идентичним
хеш-вредностима. Ако се превише објеката убаци у хеш-табелу, број колизија расте, а
перформансе тражења објеката опадају.
Ако знате колико ће приближно елемената на крају бити у табели, можете подесити број bucket-a.
Типично, он се поставља на нешто између 75% и 150% очекиваног броја елемената. Неки
истраживачи верују да је добра идеја да број bucket-а буде прост број како би се спречило
груписање кључева. Међутим, не постоји убедљив доказ за то. Стандардна библиотека за број
bucket-a користи степене двојке, подразумевано 16. (Свака вредност коју задате за величину
табеле аутоматски бива заокружена на следећи степен двојке.)
Наравно, не морате увек знати колико ће елемената бити или ваша иницијална процена може
бити премала. Ако се хеш-табела препуни, неопходно је да буде „рехеширана“. Да би се табела
рехеширала, неопходно је да се креира табела са већим бројем bucket-a, сви елементи убаце у
нову табелу и стара табела одбаци. Тзв. „load factor“ одређује када се табела рехешира. Нпр. ако је
load factor 0.75 (што је подразумевано) и табела је попуњена више од 75%, она се аутоматски
рехешира са два пута већим бројем bucket-a. За већину примена разумно је оставити да load factor
буде 0.75.
Јавина библиотека колекција поседује класу HashSet која имплементира скуп користећи хеш-
табелу. Елементи се додају методом add(). Metod contains() је предефинисан тако да врши
брзу претрагу како би открио да ли је елемент присутан у скупу. Он проверава само елементе
једног bucket-a, а не све елементе у колекцији.
18
HashSet итератор обилази све bucket-e редом. Како хеширање разбацује елементе широм
табеле, они бивају посећени наизглед случајним редом. HashSet треба користити једино ако нам
није важно уређење елемената у колекцији.
Пример 2 (SetTest): Чита речи са стандардног улаза (System.in), додаје их у скуп и на крају
штампа првих 20 речи из скупа. Нпр. могуће је као улаз задати текст „Алиса у Земљи Чуда“
(http://www.gutenberg.net) покретањем програма из командне линије са:
Програм чита све речи са улаза и додаје их у хеш-сет. Затим итерира кроз речи скупа и штампа
њихов број. Алиса у Земљи Чуда има 5909 различитих речи, укључујући copyright напомену на
почетку. Речи се појављују случајним редом.
ОПРЕЗ: Будите пажљиви када мењате елементе скупа. Ако се тиме мења хеш-кôд елемента,
елемент више неће бити на исправној позицији у структури података.
java.util.HashSet<E>
HashSet()
конструише празан хеш-сет.
HashSet(int initialCapacity)
конструише празан хеш-сет задатог капацитета (број bucket-a).
java.lang.Object
int hashCode()
враћа хеш-кôд објекта. Хеш-кôд може бити произвољан цео број, позитиван или негативан.
Дефиниције метода equals() и hashCode() морају бити компатибилне: ако је x.equals(y)
true, онда x.hashCode()мора бити иста вредност као и y.hashCode().
Tree Sets
19
sorter.add("Amy");
sorter.add("Carl");
for(String s : sorter)
System.out.println(s);
Вредности се штампају у сортираном поретку: Amy, Bob, Carl. Као што име класе указује,
сортирање је постигнуто коришћењем стабла као структуре података. (Тренутна имплементација
користи red-black tree. За детаљан опис ових стабала, погледајте, нпр. Introduction to Algorithms
аутора Thomas Cormen, Charles Leiserson, Ronald Rivest, Clifford Stein [The MIT Press, 2001].) Сваки
пут када се елемент дода у стабло, бива смештен на одговарајућу позицију у складу са
сортирањем. Према томе, итератор увек обилази елементе у сортираном поретку.
Додавање елемента у стабло је спорије од додавања у хеш-табелу, али је још увек много брже од
додавања на праву позицију у низу или повезаној листи. Ако стабло садржи n елемената,
потребно је у просеку log2 n поређења за проналажење исправне позиције новог елемента. Нпр.
ако стабло већ садржи 1000 елемената, додавање новог елемента захтева око 10 поређења.
Према томе, додавање елемената у TreeSet је донекле спорије од додавања у HashSet - видети
табелу која следи - али TreeSet аутоматски сортира елементе.
java.util.TreeSet<E>
TreeSet()
конструише празан tree set.
Поређење објеката
Како TreeSet „зна“ начин на који ми желимо да елементи буду сортирани? Подразумевано, tree
set претпоставља да убацујемо елементе класе која имплементира интерфејс Comparable. Тај
интерфејс дефинише само један метод:
Позив a.compareTo(b) мора вратити 0 ако су а и b једнаки, негативан цео број ако је а испред b
у сортираном поретку, а позитиван цео број ако је а после b. Тачна вредност није важна, већ само
њен знак (>0, 0 или <0). Неколико стандардних Јава класа имплементира интерфејс Comparable.
Једна од њих је класа String. Њен метод compareTo() пореди стрингове лексикографски.
20
Ако убацујемо сопствене објекте, морамо сами дефинисати поредак имплементирањем
интерфејса Comparable. Не постоји подразумевана имплементација метода compareTo()у класи
Object.
Нпр. овако се могу сортирати објекти класе Artikal по броју артикла (brojArtikla је типа int):
Ако поредимо два позитивна цела броја, попут бројева артикала из нашег примера, можемо
просто вратити њихову разлику - она ће бити негативна ако први артикал треба да дође пре
другог, 0 ако су бројеви артикала идентични, а позитивна иначе.
ОПРЕЗ: Овај трик функционише једино ако су бројеви из довољно малог опсега. Ако је x велики
позитиван цео број, а y велики негативан број, разлика x-y може изаћи изван опсега целих
бројева (енг. overflow).
Попут метода compareTo(), метод compare() враћа негативан цео број ако је а испред b у
сортираном поретку, 0 ако су идентични, а позитиван цео број иначе.
Како би се артикли сортирали по опису, просто дефинишемо класу која имплементира интерфејс
Comparator:
21
Затим проследимо објекат ове класе конструктору tree set-a:
Ако конструишемо стабло са компаратором, оно користи овај објекат кад год треба да пореди два
елемента.
Приметимо да овај компаратор не поседује никакве податке, има само метод за поређење. Такав
објекат се понекад назива „објекат-функција“.
SortedSet<Artikal> sortPoOpisu =
new TreeSet<Artikal>(
new Comparator<Artikal>(){
public int compare(Artikal a, Artikal b){
String opisA = a.getOpis();
String opisB = b.getOpis();
return opisA.compareTo(opisB);
}
}
);
Ако погледамо претходну табелу, можемо се запитати треба ли увек користити tree set уместо
hash set-a? После свега, не делује да додавање елемената траје много дуже, а елементи су
аутоматски сортирани. Одговор зависи од података које прикупљамо. Ако не желимо да буду
сортирани, нема разлога да плаћамо цену сортирања. Још важније, за неке податке је много теже
изаћи на крај са поретком него са хеш-функцијом. Хеш-функција само треба разумно да разбацује
објекте, док функција поређења мора да распозна објекте са потпуном прецизношћу.
Како би се ова разлика учинила конкретнијом, размотримо прикупљање скупа правоугаоника. Ако
користимо TreeSet, неопходно је да обезбедимо Comparator<Pravougaonik>. Како поредимо
два правоугаоника? По површини? То „не иде“. Можемо имати два различита правоугаоника, са
различитим координатама темена, али једнаке површине. Поредак за стабло мора бити потпун
(total ordering). Произвољна два елемента морају бити упоредива, а поређење може дати 0 само
ако су елементи једнаки. Постоји такав поредак за правоугаонике (лексикографско уређење по
координатама), али је неприродан и гломазан за израчунавање. Насупрот томе, хеш-функција је
скоро дефинисана за класу Pravougaonik. Она просто хешира координате.
Почев од Java SE 6 класа TreeSet имплементира интерфејс NavigableSet. Овај интерфејс додаје
неколико метода погодних за лоцирање елемената и обилазак уназад.
22
Пример 3 (TreeSetTest): Гради 2 tree set-a Artikal објеката. Први сортира по броју артикла, што је
подразумевани поредак Artikal објеката. Други сортира по опису користећи компаратор.
java.lang.Comparable<T>
java.util.Comparator<T>
int compare(T a, T b)
пореди два објекта и враћа негативну вредноста ако a долази испред b, 0 ако су идентични у
сортираном поретку, а позитивну вредност иначе.
java.util.SortedSet<E>
E first()
E last()
враћа најмањи или највећи елемент сортираног скупа.
java.util.NavigableSet<E>
E higher(E value)
E lower(E value)
враћа најмањи елемент >value или највећи елемент <value или null ако такав елемент не
постоји.
E ceiling(E value)
E floor(E value)
враћа најмањи елемент >=value или највећи елемент <=value или null ако такав елемент не
постоји.
E pollFirst()
E pollLast()
уклања и враћа најмањи или највећи елемент скупа или null ако је скуп празан.
Iterator<E> descendingIterator()
враћа итератор који обилази скуп у опадајућем смеру.
23
java.util.TreeSet<E>
TreeSet()
конструише tree set за смештање Comparable објеката.
Kao што је већ речено, ред омогућује ефикасно додавање елемената на крај и уклањање
елемената са почетка. Ред са два краја, тзв. deque, омогућује ефикасно додавање и уклањање
елемената и са почетка и са краја. Додавање елемената у средину није подржано. Java SE 6 уводи
интерфејс Deque. Имплементирају га класе ArrayDeque и LinkedList, при чему обе обезбеђују
колекције чија величина расте по потреби.
java.util.Queue<E>
E remove()
E poll()
уклања и враћа елемент са почетка реда када ред није празан. Ако је ред празан, први метод
избацује NoSuchElementException, а други враћа null.
E element()
E peek()
враћа елемент са почетка реда не уклањајући га када ред није празан. Ако је ред празан, први
елемент избацује NoSuchElementException, а други враћа null.
java.util.Deque<E>
24
E removeFirst()
E removeLast()
E pollFirst()
E pollLast()
уклања и враћа елемент са почетка или краја када ред није празан. Ако је ред празан, прва два
метода избацују NoSuchElementException, а последња два враћају null.
E getFirst()
E getLast()
E peekFirst()
E peekLast()
враћа елемент са почетка или краја реда не уклањајући га када ред није празан. Ако је ред празан,
прва два метода избацују NoSuchElementException, а последња два враћају null.
java.util.ArrayDeque<E>
ArrayDeque()
ArrayDeque(int initialCapacity)
конструише неограничени deque иницијалног капацитета 16 или задатог иницијалног капацитета.
Priority Queues
Ред са приоритетом дохвата елементе у сортираном поретку након што су убачени произвољним
редом. Кад год се позове метод remove(), добије се тренутно најмањи елемент у реду. Међутим,
ред са приоритетом не сортира све своје елементе. Ако се итерира кроз елементе, они нису нужно
сортирани. Ред са приоритетом користи елегантну и ефикасну структуру података, хип (енг. heap).
Хип је бинарно стабло у ком операције add() и remove() узрокују да најмањи елемент гравитира
ка врху без губљења времена на сортирање свих елемената.
Попут TreeSet, ред са приоритетом може чувати или елементе класе која имплементира
интерфејс Comparable или се Comparator објекат прослеђује конструктору.
Типична употреба реда са приоритетом је за распоређивање послова (енг. job scheduling). Сваки
посао има приоритет. Послови се додају произвољним редом. Кад год треба започети нови посао,
из реда се уклања посао највишег приоритета. (Како је традиционално приоритет 1 „највиши“
приоритет, операција remove() уклања најмањи елемент.)
java.util.PriorityQueue
PriorityQueue()
PriorityQueue(int initialCapacity)
конструише ред са приоритетом за смештање Comparable објеката.
25
Мапе
Скуп је колекција која омогућује брзо проналажење постојећег елемента. Међутим, за тражење
елемента неопходно је имати његову тачну копију. То није веома уобичајено приликом претраге -
обично имамо неку кључну информацију и желимо да пронађемо елемент који јој је придружен.
Мапа је структура података која управо томе служи. У мапу се смештају парови кључ/вредност.
Можемо наћи вредност ако имамо кључ. Нпр. можемо чувати табелу записа о запосленима, где су
кључеви ID запослених, а вредности објекти класе Radnik.
У Јавиној библиотеци налазе се две имплементације мапа опште намене: HashMap и TreeMap.
Обе ове класе имплементирају интерфејс Map.
Hash map хешира кључеве, а tree map користи тотално уређење кључева како би их организовала
у стабло претраге. Хеш или функција поређења примењује се искључиво на кључеве. Вредности
придружене кључевима се не хеширају нити се пореде.
Да ли се определити за hash map или tree map? Као и са скуповима, хеширање је за нијансу брже
и пожељније је ако није неопходан обилазак кључева у сортираном поретку.
Кад год се објекат додаје у мапу, неопходно је проследити и кључ. У нашем случају, кључ је
стринг, а одговарајућа вредност је објекат класе Radnik.
String s = "987-98-9996";
r = osoblje.get(s); // vraca hari
Ако задати кључ није претходно коришћен за смештање информација у мапу, метод get() враћа
null.
Кључеви морају бити јединствени. Не могу се у мапу сместити две вредности коришћењем истог
кључа. Ако се метод put() позове два пута са истим кључем, друга вредност замењује прву.
Заправо, метод put() враћа вредност претходно смештену коришћењем кључа који му се
проследи као први аргумент.
Метод remove() уклања елемент са датим кључем из мапе. Метод size() враћа број уноса у
мапи.
Мапа се не сматра колекцијом сама по себи. Међутим, могуће је добити тзв. погледе на мапу
(енг. view), тј. објекте који имплементирају интерфејс Collection или неки од његових
подинтерфејса.
26
Постоје три погледа: скуп кључева, колекција вредности (која није скуп) и скуп парова
кључ/вредност. Кључеви и парови кључ/вредност чине скупове јер у мапи може да постоји само
једна копија кључа. Методи:
Set<K> keySet()
Collection<V> values()
Set<Map.Entry<K, V>> entrySet()
враћају ова три погледа. (Елементи скупа уноса су објекти статичке унутрашње класе Map.Entry)
Приметимо да скуп кључева није HashSet нити TreeSet, већ објекат неке друге класе која
имплементира интерфејс Set. Интерфејс Set изведен је из интерфејса Collection. Према томе,
скуп кључева се може користити као и било која друга колекција.
Ако позовете метод remove() за итератор, заправо уклањате из мапе кључ и њему придружену
вредност. Није могуће, међутим, додати елемент у поглед скупа кључева. Нема смисла додати
кључ, а не додати и вредност. Ако се позове метод add(), он избацује
UnsupportedOperationException. Поглед скупа уноса има исто ограничење, иако концептулно
има смисла додати нови пар кључ/вредност.
Пример 5 (MapTest): Најпре се у мапу додају парови кључ/вредност. Затим се уклања један кључ,
што такође уклања и њему придружену вредност. Онда се мења вредност придружена кључу и
позива метод get() да се очита вредност. Најзад, пролази се кроз скуп уноса.
java.util.Map<K, V>
V get(K key)
дохвата вредност придружену задатом кључу; враћа објекат придружем кључу или null ако кључ
није нађен у мапи. Кључ може бити null.
27
void putAll(Map<? extends K, ? extends V> entries)
додаје све уносе из задате у текућу мапу.
Set<K> keySet()
враћа поглед који чини скуп свих кључева мапе. Могуће је уклањати елементе из овог скупа, чиме
се из мапе уклањају и кључеви и њима придружене вредности, али није могуће додавати
елементе у скуп.
Collection<V> values()
враћа поглед који чини колекција свих вредности из мапе. Могуће је уклањати вредности из ове
колекције чиме се уклоњена вредност и њен кључ уклањају из мапе, али није могуће додавати
елементе у колекцију.
java.util.Map.Entry
K getKey()
V getValue()
враћа кључ или вредност текућег уноса.
V setValue(V newValue)
мења вредност у придруженој мапи на нову и враћа стару вредност.
java.util.HashMap<K, V>
HashMap()
HashMap(int initialCapacity)
HashMap(int initialCapacity, float loadFactor)
конструише празну хеш-мапу задатог капацитета и load factor-а (број између 0.0 и 1.0 који
одређује при ком проценту попуњености се хеш-табела рехешира у већу). Подразумевани је load
factor 0.75.
java.util.TreeMap
28
TreeMap(SortedMap<? extends K, ? extends V> entries)
конструише tree map, додаје све уносе из задате сортиране мапе и користи исти компаратор као и
дата сортирана мапа.
java.util.SortedMap<K, V>
K firstKey()
K lastKey()
враћа најмањи или највећи кључ из мапе.
На жалост, ствари нису тако просте. Garbage collector прати „живе“ објекте. Све док је мапа-
објекат жив, сви bucket-и у њему су живи и garbage collector их неће скупљати. Према томе, наш
програм треба да се побрине да уклања некоришћене вредности из „дуговечних“ мапа. Уместо
тога можемо користити WeakHashMap. Ова структура података сарађује са garbage collector-ом
како би уклањала парове кључ/вредност када је једина референца на кључ она из хеш-табеле.
Овако изнутра функционише тај механизам. WeakHashMap користи слабе референце (енг. weak
references) за чување кључева. Објекат класе WeakReference чува референцу на други објекат, у
нашем случају, на кључ хеш-табеле. Garbage collector третира објекте овог типа на посебан начин.
Нормално, ако garbage collector открије да нема референци на неки објекат, просто покупи тај
објекат. Међутим, ако је објекат доступан само преко WeakReference, такође га покупи, али
смешта слабу референцу која је водила до њега у ред. Операције класе WeakHashMap периодично
проверавају тај ред у потрази за новопристиглим слабим референцама. Пристизање нове слабе
референце у ред означава да одговарајући кључ више нико не користи и да га је collector покупио.
WeakHashMap тада уклања придружени унос.
29
Linked Hash Sets and Maps
У Java SE 1.4 додате су класе LinkedHashSet и LinkedHashMap, које памте редослед у ком су
елементи додавани. На тај начин избегава се наизглед случајан поредак елемената хеш-табеле.
Како се уноси умећу у табелу, тако се придружују двоструко повезаној листи:
144-25-5464
567-24-2546
157-62-7935
456-62-5527
Cak Noris
Harison Ford
Gari Kuper
Demi Kucer
30
LinkedHashMap алтернативно може користити редослед приступа (енг. access order) уместо
редоследа додавања за итерирање кроз уносе мапе. Сваки пут када се позове get() или put(),
унос на који се метод односи се уклања са своје текуће позиције у повезаној листи уноса и смешта
на крај те листе. (Само позиција у повезаној листи уноса се мења, не и bucket хеш-табеле. Унос
увек остаје у bucket-у који одговара хеш-вредности његовог кључа.) За конструисање такве хеш-
мапе служи позив облика:
Редослед приступа је користан за имплементирање „least recently used“ политике за кеш. Нпр.
можда желимо да чувамо уносе којима се често приступа у меморији, а оне уносе којима се ређе
приступа да учитавамо из базе података. Када не пронађемо унос у табели, а табела је већ
прилично напуњена, можемо добити итератор за табелу и уклонити први елемент који он обиђе.
Такви уноси су најмање скоро коришћени (least recently used).
Додавање новог уноса тада узрокује да најстарији унос (eldest) буде уклоњен кад год наш метод
врати true. Нпр. величина следећег кеша је увек највише 100 (елемената):
Алтернативно, може се размотрити унос eldest како би се одлучило да ли га уклонити или не.
Нпр. можда желимо да проверимо временску ознаку (енг. time stamp) смештену у унос.
EnumSet је ефикасна имплементација скупа са елементима који припадају типу енумерације. Како
тип енумерације има коначан број инстанци, EnumSet је интерно имплементиран просто као
секвенца битова. Бит је постављен ако је одговарајућа вредност присутна у скупу.
Класа EnumSet нема јавних конструктора. За конструисање скупа користи се статички factory
метод:
31
EnumMap је мапа са кључевима који припадају типу енумерације. Она је просто и ефикасно
имплементирана као низ вредности. Потребно је задати тип кључева у конструктору:
У Java SE 1.4 додата је и класа IdentityHashMap за једну прилично специјализовану употребу, где
се хеш-вредности кључева не рачунају помоћу метода hashCode(), већ методом
System.identityHashCode(). То је метод кога метод hashCode() класе Object користи за
израчунавање хеш-кôда на основу меморијске адресе објекта. Такође, за поређење објеката
IdentityHashMap користи ==, а не метод equals().
Другим речима, различити објекти кључеви сматрају се различитим чак и ако имају исти садржај.
Ова класа је корисна за имплементирање алгоритама за обилажење објеката (попут
серијализације) у којима желимо да пратимо који објекти су већ обиђени.
java.util.WeakHashMap<K, V>
WeakHashMap()
WeakHashMap(int initialCapacity)
WeakHashMap(int initialCapacity, float loadFactor)
конструише празну хеш-мапу задатог капацитета и load factor-а.
java.util.LinkedHashSet
LinkedHashSet()
LinkedHashSet(int initialCapacity)
LinkedHashSet(int initialCapacity, float loadFactor)
конструише празан linked hash set задатог капацитета и load factor-а.
java.util.LinkedHashMap
LinkedHashMap()
LinkedHashMap(int initialCapacity)
LinkedHashMap(int initialCapacity, float loadFactor)
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
конструише празну linked hash map задатог капацитета и load factor-а и редоследа. Параметар
accessOrder је true за редослед приступа, а false за редослед додавања.
32
java.util.EnumSet<E extends Enum<E>>
EnumMap(Class<K> keyType)
констуише празну мапу чији су кључеви датог типа
java.util.IdentityHashMap<K, V>
IdentityHashMap()
IdentityHashMap(int expectedMaxSize)
конструише празну identity hash map чији је капацитет најмањи степен двојке већи од
1.5 * expectedMaxSize. Подразумевана вредност за expectedMaxSize је 21.
java.lang.System
Јавина библиотека колекција чини framework за колекцијске класе. Она дефинише известан број
интерфејса и апстрактних класа за имплементаторе колекција (слика) и прописује одређене
механизме, попут протокола за итерирање. Могуће је користити колекцијске класе без много
знања о framework-у, што је већ и рађено у претходним секцијама. Међутим, ако желите да
имплементирате генеричке алгоритме који раде са већим бројем колекцијских типова или желите
да додате нови колекцијски тип, разумевање framework-a је од помоћи.
33
Постоје два фундаментална интерфејса за колекције: Collection и Map. Елементи се додају у
колекцију методом:
Међутим, мапе чувају парове кључ/вредност, који се у мапу стављају методом put():
V get(K key)
List je уређена колекција. Елементи се додају на одређену позицију унутар контејнера. Објекат
може бити смештен на своју позицију на два начина: помоћу целобројног индекса и помоћу list
iterator-а. Интерфејс List дефинише методе случајног приступа:
Као што је већ речено, интерфејс List обезбеђује ове методе случајног приступа без обзира на то
да ли су они ефикасни или не за одређену имплементацију. Како би се избегло извршавање
скупих операција случајног приступа, Java SE 1.4 уводи интерфејс RandomAccess. То је интерфејс
34
без метода који се може користити за тестирање да ли одређена колекција подржава ефикасан
случајан приступ:
if (c instanceof RandomAccess) {
koristite algoritam slucajnog pristupa
} else {
koristite algoritam sekvencijalnog pristupa
}
Са теоретске тачке гледишта имало би смисла да постоји посебан интерфејс Array који је изведен
из интерфејса List и декларише методе случајног приступа. Да постоји такав интерфејс Array,
алгоритми који захтевају случајан приступ користили би параметре типа Array и не би се могло
десити да их случајно применимо на колекције са спорим случајним приступом. Међутим,
дизајнери framework-a изабрали су да не дефинишу посебан интерфејс јер су желели да одрже
број интерфејса у библиотеци малим. Можете да проследите повезану листу алгоритмима који
користе случајан приступ - само треба да будете свесни утицаја који то има на перформансе.
Зашто креирати посебан интерфејс када су потписи метода исти? Концептуално, нису све
колекције скупови. Тиме што је Set учињен интерфејсом, програмерима је омогућено да пишу
методе који прихватају само скупове.
Коначно, Java SE 6 уводи интерфејсе NavigableSet и NavigableMap који садрже додатне методе
за претраживање и обилазак сортираних скупова и мапа. Класе TreeSet и TreeMap
имплементирају ове интерфејсе.
35
AbstractCollection
AbstractList
AbstractSequentialList
AbstractSet
AbstractQueue
AbstractMap
LinkedList
ArrayList
ArrayDeque
HashSet
TreeSet
PriorityQueue
HashMap
TreeMap
Коначно, известан број „legacy“ контејнерских класа присутан је од настанка Јаве, пре него што је
настао collections framework:
Vector
Stack
Hashtable
Properties
36
Ове класе интегрисане су у collections framework (слика). О њима ће бити речи касније.
Гледајући претходне слике, рекло би се да има превише интерфејса и апстрактних класа како би
се имплементирао скроман број конкретних класа. Међутим, слике не причају читаву причу.
Коришћењем погледа (енг. view) могу се добити други објекти који имплементирају интерфејсе
Collection и Map. Видели смо такав пример када смо користили метод keySet() за мапу. На
први поглед делује да метод креира нови скуп, пуни га кључевима из мапе и враћа га. Међутим,
то није случај. Метод keySet() враћа објекат класе која имплементира интерфејс Set и чији
методи манипулишу оригиналном мапом. Таква колекција назива се погледом.
Техника погледа има корисне примене у collections framework-у које ће бити описане у наредним
секцијама.
Статички метод asList() класе Arrays враћа List омотач (енг. wrapper) за обичан Јава низ. Овај
метод омогућује да се низ проследи методу који као аргумент очекује листу или колекцију. Нпр.
37
Враћени објекат није ArrayList. То је објекат поглед са методима get() и set() који приступају
низу. Сви методи који би променили величину низа (попут метода add() и remove()
придруженог итератора) избацују UnsupportedOperationException.
Почев од Java SE 5.0 метод asList() је декларисан тако да има променљив број аргумената.
Уместо прослеђивања низа, такође се могу проследити индивидуални елементи. Нпр.
Позив
Collections.nCopies(n, objekat)
враћа непроменљиви објекат који имплементира интерфејс List и даје илузију да има n
елемената од којих сваки изгледа као objekat.
Нпр. следећи позив креира List који садржи 100 стрингова, свих постављених на "DEFAULT":
Цена смештања је веома мала - објекат се смешта само једанпут. Ово је симпатична примена
технике погледа.
Класа Collections садржи известан број utility метода са параметрима или повратним
вредностима које су колекције. Не мешати је са интерфејсом Collection.
Позив
Collections.singleton(objekat);
враћа поглед-објекат који имплементира интерфејс Set (за разлику од ncopies(), који
производи List). Враћени објекат имплементира непроменљиви скуп који садржи један објекат.
Методи singletonList() и singletonMap() се понашају слично.
Subranges
За неке колекције могуће је формирати „подопсег“ погледе. Нпр. претпоставимо да имамо листу
osoblje и желимо да издвојимо елементе од 10 до 19. Можемо користити метод subList()да
добијемо поглед на подопсег листе:
Први индекс је укључујући, а други искључујући - управо као код параметара метода
substring() класе String. На подопсег се могу примењивати произвољне операције и оне се
аутоматски одражавају на листу. Нпр. можемо обрисати читав подопсег:
Ови методи враћају подскупове елемената који су већи или једнаки од from и строго мањи од to.
За сортиране мапе, слични методи
враћају погледе на мапу који се састоје од свих уноса у којима кључеви припадају задатим
опсезима.
Интерфејс NavigableSet, уведен у Java SE 6, даје више контроле над операцијама за подопсеге.
Могуће је задати да ли су границе укључене:
Unmodifiable Views
Класа Collections поседује методе који производе непроменљиве погледе на колекције. Ови
погледи додају runtime провере постојеће колекције. Ако се детектује покушај модификовања
колекције, избацује се изузетак и колекција остаје нетакнута.
Collections.unmodifiableCollection()
Collections.unmodifiableList()
Collections.unmodifiableSet()
Collections.unmodifiableSortedSet()
Collections.unmodifiableMap()
Collections.unmodifiableSortedMap()
Нпр. претпоставимо да желите да допустите неком делу свог кôда да погледа, али „не дира“,
садржај колекције. Ево шта је потребно да урадите:
List<String> osoblje = new LinkedList<String>();
…
pogledaj(new Collections.unmodifiableList(osoblje));
39
Метод Collections.unmodifiableList() враћа објекат класе која имплементира интерфејс
List. Његови приступни (енг. accessor) методи дохватају вредности из колекције osoblje.
Наравно, метод pogledaj() може позивати све методе интерфејса List, не само приступне. Али
сви мутатор-методи, као што је метод add(), предефинисани су тако да избаце изузетак типа
UnsupportedOperationException уместо да проследе позив полазној колекцији.
Непроменљиви поглед не чини саму колекцију непроменљивом. Колекција се још увек може
модификовати преко своје оригиналне референце (osoblje, у нашем случају). Такође, још увек је
могуће позивати мутатор-методе над елементима колекције.
Како погледи омотавају интерфејс, а не стварни објекат колекцију, постоји приступ само оним
методима који су дефинисани у интерфејсу. Нпр. класа LinkedList поседује погодне методе
addFirst() и addLast(), који нису део интерфејса List. Ови методи нису доступни преко
непроменљивог погледа.
Synchronized Views
Ако колекцији приступа већи број нити, неопходно је обезбедити да се колекција случајно не
оштети. Нпр. било би катастрофално када би једна нит покушавала да дода унос у хеш-табелу док
друга врши рехеширање елемената.
Сада је могуће приступати објекту map из већег броја нити. Методи попут get() и put() су
серијализовани - сваки позив метода мора се завршити у потпуности пре него што друга нит може
позвати други метод. Проблем синхронизације приступа структурама података детаљно се
разматра у следећем, 14. поглављу.
Checked Views
У Java SE 5.0 додат је скуп „проверених“ погледа намењених подршци за отклањање проблема
који могу настати са генеричким типовима. Могуће је „прошверцовати“ елементе погрешног типа
у генеричку колекцију. Нпр.
40
ArrayList<String> stringovi = new ArrayList<String>();
ArrayList samoLista = stringovi;
// dobija se samo upozorenje, ne greska,
// zbog kompatiblinosti sa legacy kodom
samoLista.add(new Date()); // sada stringovi sadrzi Date objekat!
Проверени поглед може детектовати овај проблем. Дефинишите безбедну листу на следећи
начин:
Метод add() погледа проверава да ли убачени објекат припада датој класи и непосредно
избацује ClassCastException ако то није случај. Предност је што се грешка пријављује на
исправној локацији:
ОПРЕЗ: Проверени погледи су ограничени runtime проверама које ВМ може извршавати. Нпр. ако
имате ArrayList<Par<String>>, не можете га заштитити од додавања Par<Date> јер ВМ
поседује једну „raw“ класу Par (Детаљно објашњење налази се у поглављу 12 књиге).
Поглед обично има неко ограничење - може бити само за читање, нема могућност да промени
величину колекције или може подржавати уклањање, али не и додавање, као што је случај са
погледом кључева мапе. Такав поглед избацује UnsupportedOperationException ако се
покуша неодговарајућа операција.
Треба ли користити технику „опционих“ метода у сопственом дизајну? Аутори књиге сматрају да
не треба. Иако се колекције често користе, стил кодирања за њихову имплементацију није
типичан за проблеме из других домена. Дизајнери библиотеке колекцијских класа морају да
разеше посебно бруталан скуп конфликтних захтева. Корисници желе библиотеку која је лака за
учење, погодна за употребу, потпуно генеричка, а у исто време ефикасна као и ручно кодирани
алгоритми. Просто је немогуће симултано постићи све ове захтеве, чак ни прићи близу томе. Али,
у својим сопственим проблемима тешко да ћете наићи на тако екстреман скуп услова. Требало би
да можете да нађете решења која се не заснивају на екстремној мери „опционих“ операција
интерфејса.
41
java.util.Collections
java.util.Arrays
java.util.List<E>
java.util.SortedSet<E>
42
java.util.NavigableSet<E>
java.util.SortedMap<K, V>
java.util.NavigableMap<K, V>
До сада је већина наших примера користила итератор за обилазак колекције, елемент по елемент.
Међутим, често је могуће избећи итерирање коришћењем неке од „bulk“ операција из
библиотеке.
Претпоставимо да желимо да нађемо пресек два скупа, тј. њихове заједничке елементе. Прво
направимо нови скуп који ће бити резултат.
Овде користимо чињеницу да свака колекција има конструктор чији је параметар друга колекција
са иницијализационим вредностима.
result.retainAll(b);
Он задржава све елементе који се појављују и у b. Пресек је формиран без програмирања петље.
Ова идеја се може даље развити тако што се bulk операција примени на поглед. Нпр.
претпоставимо да имамо мапу која мапира ID запослених на запослене и имамо скуп ID-јева
запослених које треба уклонити из мапе.
43
Просто се формира скуп кључева мапе и уклоне сви ID-јеви запослених које треба уклонити из
мапе:
osobljeMapa.keySet().removeAll(uPenzijuID);
Како је скуп кључева поглед на мапу, кључеви и придружени запослени се аутоматски уклањају из
мапе.
premesteni.addAll(osoblje.subList(0, 10));
osoblje.subList(0, 10).clear();
String[] vrednosti = …;
HashSet<String> osoblje = new HashSet<String>(Arrays.asList(vrednosti));
Добијање низа од колекције је мало теже. Наравно, могуће је користити метод toArray():
Међутим, резултујући низ је низ објеката. Чак и ако знамо да наша колекција садржи објекте
одређеног типа, не можемо користити кастовање:
Низ који враћа toArray()креиран је као Object[] и није могуће променити његов тип. Уместо
тога, може се користити друга верзија метода toArray()којој се проследи низ дужине 0 типа који
желимо. Враћени низ ће онда бити тог типа:
osoblje.toArray(new String[osoblje.size()]);
44
Алгоритми
if (a.length == 0)
throw new NoSuchElementException();
T largest = a[0];
for (int i = 1; i < a.length; i++)
if(largest.compareTo(a[i]) < 0)
largest = a[i];
if(v.size() == 0)
throw new NoSuchElementException();
T largest = v.get(0);
for (int i = 1; i < v.size(); i++)
if(largest.compareTo(v.get(i)) < 0)
largest = v.get(i);
Шта је са повезаном листом? У повезаној листи немамо ефикасан случајан приступ, али можемо
користити итератор:
if (l.isEmpty())
throw new NoSuchElementException();
Iterator<T> iter = l.iterator();
T largest = iter.next();
while(iter.hasNext()){
T next = iter.next();
if(largest.compareTo(next) < 0)
largest = next;
}
Ове петље су напорне за писање и склоне су грешкама. Да ли се извршава итерација више или
мање? Да ли петље раде исправно када је контејнер празан? Када има само један елемент? Не
желите да тестирате програм сваки пут, а такође не желите да имплементирате мноштво метода
попут:
Сада можемо да рачунамо максимум повезане листе, array list и низа коришћењем једног метода.
Сортирање и мешање
Метод sort() класе Collections сортира колекцију која имплементира интерфејс List.
Овај метод претпоставља да класа елемената листе имплементира интерфејс Comparable. Ако
желите да сортирате листу на неки други начин, можете проследити Comparator објекат као
други аргумент:
Collections.sort(osoblje, Collections.reverseOrder())
сортира елементе листе osoblje у обрнутом поретку у односу на поредак одређен методом
compareTo() класе елемената листе. Слично,
Collections.sort(artikli, Collections.reverseOreder(artikalComparator))
46
Можете се запитати како метод sort() сортира листу? Типично, када погледате алгоритме са
сортирање у књигама, они су презентовани за низове и користе случајан приступ елементима.
Међутим, случајан приступ у листи може бити неефикасан. Листе се могу сортирати коришћењем
merge sort алгоритма. Ипак, имплементација у Јави не ради то. Она просто прекопира све
елементе у низ, сортира га користећи варијанту merge sort алгоритма, а затим копира сортирану
секвенцу натраг у листу.
Merge sort алгоритам коришћен у библиотеци је за нијансу спорији од quick sort, који је
традиционални избор алгоритма опште намене за сортирање. Међутим, merge sort има једну
велику предност: стабилан је, тј. не врши размену једнаких елемената. Зашто нам је битан
поредак једнаких елемената? Следи уобичајени сценарио. Претпоставимо да имамо листу
запослених која је већ сортирана по имену. Сада сортирамо по заради. Шта се дешава са
запосленима који имају исту зараду? Када је сортирање стабилно, поредак по имену бива очуван.
Другим речима, излаз је листа сортирана најпре по заради, а потом по имену.
Како колекције не морају имплементирати све своје „опционе“ методе, сви методи који имају
параметре типа колекција морају описати да ли је безбедно проследити колекцију алгоритму.
Нпр. јасно је да не можемо проследити unmodifiableList методу sort(). Коју врсту листи
можемо проследити? Према документацији, листа треба да буде таква да ју је могуће
модификовати (modifiable), али не мора бити могуће мењање њене димензије (не мора да буде
resizable). Ови термини дефинисани су на следећи начин:
- Листа је modifiable ако подржава метод set().
- Листа је resizable ако подржава операције add() и remove().
Класа Collections поседује алгоритам shuffle који ради супротно од сортирања - случајно
пермутује редослед елемената у листи. Нпр.
ArrayList<Karta> karte = …;
Collections.shuffle(karte);
Ако проследимо листу која не имплементира интерфејс RandomAccess, метод shuffle() копира
елементе у низ, промеша низ и ископира промешане елементе натраг у листу.
java.util.Collections
47
static <T> Comparator<T> reverseOrder()
враћа компаратор који сортира елементе у обрнутом поретку од поретка одређеног методом
compareTo() интерфејса Comparable.
Бинарна претрага
i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);
Повратна вредност ≥0 означава индекс пронађеног елемента, тј. c.get(i) једнако је са element
у складу са коришћеним поређењем. Ако је повратна вредност негативна, не постоји тражени
елемент у колекцији. Међутим, повратна вредност се може користити за израчунавање позиције
на коју треба уметнути element у колекцију како би она остала сортирана. Та позиција је:
insertionPoint = -i - 1;
Није просто -i јер онда би вредност 0 била двосмислена. Другим речима, операција
if ( i < 0)
c.add(-i - 1, element);
Како би имала смисла, бинарна претрага захтева случајан приступ. Уколико морате да итерирате
елемент по елемент кроз половину повезане листе како бисте нашли средишњи елемент,
изгубили сте сву предност бинарне претраге. Према томе, binarySearch() се своди на линеарну
претрагу када му дате повезану листу.
static <T extends Comprable<? super T>> int binarySearch(List<T> elements, T key)
static <T> int binarySearch(List<T> elements, T key, Comparator<? super T> c)
тражи кључ у сортираној листи, користећи линеарну претрагу ако elements не имплементира
RandomAccess интерфејс, а бинарну у супротном. Временска сложеност алгоритма је O(a(n) log
n), где је n дужина листе, а a(n) просечно време приступа елементу. Методи враћају или индекс
кључа у листи или негативну вредност i ако кључ није присутан у листи. У том случају, кључ треба
убацити у листу на позицију -i - 1 како би листа остала сортирана.
Једноставни алгоритми
Класа Collections садржи неколико једноставних, али корисних алгоритама. Међу њима је и
пример са почетка ове секције, налажење максималне вредности у колекцији. Остали укључују
копирање елемената из једне листе у другу, попуњавање контејнера константном вредношћу и
обртање листе. Зашто обезбедити тако једноставне алгоритме у стандардној библиотеци? Свакако
да их већина програмера може лако имплементирати једноставним петљама. Ови алгоритми чине
живот лакшим програмеру који чита кôд. Када читате петљу коју је имплементирао неко други,
неопходно је да дешифрујете његове намере. Када видите позив метода попут
Collections.max(), истог тренутка знате шта тај кôд ради.
java.util.Collections
49
static void reverse(List<?> l)
обрће редослед елемената у листи. Временска сложеност алгоритма је O(n), где је n дужина листе.
Ако сами пишете алгоритам (или, заправо, произвољан метод који има колекцију као параметар),
требало би да радите са интерфејсима уместо са конкретним имплементацијама кад год је то
могуће. Нпр. претпоставимо да желите да попуните JМenu скупом ставки. Традиционално, такав
метод могао би бити имплементиран на следећи начин:
Међутим, тиме сте условили позиваоца свог метода - он мора сместити ставке у ArrayList. Ако
се деси да су оне већ у неком другом контејнеру, најпре се морају препаковати. Много је боље
прихватити општију колекцију.
Потребно је запитати се: Који је најопштији колекцијски интерфејс који се може употребити? У
овом случају неопходно је само посетити све елементе, што је могућност коју пружа основни
интерфејс Collection. Овако изгледа метод popuniMeni() преписан тако да прихвата
произвољну врсту колекције:
Сада свако може позивати овај метод са ArrayList, LinkedList или чак са низом уз коришћење
омотача Arrays.asList().
Ако је идеја коришћења колекцијских интерфејса као параметара метода тако добра, зашто је
Јавина библиотека не користи чешће? Нпр. класа JComboBox има два конструктора:
JComboBox(Object[] items)
JComboBox(Vector<?> items)
Разлог је просто време. Swing библиотека настала је пре библиотеке колекција.
50
Ако пишете метод који враћа колекцију, такође можда желите да вратите интерфејс уместо класе
јер се можете предомислити и касније реимплементирати метод користећи другу колекцију.
Касније можете одлучити да не желите да копирате ставке, већ просто да обезбедите поглед на
њих. Ово се може постићи враћањем објекта анонимне поткласе класе AbstractList.
Наравно, ово је напредна техника. Ако је примените, пажљиво документујте које „опционе“
операције су подржане. У овом случају морате обавестити позиваоца да је враћени објекат
непроменљива листа.
Legacy Collections
У овој секцији разматрају се колекцијске класе које постоје у програмском језику Јава од самог
почетка: класа Hashtable и њена корисна поткласа Properties, поткласа Stack класе Vector и
класа BitSet.
Класа Hashtable
Класа Hashtable има исту намену као и HashMap и има исти интерфејс. Попут метода класе
Vector, методи класе Hashtable су синхронизовани. Ако Вам није потребна синхронизација, као
ни компатибилност са legacy кодом, требало би да, уместо ове класе, користите класу HashMap.
51
Енумерације
Нпр. метод elements() класе Hashtable враћа објекат за енумерисање вредности из табеле:
Enumeration<Radnik> r = osoblje.elements();
while (r.hasMoreElements()) {
Radnik radnik = r.nextElement();
…
}
Повремено ћете наићи на legacy метод који очекује параметар типа Enumeration. Статички метод
Collections.enumeration() враћа Enumeration објекат који енумерише елементе колекције.
Нпр.
List<InputStream> streams = …;
SequenceInputStream in =
new SequenceInputStream(Collections.enumeration(streams));
// konstruktor SequenceInputStream()ocekuje enumeraciju
У програмском језику C++ је прилично уобичајено користити итераторе као параметре метода. На
срећу, у Јави веома мали број програмера тако ради. Много је паметније проследити колекцију
него итератор. Објекат који представља колекцију је много кориснији. Прималац може увек да
добије итератор од колекције када за тим има потребе, а , сем тога, има на располагању и све
методе колекције. Међутим, наћи ћете енумерације у legacy кôду јер су оне биле једини доступан
механизам за генеричке колекције до појаве collections framework-а у Јava SE 1.2.
java.util.Enumeration<E>
boolean hasMoreElements()
враћа true ако има још елемената који нису посећени.
E nextElement()
враћа наредни елемент. Не звати га ако је hasMoreElements()вратио false.
java.util.Hashtable<K, V>
Enumeration<K> keys()
враћа објекат енумерацију који обилази кључеве хеш-табеле.
Enumeration<V> elements()
враћа објекат енумерацију који обилази елементе хеш-табеле.
java.util.Vector<E>
Enumeration<E> elements()
враћа објекат енумерацију који обилази елементе вектора.
52
Property Maps
Property map је мапа веома специјалног типа. Има три посебне карактеристике:
- кључеви и вредности су стрингови
- табела може бити сачувана у фајл и учитана из њега
- друга табела се користи за подразумеване вредности.
java.util.Properties
Properties()
креира празну property map.
Properties(Properties defaults)
креира празну property map са задатим подразумеваним вредностима.
Stacks
Од верзије 1.0 стандардна библиотека има класу Stack са познатим методима push() и pop().
Међутим, класа Stack изведена је из класе Vector, што није задовољавајуће гледано из
теоретске перспективе - могуће је применити не-стековске операције попут insert() и
remove() и тако додати или уклонити вредност са произвољне позиције, а не само са врха стека.
java.util.Stack<E>
E push(E item)
ставља item на стек и враћа item.
E pop()
скида и враћа елемент са врха стека. Не позивати овај метод ако је стек празан.
E peek()
враћа елемент са врха стека, не скидајући га са стека. Не позивати овај метод ако је стек празан.
53
BitSets
Објекат класе BitSet чува секвенцу битова. (То није скуп у математичком смислу - прикладнији
појмови били би бит-вектор или бит-низ.) Користите га ако треба ефикасно да чувате секвенцу
битова (нпр. флегова). Како он пакује битове у бајтове, далеко је ефикасније користити њега него
ArrayList објеката типа Boolean.
Класа BitSet нуди погодан интерфејс за читање, постављање и ресетовање појединачних битова.
Употребом овог интерфејса избегава се „маскирање“ које би било неопходно када би се битови
чували у променљивама типа int или long.
bucketOfBits.get(i)
bucketOfBits.set(i)
bucketOfBits.clear(i)
java.util.BitSet
BitSet(int initialCapacity)
конструише bit set.
int length()
враћа „логичку дужину“: 1 плус индекс бита највише тежине који је постављен.
Као пример коришћења bit set-ова приказана је имплементација Ератостеновог сита, алгоритма за
проналажење простих бројева. (Број је прост ако је дељив само самим собом и бројем 1, а
Ератостеново сито је било један од првих откривених алгоритама за њихову енумерацију.)
Алгоритам није нарочито добар, али је из неког разлога постао популаран benchmark за
перформансе компајлера (није добар ни као benchmark јер углавном тестира битске операције).
Без залажења у превише детаља, кључно је проћи кроз bit set са 2 милиона битова. Најпре се сви
битови укључе. Након тога искључују се битови који представљају умношке бројева за које је
познато да су прости. Позиције битова који остану укључени након овог процеса представљају
просте бројеве.
ПРИМЕДБА: Иако Ератостеново сито није добар benchmark, аутори нису могли да одоле, па су
упоредили време извршавања Јава и C++ имплементације алгоритма. Тестирање је извршено на
1.66-GHz dual core ThinkPad са 2GB RAM-a под оперативним системом Ubuntu 7.04.
Ако се подигне ниво оптимизације C++ компајлера, он побеђује Јаву са временом од 60 ms. Јава
то може достићи једино ако се програм извршава довољно дуго да се покрене Hotspot just-in-time
компајлер.
55