Java programozás 24. – asszociatív tömb avagy rugalmas párok

Több olyan feladat van, ahol nagyon hiányozna egy olyan adatszerkezet, ahol elemeket lehet párba állítani. Jó példa erre, amikor a hét napjaihoz azt a sorszámot kellene hozzárendelni, hogy a nap a hét hányadik napja. Ugyanígy igaz ez a hónapokra is, amikor a hónap neveihez kellene sorszámokat rendelni. Ha picit távolabbi célt tűzünk ki, milyen jó lenne egy hónapról egyszerűen megtudni, hogy az melyik évszakban van. Vagy épp a fordítottja, melyik évszakban milyen hónapok vannak.

Természetesen ezen feladatok megoldhatóak két tömbbel is, ahol az azonos indexű helyen lévő elemek jelentik a párokat. Lássunk erre egy példát:

String[] napok = { "hetfo","kedd","szerda","csutortok"
                   "pentek","szombat","vasarnap" };
int[] sorszamok = { 1,2,3,4,5,6,7 };

Ebben az esetben ha a szerda sorszámát szeretnénk megkapni, akkor a szerda indexének megfelelő számot kell a ‘sorszamok’ tömbből megkapni.

Map

Oké, az előző egy kicsit erőltetett példa, de egynek éppen jó. A gond az, hogy ha rendezzük az egyik tömböt, akkor a másik tömbben is cserélnünk kell az elemeket, és ugyanazokat a cseréket kell végrehajtani. A párok elveszítik egymást, ha csak az egyik tagját babráljuk.

Az asszociatív tömbök pont az ilyen jellegű feladatokat tudják nagyon megkönnyíteni. A tömbtől abban is különböznek, hogy ennek az adatszerkezetnek nincs kötött mérete. Bármikor le lehet kérdezni, hogy hány elem van benne, de mérethatár nincs. Vázlatosan bemutatom a felépítését, de fontos tudni, hogy a ([ ]) jelzések nem valódiak, egy másik programozási nyelvből vettem őket, ezzel jelölöm majd a tömb határait, aminek a neve ettől kezdve Map.

A Map filozófiája az, hogy egyedi kulcsokat rögzít, és a kulcsokhoz értékeket párosít. A kettő együtt jelent egy elemet (entry), ezek darabszáma adja a Map méretét. A példában a ([ ]) jelek között található a Map, ami kulcsokat, és hozzájuk tartozó értékeket tartalmazza.

([ kulcs1 : ertek,
   kulcs2 : ertek,
   kulcs3 : ertek ])

A kulcsoknak mindenképpen egyedieknek kell lenniük, az értékekkel kapcsolatban nincs megkötés. Ha már egy meglévő kulccsal adnánk hozzá új értéket, akkor a meglévő kulcshoz tartozó értéket cseréli ki az új értékre. Ezt később ki is fogjuk használni, akár más feladattípusban is!

Konkrét értékeket egyszerűen a kulcsok alapján tudjuk kinyerni a map.get(kulcs) metódussal, visszafelé ez nem lehetséges, vagyis már a tároláskor el kell döntenem, hogy melyik adat alapján melyiket akarom könnyen lekérdezni. Maradva a hónap – sorszám példánál, a következők képzelhetők el:

  • a hónapból akarom egyszerűbben megtudni a sorszámát
  • a sorszámból akarom megtudni, hogy az melyik hónap

A Map hasonlóan az ArrayList-hez, csak objektumokat tartalmazhat, de belerakhatók primitív értékek is (int, double, boolean, stb), a Map ezeket automatikusan burkoló (csomagoló) osztályokba helyezi. Deklarálásakor meg kell adni, hogy milyen osztályú elemeket tartalmaz majd, de itt külön kell választani a kulcsot, és értékeket. A deklaráláskor az inicializáláskor a típusokat már nem kell megismételni, itt is használható az ArrayList lecke vége felé ismertetett diamond operátor.

