11. praktikum. Kogumid (Collections)
Teemad
Geneerilised liidesed ja klassid, List, Map, Stack, Comparable
Pärast selle praktikumi läbimist oskab üliõpilane kasutada geneerilisi tüüpe ja põhilisi andmestruktuure.
Kogumid
Kogumid on andmestruktuurid, mis on mõeldud mingi hulga objektide hoidmiseks. Erinevus "tavalistest" objektidest (nt. Auto, mille sees on Mootor) on selles, et kogumis sisalduvate objektide arv ei pea olema fikseeritud. Inglise keeles kasutatakse kogumite kohta erinevaid nimetusi: Collection, Aggregate, Container.
On olemas erinevat liiki kogumeid, nt. massiiv (array), list (list), hulk (set), magasin (stack), kujutus (map), järjekord (queue). (Terminoloogia ei ole siin väga üheselt paigas. Erinevates käsitlustes võib leida erinevaid termineid samade asjade kohta (nt. Tallinnas öeldakse magasini asemel pinu.) Samuti võib sama termini kasutamises olla väiksemaid erinevusi.) Erinevat liiki kogumitel on ühiseid omadusi, aga on ka erinevusi, mistõttu erinevates situatsioonides kasutatakse erinevat liiki kogumeid. Erinevused võivad olla väga erinevates asjades: korduvate elementide lubamine, elementide muudetavus, elementide arvu muudetavus, lisamise, eemaldamise eripärad ...
Käesolev praktikum keskendub Java võimalustele kogumitega seotult. Kuna tegemist on väga mitmetahulise temaatikaga, siis paraku saab siin vaid teatud osa käsitleda. Mitmed andmestruktuurid on Javas kasutusel olnud juba esimestest versioonides peale. Viimastes versioonides on püütud kogumeid süstemaatiliselt käsitleda ja asjassepuutuv koondada ühte raamistikku - Java Collections Framework. Hea ülevaade on Java veebilehestikus.
Java Collections Framework on ülesehitatud liidestest, klassidest (ka abstraktsetest). Liidestes määratakse teatud kogumiliigi (nt. list või järjekord) jaoks vajalikud meetodid, mis siis konkreetsetes klassides realiseeritakse. Liidestes Collection ja Map on universaalsemad meetodid, alamliidestes tuuakse juurde spetsiifilisemad. Põhiliideste hierarhia on toodud joonisel. (Tegelikult on liideseid märksa rohkem.)
Geneerilised tüübid
Kogumeid läheb vaja erinevat tüüpi objektide hoidmiseks. Põhimõtteliselt võiks neid kõiki käsitleda Object-tüüpi elementidena. Tavaliselt on siiski kogumis teatud tüüpi elemendid ja kasulik on võimalikult vara (näiteks kompileerimise ajal) teada saada, kui sinna midagi muud püütakse panna. Siin oleks abiks see, kui saaks määrata, millisele tüübile antud kogum mõeldud on. Seda ideed viivadki ellu geneerilised liidesed ja klassid, kus tüübimuutuja(te) abil saame vajalikud määramised teha. Näiteks List<Integer> on mõeldud täisarvudele ja List<String> sõnedele.
Geneerilised tüübid (e geneerikud) on Javas alates versioonist 1.5 (väljalase aastal 2004). Nii kohtab ka koodi, kus kogumi tüüpi (nt. List) on kasutatud ilma täpsustava tüübiparameetrita. Taolisi tüüpe nimetatakse tooreteks (raw). Nende kasutamise korral on programmeerija ülesanne pidada meeles, mis tüüpi objektid kogumis on, Java arvates on sel juhul elementide tüüp Object.
Hea ülevaade geneerikutest on Java veebilehestikus.
Massiiv on mõnes mõttes kõige olulisem kogumitüüp - paljud teised kogumid on realiseeritud kasutades massiivi. Massiivi kasutamine on mugav juhul, kui elementide arv on massiivi loomisel teada ja see ei muutu. Kuna massiivi elemendid hoitakse arvuti mälus järjest, siis massiivi suurendamiseks on tarvis leida uus ja suurem vaba mälukoht ning vana massiivi elemendid sinna "ümber kolida".
Käesolevas praktikumis me otseselt massiiviga ei tegele, oleme ju seda varem teinud.
List (ArrayList, LinkedList)
Listis on elemendid teatud järjestuses. Põhilise erinevusena massiivist saab listi pikkust programmi töö käigus vabalt muuta, elemente saab erinevatesse kohtadesse lisada ja neid eemaldada. Listile omased operatsioonid on määratud liideses java.util.List. Konkreetse listi loomisel tuleb kasutada mõnd klassi, mis selle liidese realiseerib (nt. ArrayList, LinkedList).
Näide:
import java.util.List; |
Põhilised toimingud, mida saab listiga teha:
elemendi võtmine indeksi järgi, nt. list.get(2) (NB! indeksid algavad 0-st)
elemendi lisamine listi lõppu ( list.add(780); ) või määratud positsioonile ( list.add(3, 780); )
mitme elemendi lisamine ( list.addAll(mingiTeineList); )
elemendi eemaldamine indeksi järgi (list.remove(3))
sisalduvuse kontroll (list.contains(780))
sisalduvuse kontroll koos indeksi tagastamisega (list.indexOf(780))
pikkuse küsimine (list.size())
NB! erinevalt massiividest, ei saa listid (ega ka teised allpoolmainitud andmestruktuurid) sisaldada Java algtüüpi elemente (nt. int, boolean, char), nende asemel tuleb kasutada vastavaid mähisklasse (nt. Integer, Boolean, Character).
Näide - listi koostamine
import java.util.List; |
alternatiivsed variandid sama tulemuse jaoks:
|
Näide - listi ümberpööramine
Järgnev on näide geneerilisest meetodist:
public static <T> List<T> reverse(List<T> list) { |
Alustame selle meetodi signatuuri uurimist vasakult:
"<T>" ütleb, et "T" on järgnevas meetodis tüübiparameeter, st. nii meetodi signatuuris kui ka kehas ei ole T all mõeldud mitte konkreetset tüüpi, vaid T asendatakse konkreetse tüübiga meetodi välja kutsumisel.
"List<T>" ütleb, et meetod tagastab mingi listi, mille elementide tüüp vastab eelpoolkirjeldatud T-le.
"(List<T> list)" ütleb, et argumendiks tuleb anda list elemendi tüübiga T. Kuna esimene punkt ütles, et T on tüübimuutuja, siis seetõttu võib argumendiks anda suvaliste objektide listi. Signatuur kindlustab selle, et tagastatavas listis on sama tüüpi objektid.
Kui kasutame seda meetodit järgmiselt:
|
siis T asendatakse meetodis igal pool Integer-iga.
Liidese List realisatsioonid
ArrayList on enamasti kõige efektiivsem viis listide realiseerimiseks - elementide hoidmiseks kasutatakse massiivi.
LinkedList hoiab oma elemente mälus eraldi, kasutades listi kooshoidmiseks viitasid. LinkedList on efektiivne, kui on tarvis tihti lisada andmeid listi etteotsa või keskele (või kui on vaja elemente sealt eemaldada). Samas elemendi võtmine indeksi järgi on aeglasem kui ArrayList-i puhul.
|
Vector on peaaegu sama kui ArrayList, erinevused tulevad välja mitmelõimeliste programmide puhul. Ühelõimeliste programmide puhul soovitatakse kasutada ArrayListi.
MagasinMagasin (stack) on listil põhinev andmestruktuur, mille põhimeetoditeks on pop ja push. Meetod push(x) lisab elemendi x listi lõppu (magasini tippu). Meetod pop() eemaldab magasini tipuelemendi (listi viimase elemendi) ja tagastab selle.
Javas on taolise funktsionaalsuse jaoks olemas ka klass Stack, aga soovitatakse kasutada hoopis liidest Deque (, mis on Queue alamliides) ja realisatsioonina klasse ArrayDeque (või LinkedList). Juhime tähelepanu, et LinkedList realiseerib nii liidest List kui ka Deque. Deque on Stack-ist pisut universaalsem - lubab lisada ka listi etteotsa.
Järgnev näitekood prindib "321"
|
Viimasena magasini pandud elemendid pääsevad välja kõige esimesena, seetõttu kasutatakse selle andmestruktuuri kohta ka nimetust LIFO (last-in-first-out).
Järjekord (Queue)
Järjekorra (queue) all mõeldakse (enamasti) järjestatud elementide kogumit, kusjuures elemente lisatakse (add või offer) alati järjendi lõppu ja neid eemaldatakse (remove või poll) järjendi algusest. Siit tuleb ka nimetus FIFO (first-in-first-out).
|
Java liides Queue on veidi üldisem kui FIFO - realiseeriv klass otsustab, kuhu lisatud element satub. Nt. eelisjärjekorra puhul järjestatakse järjendi elemendid igal lisamisel uuesti.
Liides Comparable
Liidesest Comparable oli juttu ka 6. praktikumis. Sealsetes näidetes kasutati Comparable'it raw-tüübina - st. ei täpsustatud, millist tüüpi objektidega on võrdlemine lubatud. Tulemus oli see, et võrrelda võis kõikide objektidega (tüüp Object).
Alates Java 1.5-st saab seda liidest kasutada ka geneerilisena. Muudetud näide 6. praktikumist oleks järgmine:
class Isik implements Comparable<Isik> { private String nimi; // isendiväli isiku nime jaoks Isik(String isikuNimi, double isikuPikkus) { double getPikkus() { public int compareTo(Isik isik) { public String toString() { |
Muudatused:
"implements Comparable" asemel on "implements Comparable<Isik>"
"public int compareTo(Object o)" asemel "public int compareTo(Isik isik)"
tüübiteisendused Object->Isik pole enam vajalikud ja on eemaldatud
NB! See, et võrrelda saab vaid teiste Isik-utega, on antud juhul vaba valik, tüübiparameetriks võib anda suvalise tüübi (kasvõi Object, sel juhul oleks programmi tähendus sama nagu algsel, raw-tüübiga programmil)
Ülesanne 1Tutvuda klassiga PriorityQueue (eelisjärjekord).
Kujutus (Map)
Kujutus sisaldab endas kaheosalisi kirjeid, mille esimest komponenti nimetatakse võtmeks ja teist väärtuseks. Ühes kujutuses saab sama võtmega olla vaid üks kirje. Kujutust kasutatakse siis, kui mingi identifikaatori järgi on vaja kiiresti leida üles sellele vastav info. Kujutuse (liidese Map) realisatsiooniks on parim valik enamasti HashMap.
Järgnevas näites kasutatakse HashMapi telefoniraamatu imiteerimiseks:
|
Paneme tähele, et seekord on mängus kaks tüübiparameetrit, üks võtme jaoks (String) ja teine väärtuse jaoks (Integer).
Kui 'get'-iga küsitakse mingit elementi, mida kujutuses ei leidu, siis tagastatakse null.
Kujutuse
kohta võidakse öelda veel sõnastik
(dictionary on Pythoni vastav andmestruktuur)
või
paisktabel (hashtable).
Ülesanne 2
Kasutades kujutust, kirjutada programm, mis loeb sisse mingi tekstifaili ning leiab failis olevate sõnade esinemiste arvud ja kuvab need lõpuks ekraanile.
Iga sõna puhul kontrollida, kas see juba võtmena kujutuses. Kui ei ole, siis lisada koos väärtusega 1. Kui on, siis jätta väärtus meelde ja lisada sõna koos ühe võrra suurema väärtusega. (Juba olemasoleva võtmega kirje lisamisel väärtus asendatakse.)
Järgnev ülesanne on mõeldud eelkõige magasini ja kujutuse kasutamise harjutamiseks. Avaldise väärtustamise punkt võib tunduda esmapilgul keeruline, aga lahendus on tegelikult üpris lihtne.
Tutvuda
pööratud poola notatsiooniga (Reverse Polish Notation)
http://en.wikipedia.org/wiki/Reverse_Polish_notation
http://courses.cs.ut.ee/2009/programmeerimine/uploads/Main/Arvutipraksid/VIpraktikum.htm
Kirjutada meetod, mis küsib kasutajalt RPN kujul kirjutatud aritmeetilise avaldise, jagab avaldise tühikute kohalt juppideks ja tagastab saadud jupid sõnede listina.
Kirjutada meetod, mis võtab argumendiks List-i salvestatud RPN avaldise jupid ning arvutab avaldise väärtuse, kasutades seejuures Integer-ide magasini
Kombineerida saadud meetodid töötavaks RPN kalkulaatoriks
Kirjutada meetod, mis küsib kasutajalt muutujanimesid ja neile vastavaid väärtusi (täisarvud) ja salvestab need Map<String, Integer> tüüpi andmestruktuuri, ning tagastab saadud kujutuse
Täiustada kalkulaatorit nii, et see lubaks avaldises ka muutujaid, mille väärtused võetakse eelmises punktis koostatud Map-ist
Võib proovida modifitseerida kalkulaatorit nii, et see töötaks tavaliste (infix) avaldistega (vt. http://en.wikipedia.org/wiki/Shunting-yard_algorithm)