Kihagyás

8. gyakorlat

A gyakorlat anyaga

Kollekciók

Az egyszerű tömböknek már a korábban látott módokon lehetnek hiányosságai. Részben ezek kiküszöbölésére alkalmasak az ún. kollekciók (collections), amelyek a tömbökhöz hasonlóan egy bizonyos típus tárolására szolgálnak, ám további funkcionalitásokkal is bírnak. Használatuk kényelmes, és nagyban egyszerűsítheti munkánkat. Kettő ilyet fogunk sorra venni a következőkben, ezek a lista és a halmaz, valamint találkozni fogunk az asszociatív tömb (leképezés, dictionary, map) fogalmával is. Bővebben a kollekciókról.

Listák (List)

A tömböknél például továbbra is nagy problémát jelenthet, hogy feltöltés előtt meg kell adni számukra a maximális méretet. Ez azt is jelenti, hogy elővigyázatosságból olykor feleslegesen nagy tömböket tárolhatunk, amelyek nagy része kitöltetlen marad. Ez az információ sokszor csak futás közben derül ki, a program írásakor még nem (például tetszőleges számú elem érkezik parancssori paraméterben), illetve ami még nagyobb gondot okozhat, az is megeshet, hogy a tömb létrehozásakor futás közben sem tudjuk még, hogy hány elemet szeretnénk benne tárolni (például a felhasználó tetszőleges számú elemet ad meg konzolon, ezeket el kell tárolni). Ezért az egyszerű tömb használatával komoly nehézségekbe ütközhetünk.

További problémát okozhat az is, hogy a tömbök már korábban látott length tulajdonsága a maximális számát tárolja, így ha elővigyázatlanul egy ciklust például ennyiszer ismétlünk, akkor könnyedén olyan elemre hivatkozhatunk, amely nem is létezik, és adott esetben akár NullPointerException típusú kivételt is kaphatunk. Így mondjuk egy változóban le kell tárolnunk, ténylegesen mennyi elem van a tömbben, és erre figyelni, de ez is hibalehetőségeket rejthet.

Ezt a problémát illusztrálja a következő példa, amely tetszőleges számú lebegőpontos számot olvas be, és kiírja a szorzatukat (legtöbb esetben helytelenül):

import java.util.Scanner;

public class HelytelenOsszeadasKonzolrol {
    public static void main(String[] args) {

        double[] szamok = new double[100];
        Scanner sc = new Scanner(System.in);

        //addig olvassunk be számot, amíg 1-t nem kapunk
        int i=-1;
        do {
            i++;
            szamok[i]=sc.nextDouble(); //hibás, ha a felhasználó több számot akar 100-nál (kifut)
        } while(szamok[i]!=1);

        //számoljuk ki a kapott számok szorzatát
        int szorzat=0;
        for(i=0;i<szamok.length;i++) {
            szorzat*=szamok[i]; //hibás, ha a felhasználó nem pont 100 számot adott (nulláz)
        }

        System.out.println(szorzat);
    }
}

Illetve más esetekben is komplikált lehet a tömbök kezelése, még ha jól is kezeli őket az ember. Ez látható például egyszerűsített megoldáson az állatos példa Csorda osztályának csordábaFogad metódusára:

    private int maximum = 100;
    private Allat[] tagok = new Allat[maximum];
    private int jelenlegi=0;

    public void csordabaFogad(Allat kit) {
        if (jelenlegi < maximum) {
            tagok[jelenlegi]=kit;
            jelenlegi++;
        }
    }

Ez a kód helyesen működik, ám igencsak komplikált, illetve a maximumot túllépő csordát nem tud kezelni.

Ezekre a problémákra megoldást nyújthat az ún. lista (List), amely a tömbhöz nagyon hasonló működésű, ám sokkal rugalmasabban kezelhető. Ez a tömbhöz hasonlóan továbbra is egy típusból tud tárolni elemeket, ám ez tetszőleges méretet felvehet, kevés tárolt adatnál kisméretű, sok adatnál nagy. Ez nem csak a memória-spórolás szempontjából fontos, ugyanis pontosan annyi eleme lesz, amennyit mi hozzáadunk. Tulajdonképpen megegyezik a Programozás alapjain már látott dinamikus tömb működésével, ám itt nincs szükség mutatóval való foglalásra és felszabadításra, használata igen egyszerű. Deklarációja a következőképpen nézhet ki:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Listak {
    public static void main(String[] args) {
        List<Integer> lista = new ArrayList<>(); //tömbös megvalósítás
        List<Integer> lista2 = new LinkedList<>(); //láncolt listás megvalósítás
    }
}