Olyan osztályt, hogy Map soha nem példányosíthatsz. Ez egy gyűjtőneve azoknak az osztályoknak, melyek kulcs-érték párokat tartalmaznak, de ezek a specializált osztályok a konkrét megvalósításban eltérnek egymástól.

Lássuk mik azok a fontosabb osztályok, melyek ilyen feladatra használhatóak:

Map osztályok

Az osztályhierarchia szerint a Map alá a következő osztályok tartoznak:

  • HashMap – nem garantált a behelyezett elemek sorrendje, egyetlen null kulcs és tetszőleges null érték létezhet
  • LinkedHashMap – az elemek behelyezési sorrendjét megtartja
  • TreeMap – kulcs szerint rendezett (ékezetes karaktereket a sor végére teszi)
  • HashTable – többszálú végrehajtásra felkészített, nincs null kulcs és érték

A továbbiakban csak az első három osztállyal foglalkozok.

HashMap

Lássuk a következő feladatot: Állítsunk párba kutyákat és gazdákat, ahol ezek neveit tároljuk.

import java.util.HashMap;
...
...
HashMap<String,String> hmap = new HashMap<>();
hmap.put("Janos","Bordi");
hmap.put("Bela","Cezar");
hmap.put("Aniko","Hektor");
hmap.put("Agnes","Cezar");
hmap.put("Jozsef","Csibesz");

for( String s : hmap.keySet() )
{
  System.out.println( s+": "+hmap.get(s) );
}

A program kimenete:

Agnes: Cezar
Jozsef: Csibesz
Bela: Cezar
Aniko: Hektor
Janos: Bordi

Láthatod, hogy a kimenet sorrendje valóban nem ugyanaz, mint amilyen sorrendben bepakoltuk az adatokat. Bármikor megtudhatjuk, hogy kinek milyen nevű kutyája van, a sorrend azonban teljesen lényegtelen a feladat szempontjából, így nem is foglalkozunk vele. Fontos azonban, hogy nem lehet két azonos kulcs, vagyis azonos nevű gazdák, de azonos nevű kutyája lehet két különböző gazdának.

hmap.put("Bela","Burkus");

Ha egy ilyen elemeit szeretnénk berakni, akkor a meglévő ember kutyáját módosítanánk Burkus-ra, nem új elemet adnánk hozzá.

Konkrét gazdához tartozó kutyát bármikor megkaphatunk a hmap.get(gazda) metódussal, de vannak olyan esetek, amikor a Map minden elemére szükségünk van, vagyis szeretnénk az adatszerkezetet bejárni. Ezekben az esetekben foreach ciklust használhatunk. Nincs indexelés, nincsen elem sorrend, kulcsok halmaza van melyet bármikor megkaphatunk a map.keySet() metódussal. Ez a halmaz foreach ciklus által bejárható, amely egyesével végiglépked a halmaz elemein, vagyis a kulcsokon. A kulcsokhoz tartozó értékeket a ciklus közben bármikor megkaphatod a map.get(kulcs) metódussal. Van olyan kiíratás is, amikor magukat a Map elemeket, vagyis a kulcs-érték párokat közvetlenül is elérhetjük, erről egy picit később.

Álljon itt akkor egy másik példa is, amikor a hét napjaihoz rendelünk sorszámokat:

import java.util.HashMap;
...
...
HashMap<String,Integer> hmap = new HashMap<>();
hmap.put("hetfo",1);
hmap.put("kedd",2);
hmap.put("szerda",3);
hmap.put("csutortok",4);
hmap.put("pentek",5);
hmap.put("szombat",6);
hmap.put("vasarnap",7);

for( String s : hmap.keySet() )
{
  System.out.println( s+": "+hmap.get(s) );
}

LinkedHashMap

Az előző példában szereplő HashMap tehát olyan esetekben használható, amikor egyedi adatokhoz más adatokat társítunk, de ezeket kinyerni már nem az eltárolás sorrendjében szeretnénk.

