Kihagyás

9. gyakorlat

A gyakorlat anyaga

Belső osztály

Mennyire jó lenne, ha egy csordának nem kellene megmondanunk az elemszámát, hanem attól függően, hogy hány elemet rakok bele, változna a mérete. A jó hír, hogy ezt megtehetjük, emlékezzünk vissza a Programozás alapjai kurzuson tanultakra, volt "valami" láncolt lista megvalósítás. Igaz, ott C-ben dolgoztunk, de azért az ott tanultakat jó eséllyel itt is el tudjuk sütni, de ehhez szükségünk lesz egy új osztályra, például LancElem néven.

Azonban érdemes kicsit gondolkozni rajta, hiszen ezt az osztályt nem szeretnénk a nyilvánosság elé tárni, sőt igazából gyakorlati jelentősége nincs, hogy a Csorda milyen módon tárolja az csordában lévő állatokat. Milyen jó lenne, ha megoldható lenne az, hogy van egy osztályunk, és azt csak a Csorda osztály számára tesszük láthatóvá. Osztályok láthatósága public vagy package-private (kulcsszó nélkül) lehet, úgyhogy erre nincs lehetőségünk alapvetően. Ami a jó hír, hogy nem ezen a módon, de mégis megtehetjük ezt, azaz készíthetünk egy osztályt, amelyet csak egy másik osztály lát. Ehhez egy belső osztályt kell készítenünk, a fenti LancElem néven.

Lehetőségünk van osztályon belül, vagy akár metóduson belül deklarálni osztályokat. Ezek a "hagyományos "osztályokkal ellentétben lehetnek private, protected láthatóságúak is (a "hagyományos" osztályok láthatósága csak public vagy package-private lehet). Sőt, a belső osztályokat elláthatunk static módosítószóval is.

A belső osztályok hozzáférnek a külső osztály adattagjaihoz, metódusaihoz. Ez alól kivétel, ha a belső osztály statikus. Ez a kulcsszó jelen helyzetben gyakorlatilag annyit jelent, hogy a belső és külső osztálynak nincs köze egymáshoz.

A belső osztályok célja, hogy egy osztályon belül elrejtsünk egy máshol nem használt adatszerkezetet, algoritmust, ugyanakkor ezeket akár kívülről is elérhetjük (ha úgy állítjuk be a láthatóságukat). Az osztályon belül egyszerűen, a tanult módon példányosítjuk őket, azonban akár kívülről is megtehetjük ezt.

Példányosítás, ha a belső osztály nem statikus láthatóságú:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Kulso {
   private int num = 175;

   public class Belso {
      public int getNum() {
         System.out.println("Visszaterunk a szammal.");
         return num;
      }
   }
}

public class MainOsztaly {

   public static void main(String args[]) {
      Kulso kulsoPeldany = new Kulso();
      // Nem statikus belso osztaly
      Kulso.Belso belsoPeldany = kulsoPeldany.new Belso();
      System.out.println(belsoPeldany.getNum());
   }
}

Példányosítás, ha a belső osztály statikus láthatóságú:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Kulso {
   static class Belso {
      public void print() {
         System.out.println("Belso osztaly kiiratasa");
      }
   }
}

public class MainOsztaly {

   public static void main(String args[]) {
      Kulso.Belso belsoPeldany = new Kulso.Belso();
      belsoPeldany.print();
   }
}

Ezek ismeretében valósítsuk meg a belső LancElem osztályt, a Csorda osztályon belül. Mivel ezt máshol nem szeretnénk használni, nyugodtan tegyük privát láthatóságúvá. Ezt követően írjuk át a Csorda osztály, hogy az eddig használt statikus tömb helyett a saját láncolt lista megvalósításunkat használjuk!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Csorda {
    private class LancElem {
        public Allat tag;
        public LancElem kov = null;

        public LancElem(Allat tag, LancElem kov) {
            this.tag = tag;
            this.kov = kov;
        }
    }

    // Tömb helyett csak a lánc fejét tároljuk
    private LancElem fej;
    private int jelenlegi;

    private boolean szarazfoldiAllatok;

    public Csorda() {
        this.jelenlegi = 0;
        this.fej = null;
    }

    public boolean csordabaFogad(Allat kit) throws InkompatibilisAllatok {
        if (jelenlegi == 0) {
            if (kit instanceof SzarazfoldiAllat) {
                szarazfoldiAllatok = true;
            }
        }
        if ( (szarazfoldiAllatok && kit instanceof ViziAllat) ||
             (!szarazfoldiAllatok && kit instanceof SzarazfoldiAllat)) {
            throw new InkompatibilisAllatok();
        } else {
            LancElem tmp = new LancElem(kit, this.fej);
            this.fej = tmp;
            jelenlegi++;
            return true;
        }
    }

    public String toString() {
        String returnValue = "Allatok: ";
        LancElem jelenlegi = fej;
        while (jelenlegi != null) {
            returnValue += jelenlegi.tag.getNev();
            returnValue += ", ";
            jelenlegi = jelenlegi.kov;
        }
        return returnValue;
    }
}

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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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, amit előző órán mi is megvalósítottunk, kézzel. 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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.

1
2
3
4
5
6
7
    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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
        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:

1
2
3
        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.

1
2
3
4
5
        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öbség annyi az előző megoldáshoz képest, hogy ebben az esetben nem tudunk róla. :)

1
2
3
        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ő:

1
2
3
4
5
    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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
        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.

1
2
3
4
5
6
7
        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ó:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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ű:

1
2
        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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
        //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 comparator segítségével.

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é:

1
2
3
4
5
    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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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.

1
2
        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:

1
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
        //konzolon érkezett szavak számlálása mappel
        Map<String, Integer> map1 = 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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        //map bejárása - iterátorral
        Iterator elemek = map1.entrySet().iterator();
        while (elemek.hasNext()) {
          Entry elem = (Entry) 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    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;
    }

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!

Kapcsolódó linkek


Utolsó frissítés: 2021-05-04 07:53:32