A java.util csomag List osztálya egy interface, amely a már átvett ismereteink alapján azt jelenti, hogy önmagában nem végzi el a műveleteit, ez az őt megvalósító osztályok dolga. Ennek megvalósításai viszont már használhatóak, ezek közül választhatnunk. Ezek lehetnek például az ArrayList és a LinkedList, de további megvalósítások is rendelkezésre állnak, ezekről bővebben itt olvashatsz.

A két osztály pontosan ugyanazokat a feladatokat látja el, csak a mögöttes működésükben térnek el egymástól, de minden műveletük és ezek helyessége megegyezik. Az ArrayList egy tömbös megvalósításon alapul, a LinkedList pedig láncolt listákon. A két listatípus tehát használati szempontból teljesen ugyanaz, hiszen mindkettő ugyanazt az interfészt implementálja.

Generikus típusmegadás

A látott deklaráció elsőre kicsit furcsának tűnhet. A tárolt adatok típusának megadása itt <> (kacsacsőrök) között történik. Egy List<Double> típus tehát egy lista, amely lebegőpontos elemeket tárol. Amint látható, itt nem az egyszerű primitív típusokat, hanem azok csomagoló (wrapper) osztályait kell megadni. Az egyenlőségjel után pedig már egy konkrét megvalósítás konstruktorával kell példányosítanunk, Java 7.0 vagy afeletti verzióban már nem fontos a típus megadása újra, elegendő az üres kacsacsőröket kitenni (diamond operátor), ezzel is megkönnyítve a dolgunkat. Maximális méret megadására nincs szükség, az újonnan létrehozott lista mindig üres, és 0 elemű.

Az imént látott szintaxis a generikus típusmegadást jelöli. Erről bővebben hallhatsz az előadáson. Gyakorlatilag statikus polimorfizmusról van szó, egy típusparamétert adunk meg, mivel az osztály maga úgy lett megírva, hogy a lehető legáltalánosabb legyen, és ne kelljen külön IntegerList, StringList, AllatList, stb. osztályokat megírnunk, hanem egy általános osztályt, mint sablont használunk, és a tényleges típust a kacsacsőrök között mondjuk meg.

Generikus osztályok

Ez természetesen nem csak a listák, leképezések (mapek) esetében használható, mi is csinálhatunk ilyen osztályokat minden további nélkül. A következőekben egy nagyon egyszerű osztályt mutatunk be:

public class ElrejtettErtek<GenerikusTipus> {
    private GenerikusTipus ertek;

    public ElrejtettErtek(GenerikusTipus ertek) {
        this.ertek = ertek;
    }

    public GenerikusTipus getErtek() {
        return ertek;
    }

    public void setErtek(GenerikusTipus ertek) {
        this.ertek = ertek;
    }

    @Override
    public String toString() {
        return "ElrejtettErtek [ertek=" + ertek + "]";
    }
}

Ez egy olyan osztály, ami egy valamilyen típusú értéket tud tárolni, erre van egy getter és egy szetter függvény, valamint egy toString metódus. Tehát, ha én példányosításkor azt mondom, hogy ElrejtettErtek<String> ertek = new ElrejtettErtek<>("Szeretem az almát!");, akkor a létrejövő objektumban egy szöveget tudok eltárolni, és így tovább.

    public static void main(String[] args) {
        ElrejtettErtek<String> ertek = new ElrejtettErtek<>("Szeretem az almát!"); // Egy szöveget tudok így tárolni
        ertek.setErtek("Sikerül vajon?"); // Igen
        //ertek.setErtek(103); //Nem fog sikerülni
        ElrejtettErtek<Integer> szamErtek = new ElrejtettErtek<>(120); // Egy egész számot tudok így tárolni
        ElrejtettErtek<Allat> allatErtek = new ElrejtettErtek<>(new Medve("Jason")); // Egy állatot tudok így eltárolni
    }