Vannak azonban olyan esetek, amelyek kicsit kilógnak a sorból: Évszakok – hónapok. Tegyük fel egy olyan adatszerkezetet szeretnénk létrehozni, melyben az adott évszakhoz kapcsolnánk a hozzá tartozó hónapokat úgy, hogy az évszakok sorrendje a kiíratáskor tartsa meg a tárolás sorrendjét. Az egyik különbség, hogy egy kulcshoz több értéket kell kapcsolni, hiszen egy évszakhoz több hónap is kapcsolódik. Az is gond, hogy a HashMap itt nem működik, hiszen az a kulcsokat összekeveri, nem garantálja a sorrendet. Akár egyetlen új elem tárolása is teljesen összekavarhatja a meglévőket, nem csak egyszerű beszúrásként működik.

Ilyenkor ilyen tárolási alakok képzelhetők el:

([ januar     : tel,
   februar    : tel,
   marcius    : tavasz,
   aprilis    : tavasz,
   majus      : tavasz,
   junius     : nyar,
   julius     : nyar,
   augusztus  : nyar,
   szeptember : osz,
   oktober    : osz,
   november   : osz,
   december   : tel ])
([ tel :    { december, januar, februar },
   tavasz : { marcius, aprilis, majus },
   nyar :   { junius, julius, augusztus },
   osz :    { szeptember, oktober, november } ])

Ezekben az esetekben tehát jó lenne, ha megtarthatnánk a sorrendet, lássunk akkor erre is egy példát:

package webotlet_linkedhashmap;
...
...
LinkedHashMap<String,String> lhmap = new LinkedHashMap<>();
lhmap.put("januar","tel");
lhmap.put("februar","tel");
lhmap.put("marcius","tavasz");
lhmap.put("aprilis","tavasz");
lhmap.put("majus","tavasz");
lhmap.put("junius","nyar");
lhmap.put("julius","nyar");
lhmap.put("augusztus","nyar");
lhmap.put("szeptember","osz");
lhmap.put("oktober","osz");
lhmap.put("november","osz");
lhmap.put("december","tel");

for( String s : lhmap.keySet() )
{
  System.out.println( s+": "+lhmap.get(s) );
}

Láthatod, hogy a tárolt adatokat a tárolás sorrendjében kaptad vissza, vagyis a LinkedHashMap valóban megőrzi az elemek sorrendjét. Mi van akkor, ha évszakokhoz rendeljük a hónapokat?

package webotlet_linkedhashmap;
...
...
LinkedHashMap<String,String[]> lhmap = new LinkedHashMap<>();
// eloszor hozzaadom az ures evszakokat,
// ezzel biztositom a sorrendjuket
lhmap.put("tel", new String[3]);
lhmap.put("tavasz", new String[3]);
lhmap.put("nyar", new String[3]);
lhmap.put("osz", new String[3]);

// a evszakokhoz darabonkent hozzaadom a megfelelo honapokat
lhmap.get("tel")[0] = "december";
lhmap.get("tel")[1] = "januar";
lhmap.get("tel")[2] = "februar";

lhmap.get("tavasz")[0] = "marcius";
lhmap.get("tavasz")[1] = "aprilis";
lhmap.get("tavasz")[2] = "majus";

lhmap.get("nyar")[0] = "junius";
lhmap.get("nyar")[1] = "julius";
lhmap.get("nyar")[2] = "augusztus";

lhmap.get("osz")[0] = "szeptember";
lhmap.get("osz")[1] = "oktober";
lhmap.get("osz")[2] = "november";

StringBuilder sb = new StringBuilder();
for (String s : lhmap.keySet())
{
  for( String ho : lhmap.get(s) )
  {
    sb.append(ho+" ");
  }
  System.out.println(s + ": " + sb);
  sb.setLength(0);
}

A Map elemeinek feltöltése csak egyesével egyedileg történhet, előre megadott literállal nem lehet megoldani. Esetleg annyit lehetne még trükközni, hogy az évszakok tömbjeit külön literálként megadod, és úgy állítod be az évszakokhoz a hónapok tömbjeit, de tömböt is csak egyszer lehet literállal feltölteni, így 4 különböző tömbre lenne szükséged.