Vissza a listákhoz

Használatuk igencsak egyszerű, új elem hozzáadása az add metódusával történik, ami olyan elemet vár, mint amilyen maga a lista. Ilyenkor a lista dinamikusan bővül, tehát amennyiben alapállapotában adjuk ki az utasítást, létrejön benne a 0. indexű elem, illetve már létező elemeknél a következő szabad indexen érhető el az új elem. Új elem tetszőleges indexre is beszúrható, ilyenkor az addig azon az indexen lévő, és az összes nála nagyobb indexű elem egy indexszel feljebb lép, ezt az add(index, elem); metódussal vihetjük véghez, hasonló a sima add-hoz, csak az elem elé a kívánt indexet is meg kell adnunk. A lista rendelkezik egy size (vigyázat, ez nem length! és nem tulajdonság!) metódussal, amely az elemeinek számát tárolja, ez automatikusan változik a lista növekedésével/csökkenésével.

Egy bizonyos elemre itt nem [] jelek között kell hivatkoznunk, hanem a get viselkedést meghívva, például lista.get(0) a 0. indexű elem lekérése. A külsőségektől eltekintve ez ugyanúgy indexelhető, mint a tömb.

Az elemek törlése is igencsak intuitív. Ezt a remove metódussal tudjuk meghívni. Ennek két típusa is létezik. Egyrészt megadhatjuk az indexet, másrészt konkrét elemet is adhatunk, amelynek első példányát töröljük.

Ha van egy Integer elemeket tároló listánk, akkor figyelni kell, mert ha nem index, hanem elem szerint szeretnénk törölni, akkor az alapesetben nem fog működni, hiszen az int és csomagoló típusa nagyon összekeverhető lehet. Ilyen esetben, ha egy adott értékű elemet szeretnénk törölni, akkor mindig castolni kell, például a list.remove(3) helyett írjunk list.remove((Integer)3), vagy list.remove(Integer.valueOf(3)), esetleg list.remove(new Integer(3)) parancsot.

Ezen parancsokat a következő példakód illusztrálja:

        List<Double> lista = new ArrayList<>(); //Polimorfizmus miatt kezelhetjük List-ként.

        //beszúrás a lista végére
        lista.add(1.2);
        lista.add(2.1);
        lista.add(3.10);
        lista.add(2.1);
        lista.add(3.05);

        //beszúrás a legelső helyre -> 3.55, 1.2, 2.1, 3.1, 2.1, 3.05
        lista.add(0, 3.55);

        //a legelső elem törlése -> 1.2, 2.1, 3.1, 2.1, 3.05
        lista.remove(0);

        //a legelső 2.1 érték törlése -> 1.2, 3.1, 2.1, 3.05
        lista.remove(2.1);

Egy listához hozzáadhatunk egy másik listát is, a listák addAll metódusával, amely paraméterül a másik kollekciót várja.

Bejárás

A listák bejárására több módszer is lehetséges.

Egy már megszokott módszer lehet az index alapján történő bejárás, ahogy azt tömböknél is szokás:

        for (int i=0; i < lista.size(); i++) {
            System.out.print(lista.get(i) + " ");
        }

Egy másik lehetőség iterátor használata. Az iterátor egy olyan objektum (nem mellesleg az Iterátor tervezési mintát valósítja meg, amelyről bővebben az előadáson hallhatunk), amely képes egyenként bejárni a kollekciók összes elemét. Deklarációjakor szintén <> jelek között adhatjuk meg a típust, illetve ezután new kulcsszó helyett a lista iterator() metódusát hívjuk meg, amely elkészíti a megfelelő iterátort. Az iterátorral használat közben lépkedni kell, amíg az utolsó elemet el nem érjük. Ezt általában célszerű ciklussal tenni (legtöbbször while ciklussal). Azt lekérni, hogy az utolsó elemnél tartunk-e az iterátor hasNext() metódusával tudjuk, amely boolean értéket ad vissza. Ez nem állítja automatikusan a következő elemre az iterátort, azt a next() metódus teszi, amely a léptetésen túl visszatér a következő elemmel. Ennek hívásakor ügyelni kell arra, hogy ez a ciklustól függetlenül is minden híváskor lépteti az iterátort, így ha nem tároljuk ideiglenes változóban az értéket (pl.: double ideigl = it.next();), akkor beleeshetünk abba a csapdába, hogy kétszer léptetjük két ellenőrzés között, lekérve a következő következőjét is, amelynek létezésére már nincsen garancia, sőt az utolsó elem léptetése után garantáltan hibás lesz.

        Iterator<Double> it = lista.iterator();
        while(it.hasNext()) {
            Double elem = it.next();
            System.out.print(elem + " ");
        }

Egy harmadik lehetőség, ha a for ciklus elemenkénti bejárását alkalmazzuk. Ez nagyon könnyű és értelemszerű használatot biztosít. A megszokott for struktúrája helyett itt nem lesznek pontosvesszők, sem megállási feltétel. Egy elemet deklarálunk, amely a lista elemeinek típusával rendelkezik, utána kettősponttal elválasztva a lista nevét. A ciklus minden futásakor a következő elem fog a deklarált elembe kerülni. Ez a for ciklus a háttérben szintén iterátorral dolgozik, a különbség annyi az előző megoldáshoz képest, hogy ebben az esetben nem tudunk róla. :)

        for (double elem : lista) {
            System.out.print(elem + " ");
        }

A fent látott állatos példa csordabaFogad metódusa listákkal a következőre egyszerűsíthető:

    private List<Allat> tagok = new ArrayList<>();

    public void csordabaFogad(Allat kit) {
        tagok.add(kit);
    }

Törlés

Ahogy korábban is szó volt róla, a listából a remove nevű metódussal törölhetünk egy elemet. Ez egyszerűnek hangzik, de egy esetben nem fog működni: ha a listából mondjuk bejárás közben szeretnénk törölni. Nyugodtan próbáljuk meg törölni az összes elemet, amik mondjuk 1.5-nél kissebbek:

        List<Double> lista = new ArrayList<>();

        lista.add(1.2);
        lista.add(2.1);
        lista.add(3.10);
        lista.add(2.1);
        lista.add(3.05);

        for(double elem : lista) {
            if(elem < 1.5){
                lista.remove(elem);
            }
        }

Ilyen esetekre jön jól a már ismertetett iterátor. Az iterátorral történő bejárás során egyszerűen törölhetünk bármilyen nekünk nem tetsző elemet, az iterátor remove metódusát használva.

        Iterator<Double> it = lista.iterator();
        while(it.hasNext()) {
            double elem = it.next();
            if(elem < 1.5){
                it.remove();
            }
        }

Amennyiben a lista összes elemét törölni szeretnénk, a listák clear() metódusa alkalmazható.

Halmazok (Set)

Az ún. halmaz (Set) a listához igencsak hasonló mind funkciójában, mind működtetésében. Ez is interface, és ennek is két fajtáját érdemes ismernünk, a HashSet-et, amely hasítótáblás, illetve a TreeSet-et, amely piros-fekete fás megvalósítást jelöl. Ezen implementációk használata is teljesen megegyező egymással.

A listákhoz hasonlóan ugyanúgy egy típusból tárolhatnak tetszőleges számú elemet, és ugyanúgy dinamikusan bővülnek. Ugyanúgy használható az add és remove metódus is. Két alapvető eltérés van a listáktól:

  • A halmazok minden elemet csak egyszer tartalmazhatnak. Tehát, mint amikor matematikai halmazokról beszélünk, azt nem tartjuk számon, hogy hány darab van egy elemből benne, csak hogy egy bizonyos elemet (például számot) tartalmaz-e. Ezzel kapcsolatban felmerülhet azonban a kérdés, hogy mi van akkor, ha hozzáadunk egy számokat tároló halmazhoz egy 2-es elemet, aztán egy 3-ast, végül egy új 2-est. Ilyenkor érthető, hogy a 2-es is csak egyszer lesz benne, de mi történik az indexekkel? A választ a következő pontban találjuk:

  • A halmazok elemei index szerint nem rendezettek. Tehát nem lehet őket index szerint lekérni, tehát az nem is tárolódik, hogy milyen sorrendben helyeztük bele az elemeket, csak az, hogy benne vannak-e.