TreeMap

Maradjunk a kutyás példánál. Szeretném, ha a kutyák gazdái névsorban lennének tárolva, és ilyen sorrendben lehetne előhívni őket. A TreeMap, mint olvashattad, pont erre való. Az egyetlen gond vele, hogy a magyar ékezetes karaktereket a z betű után kezdi listázni. Lássuk a példát:

TreeMap<String,String> hmap = new TreeMap<>();
hmap.put("Janos","Bordi");
hmap.put("Bela","Cezar");
hmap.put("Aniko","Hektor");
hmap.put("Agnes","Cezar");
hmap.put("Jozsef","Csibesz");

for( String s : hmap.keySet() )
{
System.out.println( s+": "+hmap.get(s) );
}

A program kimenete:

Agnes: Cezar
Aniko: Hektor
Bela: Cezar
Janos: Bordi
Jozsef: Csibesz
TreeMap<Integer,String> hmap = new TreeMap<>();
hmap.put(7,"vasarnap");
hmap.put(1,"hetfo");
hmap.put(4,"csutortok");
hmap.put(6,"szombat");
hmap.put(2,"kedd");
hmap.put(3,"szerda");
hmap.put(5,"pentek");

for( int i : hmap.keySet() )
{
System.out.println( i+": "+hmap.get(i) );
}

A program kimenete:

1: hetfo
2: kedd
3: szerda
4: csutortok
5: pentek
6: szombat
7: vasarnap

Map metódusok

A Map adattípus, mint már ismertettem a hierarchia csúcsa. A HashMap, LinkedHashMap és Treemap mind belőle származik, ezek közös őse. A metódusok egy része ezért megegyezik mindhárom Map megvalósításban, lássuk akkor ezek felsorolását.

Adott elem hozzáadása – put( kulcs, érték )

A Map ezzel a metódussal tölthető fel, az egyes elemeket egyesével hozzáadhatjuk.

Másik Map elemeinek hozzáadása – putAll( Map )

Egy másik map elemeit bemásolhatjuk a sajátunkba. Ez gyakorlatilag egy put() hívást jelent a paraméter Map minden elemére.

Adott érték kinyerése a kulcs által – get( kulcs )

Visszaadja az adott, egyedi kulcshoz tartozó értéket.

Adott kulcs-érték páros eltávolítása – remove( kulcs )

Eltávolítja az adott kulcsot és a hozzátartozó értéket, feltéve, hogy léteznek.

Üres a Map? – isEmpty()

Igazat ad, ha a Map nem tartalmaz elemeket.

Map mérete – size()

Visszaadja, hogy a Map hány elemet tartalmaz.

Map kulcsai – keySet()

Visszaadja a Map kulcsainak halmazát, amely egy foreach ciklussal bejárható.

Map értékei – values()

Visszaadja a Map értékeinek gyűjteményét, amely egy foreach ciklussal bejárható. Az értékek ismétlődhetnek, de nem fogod tudni, hogy melyik érték melyik kulcshoz tartozik.

Létezik-e a kulcs – containsKey( objektum )

Igaz értéket ad, ha az adott kulcs létezik.

Létezik-e az érték – containsValue( objektum )

Igazat ad, ha az adott érték hozzátartozik a Map valamelyik kulcsához, akár többhöz is.

Map kiürítése – clear()

Eltávolítja az adott Map teljes tartalmát.

Map elemeinek kinyerése – entrySet()

Visszaadja a Map elemeit olyan halmazként, mely foreach ciklussal bejárható. Ilyenkor a kulcsokat és értékeket páronként kapod meg, nincs szükség külön get() metódusra az értékek kinyeréséhez. Ehhez a példához a Map osztályt is importálnod kell. Egy egyszerű példa. Maradjunk a HashMap példánál, ahol a kutyákat és gazdáikat kell kiíratni:

import java.util.Map;
...
...
for( Map.Entry elem : hmap.entrySet() )
{
  System.out.println( elem.getKey()+": "+elem.getValue() );
}

Fontos tisztáznunk a következőt. Ez utóbbi példánál ha jobban megnézed, akkor közvetlenül a Map elemeit, a párokat nyered ki, nem csak a kulcsot. Vagyis, amikor az adott kulcshoz tartozó adott értéket akarod kikeresni, akkor nem is kell keresni a Map tartalmában, hiszen a kettőt együtt kaptad meg, páronként. Igaz, ehhez ne felejtsd el importálni az Map ősosztályt.

A fenti példa nem működik minden esetben ilyen egyszerűen. Ha megnézed, a Map.Entry esetében az elemnek nem adtam meg semmilyen típust. Hogy mi? Milyen típust kellett volna megadnom és minek?

Még mindig a kutyás példánál maradva, a kulcs-érték párok az elemek csak egyértelmű típusokat tartalmaznak. Az Entry típusát ilyenkor nem is kötelező megadni, én is elhagytam. De mi van akkor, ha az érték mondjuk egy bonyolultabb struktúra, mondjuk egy lista. Akkor tudni fogja majd magától a ciklus, hogy az Entry pontosan mit tartalmaz? Erre a következő feladat majd választ ad, addig is álljon itt az előző példa, típusokkal kibővítve, egyértelműsítve.

import java.util.Map;
...
...
for( Map.Entry<String,String> elem : hmap.entrySet() )
{
  System.out.println( elem.getKey()+": "+elem.getValue() );
}

Komplex Map példa

Tegyük fel, létre szeretnénk hozni egy olyan adatszerkezetet, melyben diákokat és osztályokat párosítunk aszerint, hogy a kulcs az osztály neve, a hozzá tartozó értékek pedig az osztályába járó diákok. Ha megtaláljuk az adott osztály osztályfőnökét is, akkor tegyük a diákok elé. Azt is kikötjük, hogy az osztályok sorrendje fontos, ami már önmagában is meghatározza, hogy melyik Map szükséges a feladathoz.

A diákokat tömbként is tárolhatnánk, ilyenkor azonban tudni kellene az osztálylétszámot is, hiszen a tömb mérete fix. Fogalmunk sincs, melyik osztályba hányan járnak, valamint az első helyet az osztályfőnöknek kell fenntartani. Ettől a ponttól kezdve tudjuk, hogy LinkedHashMap kell a programhoz, a kulcsok az osztályok nevei lesznek, a hozzájuk tartozó értékek pedig az osztályba járó diákok listája, élükön az osztályfőnökkel.

Itt egy minta a forrásból:

Resch Nora;10B
Barzo Gabor;10D
Jakab Bela;10D;OF
Balogh Aron;10A
Bogar Lenard;10C
Gergely Andras;10A;OF
Hajos Jeno;10C
Bodi Adrienn;10C

A forrást itt találhatod, amit be kell olvasni, és létre kell hozni egy adatszerkezetet, mely osztályonként tartalmazza a tanulókat úgy, hogy a tanulók listájának elején az adott osztály osztályfőnöke áll, valahogy így:

([ "10A" : [ofo, diak1, diak2, diak3, ..., diakN ],
   "10B" : [ofo, diak1, diak2, diak3, ..., diakN ],
   ... ])

A forrás, mint a mintán láthattad, ömlesztve tartalmazza a diákokat és tanárokat. Mindenki neve mögött szerepel az osztálya, valamint az osztályfőnököknél az OF megjegyzés is. A feladat szempontjából fontos tényező, hogy előre tudom, milyen osztályok adatai szerepelnek a forrásban: 10A, 10B, 10C, 10D!

Lássuk akkor a megoldást:

/**
 *
 * @author http://webotlet.hu
 */
package webotlet_maposztalyok;

import java.io.*;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.ArrayList;