Deklarációjuk a listákéhoz nagyon hasonló:

import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class Halmazok {
    public static void main(String[] args) {
        Set<Integer> halmaz = new HashSet<>();  //hasítótáblás megvalósítás
        Set<Integer> halmaz2 = new TreeSet<>(); //piros-fekete fás megvalósítás
    }
}

Contains

A halmazok egyik legfontosabb tulajdonsága lehet, hogy tartalmaznak-e egy bizonyos elemet. Ezt könnyedén lekérdezhetjük a contains metódusának meghívásával. Ennek használata igen egyszerű:

        if(halmaz.contains(2)) System.out.println("A halmazban van 2");
        else System.out.println("A halmazban nincs 2");

Természetesen ez nem csak számokkal tud működni, ha például az Állat osztályból származó példányokat teszünk a halmazba, akkor is használható.

Bejárás

Az elemek bejárására nagyjából ugyanúgy vannak lehetőségeink, ahogy a listáknál. Természetesen mivel itt az index nem értelmezett a halmazra, index alapú bejárásra nincs lehetőségünk. Az iterátoros, illetve az elemenkénti bejárást viszont komolyabb változtatások nélkül elérhetjük:

        //bejárás - iterátorral
        Iterator<Integer> it = halmaz.iterator();
        while(it.hasNext()) {
            System.out.print(it.next() + " ");
        }
        System.out.println();

        //bejárás - elemenként
        for(int szam : halmaz) {
            System.out.print(szam + " ");
        }
        System.out.println();

Felmerülhet ilyenkor a kérdés, hogy milyen sorrendben fogjuk visszakapni a beírt elemeinket. Ez sok problémánál nem fontos, mert a sorrendtől teljesen független tevékenységet végez. A HashSet semmilyen rendezést nem garantál, a TreeSet viszont igen, ezért az ő elemeinek meg is kell valósítaniuk valamilyen rendezést (a primitív típusok csomagoló osztályai és a String ezt megteszik). Ha saját objektumokat tárolunk, definiálnunk kell kisebb-nagyobb-egyenlő műveleteket a Comparable interfész implementálásával, illetve ha nincs lehetőségünk az adott osztályt módosítani, vagy csak alkalmi rendezést szeretnénk megvalósítani, akkor használhatjuk a Comparator interfészt, amely minden olyan esetben használható majd a tárolóknál, amelyek egyedi rendezést igényelnek.

A halmazok használata tehát egyszerű, és sok olyan eset előfordul, ahol könnyebben felhasználható a listáknál is. A korábban látott csorda például gond nélkül megvalósítható halmazokkal is, mivel minden állat legfeljebb egyszeresen lehet egy csordában. Az erre vonatkozó kód semmivel sem bonyolultabb, mint a listákkal való megvalósításé:

    private Set<Allat> tagok = new HashSet<>();

    public void csordabaFogad(Allat kit) {
        tagok.add(kit);
    }

Leképezések (Map)

Az eddig látott tömbök és listák elemeire mind 0-tól kezdődő, növekvő indexekkel tudtunk hivatkozni. Viszont számos esetben hasznos lehetne, ha nem csak egész számokhoz rendelhetnénk elemeket, hanem más dolgokhoz is, például szavakhoz vagy objektumokhoz. Erre használhatóak az ún. leképezések, azaz Map-ek. Ebből szintén két implementációt érdemes ismernünk, a Hash Map-et, amely hasítótáblán és a Tree Map-et, amely piros-fekete fán alapul.

Minden map kulcs-érték (key-value) párokból áll. Ebből mindkettő lehet bármely tetszőleges referencia típusú. A kulcsokhoz értékeket rendelünk, amely azt jelenti, hogy egy bizonyos kulcshoz mindig egy érték tartozik. Egy érték viszont több kulcsnál is előfordulhat. Ebből adódóan a kulcs a párt egyértelműen beazonosítja, míg az érték nem. Ezt felfoghatjuk úgy is, hogy számok helyett tetszőleges típusú elemeket is megadhatunk indexként.

Deklarációjuk a következőképpen nézhet ki:

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class Mapek {
    public static void main(String[] args) {
        Map<Integer,String> map1 = new HashMap<>();
        Map<Integer,String> map2 = new TreeMap<>();
    }
}

Észrevehetjük, hogy itt a <> jelek között már nem csak egy, hanem vesszővel elválasztva két típust kell megadnunk. Az első a kulcs, míg a második a hozzá rendelt érték típusa. A fenti példán tehát a kulcs egész szám, míg értéke szöveges.

Új elempár hozzáadása itt a 'put' metódussal történik.

        map1.put(320,"Kék");
        map1.put(200,"Zöld");

A példán látható kód a map1 map-be helyez két kulcs-érték párt, a 320-as számhoz a "Kék" szót, míg 200-hoz a "Zöld" szót rendeli. Ezután már lekérhető a kulcshoz tartozó elem a listákhoz és halmazokhoz hasonlóan get metódussal:

String elem = map1.get(320);

Ilyenkor a "Kék" szöveget kapjuk. Fontos, hogy ez nem fordítható meg, itt nem mondhatnánk, hogy map1.get("Kék"). Ez azért van, mert akár a 200-hoz is rendelhettünk volna ugyanúgy "Kék"-et. Ha megpróbálnánk újabb 320-as kulcsú elemet tenni a map-be, akkor viszont felülírnánk az előzőt, így ez mindig egyértelmű.

Elemek törlése a listáknál már látott remove metódussal, a látottakkal megegyező módon alkalmazható, viszont itt is csak kulcs megadása lehetséges, az érték itt sem azonosít megfelelően. Törléskor természetesen mind a kulcs, mind az érték törlésre kerül. Kulcs-érték pár megadása is lehetséges viszont, ha csak bizonyos érték esetén szeretnénk törölni a párt.

Megfigyelhetjük, hogy a fenti működés hasonló egy indexeléshez. Annyiban különbözik tőle, hogy nem feltétlenül 0-tól indul, illetve nem csak sorban tartalmazhat indexeket. Ennél a map-ek azonban sokkal többre is képesek.

Az alábbiakban láthatunk egy példát, amely a konzolon kapott szavakat számolja meg, melyik szóból hány darab érkezett, ezeket egy map-ben tárolja.

        //konzolon érkezett szavak számlálása mappel
        Map<String, Integer> map1 = new HashMap<>();
        for(int i=0; i<args.length; i++) {
            if(map1.containsKey(args[i])) { //ha már láttuk a szót
                int darabszam = map1.get(args[i]);
                darabszam++;
                map1.put(args[i], darabszam); //felülírjuk az eddigi számát
            }
            else { //ha még nem láttuk a szót
                map1.put(args[i], 1);
            }
        }

Ilyen map-eket használhatunk a gyakorlatban például szövegfeldolgozás közben, ahol a szavak előfordulási számából tudunk következtetést levonni, sok algoritmusnak ez az alapja.

Bejárás

A map-eknél a halmazokhoz hasonlóan nincs egyértelmű rendszer a kiírás sorrendjére. Indexenként ezek bejárására sincs lehetőség (esetleg ha a listákhoz hasonló map-et készítünk, vagy fenntartunk egy index-halmazt, amelyet bejárva a kulcsokat kapjuk sorban). Itt is használható a kijárásra iterátor és az elemenkénti kiírás is működik (kicsivel bonyolultabb fromákban):

        //map bejárása - iterátorral
        Iterator elemek = map1.entrySet().iterator();
        while (elemek.hasNext()) {
          Entry<String, Integer> elem = (Entry<String, Integer>) elemek.next();
          System.out.println(elem.getKey() + "\t" + elem.getValue());
        }

        //map bejárása - elemenként
        for(Entry<String, Integer> elem : map1.entrySet()) {
            System.out.println(elem.getKey() + "\t" + elem.getValue());
        }