public class Webotlet_MapOsztalyok
{
  public static void main(String[] args)
  {
    LinkedHashMap<String,ArrayList<String>> lhmap = new LinkedHashMap<>();
    RandomAccessFile raf;
    String sor;
    String[] adat;
    try
    {
      raf = new RandomAccessFile("map_osztalyok.csv","r");
      lhmap.put("10A", new ArrayList<String>());
      lhmap.put("10B", new ArrayList<String>());
      lhmap.put("10C", new ArrayList<String>());
      lhmap.put("10D", new ArrayList<String>());

      for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
      {
        adat = sor.split(";");
        if( adat.length == 3 )
        {
          lhmap.get(adat[1]).add(0, adat[0]);
        }
        else
        {
          lhmap.get(adat[1]).add(adat[0]);
        }
      }
      StringBuilder sb = new StringBuilder();

      for( Map.Entry<String,ArrayList<String>> e : lhmap.entrySet() )
      {
        System.out.print(e.getKey()+": ");
        for( String nev : e.getValue() )
        {
          sb.append(nev+", ");
        }

        sb.setLength(sb.length()-2);
        System.out.println(sb);
        sb.setLength(0);
      }
    }
    catch( IOException e )
    {
      System.err.println("HIBA");
    }
  }
}

Végezetül akkor álljon itt az előző fejezet kérésére a válasz. Mit is kellett megadni az Entry típusának? Pontosan az adott Map deklarálásakor megadott típusokat, hogy a foreach ciklusnak gondolkodnia se kelljen, azonnal tudja, hogy mit kap. Lásd a kiemelt részeket.

One Reply to “Java programozás 24. – asszociatív tömb avagy rugalmas párok”

  1. A TreeMap képes akármilyen nyelv sorrendezését implementálni. Ide rakom a magyar szabályok alapján működő TreeMap kódját:
    import java.text.ParseException;
    import java.text.RuleBasedCollator;
    import java.util.Map;
    import java.util.TreeMap;

    public class Start {

    public static void main(String[] args) {
    RuleBasedCollator myCollator;
    Map myMap = null;
    String hungarianRules = “< a,A < á,Á < b,B < c,C < cs,Cs,CS < d,D < dz,Dz,DZ < dzs,Dzs,DZS" +
    " < e,E < é,É < f,F < g,G < gy,Gy,GY < h,H < i,I < í,Í < j,J" +
    " < k,K < l,L < ly,Ly,LY < m,M < n,N < ny,Ny,NY < o,O < ó,Ó " +
    " < ö,Ö < ő,Ő < p,P < q,Q < r,R < s,S < sz,Sz,SZ < t,T" +
    " < ty,Ty,TY < u,U < ú,Ú < ü,Ü < ű,Ű < v,V < w,W < x,X < y,Y < z,Z < zs,Zs,ZS";
    try {
    myCollator = new RuleBasedCollator(hungarianRules);
    myMap = new TreeMap(myCollator);
    } catch (ParseException e) {
    e.printStackTrace();
    }
    myMap.put(“Zulu”,0);
    myMap.put(“Zsámbék”,1);
    myMap.put(“Kékes”,2);
    myMap.put(“Alap”,3);

    myMap.put(“Árad”,4);
    myMap.put(“Leszed”,5);
    myMap.put(“Ép”,6);
    myMap.put(“Kép”,7);

    myMap.put(“Dzun”,8);
    myMap.put(“Dzseki”,9);
    myMap.put(“Őr”,10);
    myMap.put(“Öl”,11);

    for(String key: myMap.keySet()){
    System.out.println(“Value of key: ” + key + ” is:” + myMap.get(key));

    }
    }
    }

    A források:
    https://coderanch.com/t/537850/java/Tree-Map-Sort-foreign-languages
    https://stackoverflow.com/questions/7502642/sort-a-list-of-hungarian-strings-in-the-hungarian-alphabetical-order/7705607#7705607

Vélemény, hozzászólás?

Az email címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük

*

Ez a weboldal az Akismet szolgáltatását használja a spam kiszűrésére. Tudjunk meg többet arról, hogyan dolgozzák fel a hozzászólásunk adatait..