Ilyenkor nem kulcsokkal vagy elemekkel tudjuk bejárni a map-et, hanem a konkrét párokkal. Egy ilyen párt hívunk Entrynek. Ennek szintén két típust kell megadnunk, ez pontosan egy kulcs-érték párját jelöli a map-nek. A map tehát felfogható úgy, mint ilyen entry-k halmaza. Ebbe pedig a map entry-jeit helyezhetjük úgy, hogy egyszerűen ténylegesen entry-k halmazaként kezeljük az entrySet metódusával, amely egy halmazt ad vissza a map tartalmával.

Egy entry tehát egy kulcs-érték pár, amelynek kulcsát a getKey(), míg értékét a getValue() metódussal kaphatjuk meg.

A map-ekre is létezik a halmazoknál látott contains metódus, ám itt kettő is van belőle, a containsKey és a containsValue, amelyekkel a kulcsokat és az értékeket ellenőrizhetjük. Ezek értelemszerűen a nekik megfelelő típust várják paraméterül.

A map-ek tehát szintén egyszerű működtetést biztosítanak, illetve szintén dinamikus méretet támogatnak. Alkalmazhatóak például objektumok számolására, két objektum egymáshoz rendelésére, vagy akár bármilyen érték ideiglenes objektumonkénti tárolására.

A már látott állatos példára visszatérve, ha létezik egy halmazban tárolt csorda a látott formában, akkor ahhoz megadható map, amely például számon tartja, hogy melyik fajból hány darabot tartalmaz. Egy ezt visszaadó metódus:

    public Map<String, Integer> fajokatSzamol() {
        Map<String,Integer> fajSzamok = new HashMap<>();

        for(Allat allat : tagok) {  //bejárjuk a csordát
            if(!fajSzamok.containsKey(allat.getClass().getName())) { //ha még nincs ilyen faj a map-ben
                fajSzamok.put(allat.getClass().getName(), 1); //beleteszünk egyet
            }
            else {  //ha már láttunk ilyen fajt
                int eddigiSzam = fajSzamok.get(allat.getClass().getName()); //lekérjük az eddigi hozzárendelt számot
                eddigiSzam++; //növeljük 1-el
                fajSzamok.put(allat.getClass().getName(), eddigiSzam); //újra beletesszük az új számmal, felülírva a régit
            }
        }

        return fajSzamok;
    }

Comparable és Comparator interfészek

Ahogy azt már láttuk korábban is a tömböknél, lehetőség van arra, hogy egy-egy típus elemei között rendezési relációkat adjunk meg. A megadott rendezési relációkat fel tudjuk használni az olyan halmazok, esetlegesen map-ek kulcs halmazainál is, amelyek nem hash függvények segítségével "rendezik" az elemeiket. Ilyenek például a TreeSet, illetve TreeMap, de ilyen lehet minden olyan kollekció, vagy leképezés, amely megvalósítja a SortedSet , illetve SortedMap interfészeket. Nagyon fontos, hogy ezek a halmazok a null értéket sosem tartalmazhatják.

A Comparable interfész használata akkor lehet tanácsos, ha egy osztály objektumai között általánosan szeretnénk egy rendezést megvalósítani. A primitív típusok wrapper osztályai, vagy a String osztály is megvalósítja ezen interfészt, így ha ezen típusoknál egy általános rendezést várunk el (pl. Integer esetén növekvő módon rendezzen, String esetében ábécé szerint), akkor ez adott már. Ha saját osztály objektumai között szeretnénk ezt elérni, implemenáljuk mi is a Comparable interfészt.

Például rendezzük az Allat osztály objektumait a nevük szerint:

    public class Allat implements Comparable<Allat>{
      private String nev;

      public Allat(String nev) {
          if (nev != null)
            this.nev = nev;
          else 
            this.nev="";
      }

      public String getNev() {
          return nev;
      }

      @Override
      public int compareTo(Allat o) {
          return this.nev.compareTo(o.nev);
      }
    }

Ekkor ha olyan TreeSetet hozunk létre, ahol mi magunk nem adjuk meg, milyen Comparatorral szeretnénk azt majd bejárni, akkor a most definiált bejárás lesz az érvényes. FONTOS! A compareTo metódust úgy kell megvalósítani, hogy az az általa használt két objektum szempontjából felcserélhető legyen. Valamint tranzitív tulajdonsággal is rendelkeznie kell. Visszatérési értéke a compareTo-nak 0, ha a két objektumot egyenlőnek tekintjük, 0-nál kisebb, ha a this objektumot tekintjük kisebbnek, 0-nál nagyobb, ha a this objektumot tekintjük nagyobbnak.

Példa felhasználás:

public static void main(String args[]) {
    Set<Allat> allatok = new TreeSet<>(){{add(new Allat("Leo")); add(new Allat("Csipi")); add(new Allat("Vili"));}};
    for(Allat a: allatok) {
        System.out.println(a.getNev());
    }
}

Ekkor az állatok a nevük szerinti sorrendben lesznek bejárva.

Amennyiben nem áll módunkban a Comparable interfészt implementálni, vagy csak olyan rendezést kívánunk megvalósítani, amit nem szeretnénk túl sok esetben megvalósítani, akkor a TreeSetet (Mapre hasonlóan működik) úgy inicializáljuk, hogy az egy Comparator típusú objektumot kap a konstruktorában. Ennél a compare metódust kell megvalósítani, amelyre hasonlóak igazak, mint a compareTo-ra: olyan rendezést kell megvalósítania, amely az adott objektumokra teljes rendezést valósít meg és az elemekre definiált rendezés felcserélhető, tranzitív.

Például az Allat osztályra megadhatunk egy olyan Comparatort, ami most a neveik hossza alapján állítja sorrendbe az állatokat.

class AllatComparator implements Comparator<Allat>{
    @Override
    public int compare(Allat o1, Allat o2) {
        return o1.getNev().length()-o2.getNev().length();
    }
}

A halmazunkat úgy hozhatjuk létre egy ilyen comparator objektummal, ha a létrehozásakor a megfelelő konstruktort hívjuk:

public static void main(String args[]) {
        Set<Allat> allatok = new TreeSet<>(new AllatComparator()){
            {
                add(new Allat("Leo"));
                add(new Allat("Csipi"));
                add(new Allat("Kázmér"));
                add(new Allat("Vili"));}
            };
        for(Allat a: allatok) {
            System.out.println(a.getNev());
        }
    }

Ha csak egy comparator objektumot fogunk ezzel a rendezéssel meghívni, azt is megtehetjük, hogy nem is hozunk létre külön Comparator`t, hanem Anonymous osztályként definiáljuk azt, de ennek részletei már a következő gyakorlat témája lesz:

public static void main(String args[]) {
    Set<Allat> allatok = new TreeSet<>(
        new Comparator<Allat>() {
            @Override
            public int compare(Allat o1, Allat o2) {
                return o1.getNev().length()-o2.getNev().length();
            }
        }
    ){{add(new Allat("Leo")); add(new Allat("Csipi")); add(new Allat("Kázmér"));add(new Allat("Vili"));}};
    for(Allat a: allatok) {
        System.out.println(a.getNev());
    }
}

Videók

Feladatok

  • A korábbi saját láncolt lista implementációt módosítsuk úgy, hogy bármilyen típusú elemet tudjon tárolni, ne csak Allat típusút.
  • Hozz létre egy fix méretű vermet egész számok tárolására (tömb segítségével) és valósítsd meg a push/pop műveleteket.
  • Írj egy futtatható osztályt, mely a Main metódusban "push" vagy "pop" utasításokat vár a konzolról. Ha pop utasítást kap, hajtsa végre azt, és írja ki a konzolra a kivett elemet. Push utasítás esetén egy egész számnak kell következnie, ezt tegye be a verembe.
  • A korábbi vermes feladatot valósítsd meg úgy, hogy bármilyen típusú elemet tudjon tárolni, amit generikus típusparaméterként kelljen neki megadni.
  • Írd át a fix méretű vermet saját láncolt listára
  • Cseréld ki a saját lista implementációd valamelyik kollekcióra!
  • Hozz létre map-et, amelyben a kulcsokat sztringek adják! A megvalósításod során biztosítsd, hogy a map bejárásakor a kulcsok hossza szerint CSÖKKENŐ sorrendben történjen!

Kapcsolódó linkek


Utolsó frissítés: 2024-04-11 07:54:27