Java SE 8 Documentation
Feladatok

equals() és hashCode()

Javaban minden típus a java.lang.Object osztály leszármazottja, ha nincs közvetlen őse. Ha pedig van, akkor az öröklődés tranzitivitása miatt szintén őse lesz, csak közvetett módon. A java.lang.Object osztálynak része néhány olyan metódus, amelyet szinte mindig célszerű megvalósítani: ezek az equals(), hashCode() és a toString(). Az utóbbival már tulajdonképpen foglalkoztunk, és definiáltuk is, az előbbiek viszont még nem szerepeltek eddig:

  • equals() : Két objektum egyenlőségének vizsgálata, egy egyenlőségi reláció. Emiatt a következő elvárások vannak ezzel a művelettel szemben (akárcsak matematikában): legyen

    • reflexív,
    • szimmetrikus,
    • tranzitív,
    • és null érték esetén mindig hamis!

    Továbbá láthatjuk azt is, hogy paraméterként itt egy másik java.lang.Object típusú értéket kapunk. Emiatt már az nem biztos, hogy az értékek típusok szintén is megfelelnek egymásnak, ezért ilyenkor ezt is ellenőriznünk kell (ld. kicsivel lentebb a példában).

          public boolean equals(Object o);
  • hashCode() : Hasításhoz alkalmazható, feladata megadni az adott példányhoz rendelhető hasítókódot, amely lényegében egy 32 bites egész szám. Ezt egyes adatszerkezetek, például halmazok vagy hasítótáblák (avagy kulcstranszformációs táblázatok, ld. később) esetén használják fel. Emiatt a metódus megvalósítása során elvárás, hogy:

    • ha az objektum nem változik, ugyanazt az értéket adja;

    • ha két objektum az equals() szerint megegyezik, a hashCode() is egyezzen meg;

    • két különböző objektumra nem kell különböző értéket adni (noha ez a hasítókód ütközéséhez vezet, amelynek vannak bizonyos következményei).

           public int hashCode();

Nézzünk meg ezek együttes alkalmazára egy példát:

class Point3D {
    private double x;
    private double y;
    private double z;

    public Point3D(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj != null && obj instanceof Point) {
           Point other = (Point) obj;
           return (other.x == this.x && other.y == this.y &&
             other.z == this.z);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return new Double(2 * x + 3 * y + 4 * z).hashCode();
    }

    @Override
    public String toString() {
        return String.format("Point3D { x = %.3f, y = %.3f, z = %.3f }",
          x, y, z);
    }
}

A fenti kódrészletben újdonság még az @Override jelölés. Ezt annotációnak nevezik. Többféle ilyen annotáció létezik, ezek közül most csak ezt fogjuk alkalmazni. Az @Override annotációnak az a szerepe, hogy segítse a fordítónak eldönteni, hogy helyesen definiáltuk-e felül (ezt jelenti tulajdonképpen magyarul az "override") az adott metódust. Akkor definiálunk helyesen felül egy metódust, ha az ősben pontosan ugyanazzal a névvel szignatúrával (paraméterlistával és visszatérési értékkel) szerepelt. Ekkor specializáljuk ugyanis az adott osztály esetén az ős által definiált valamelyik korábbi műveletet.

Mivel ezeket könnyen el lehet téveszteni, viszont olyan is előfordulhat, hogy az ősben levő metódushoz csak egy nagyon hasonló metódust akarunk készíteni, ezért ezt a szándékot az annotációval erősítjük meg. Az annotáció használata ezért nem kötelező, viszont határozottan javasolt.

Generikus (parametrikusan polimorf) osztályok

A Java nyelvben nem csak az öröklődés segítségével lehet megoldani, hogy újrafelhasználhatóak legyenek a osztályaink. Ez az, amikor az osztály leírásában típusokat lecserélünk egy paraméterre és segítségükkel egy sablont, vagy más néven generikus osztálydefiníciót hozunk létre.

Ennek illusztrálására a korábban már felmerült IntList osztályunk ilyen fajta továbbfejlesztéseként tekintsük a Store<T> osztályt, ahol már a tárolandó elemek típusával tudunk paraméterezni!

class Store<T> {
    private Object[] store;

    public Store(int size) {
       store = new Object[size];
    }

    public void add(T x) {
       store = Arrays.copyOf(store, store.length + 1);
       store[store.length - 1] = x;
    }

    public void add(int i, T x) {
       if (i >= size()) return;

       add(x);

       for (int j = size() - 1; j > i; j--) {
           set(j, get(j - 1));
       }

       set(i, x);
    }

    public void concat(Store<T> list) {
        for (Object x : list.store) {
            add((T) x);
        }
    }

    public T get(int i) {
        return ((T) store[i]);
    }

    public void set(int i, T x) {
        store[i] = x;
    }

    public void remove(int i) {
        if (i < 0 || i >= size()) return;

        for (int j = i; j < size() - 1; ++j) {
            set(j, get(j + 1));
        }

        store = Arrays.copyOf(store, store.length - 1);
    }

    public int indexOf(T x) {
        for (int i = 0; i < size(); ++i) {
            if (get(i).equals(x))
            return i;
        }

        return (-1);
    }

    public int size() {
        return store.length;
    }

    public void clear() {
        store = new Object[0];
    }

    public T[] toArray(T[] dummy) {
        return (T[]) Arrays.copyOfRange(store, 0, store.length);
    }

    public String toString() {
        return "Store<T> " + Arrays.toString(store);
    }
}

Láthatjuk viszont, hogy ez nem lesz tökéletesen generikus, mivel az objektumokat tulajdonképpen egy olyan tömbben helyezzük el, amely java.lang.Object típusú értékeket tartalmaz. Mivel azonban ez az osztály mindegyik osztály őse a nyelvben, ez nem okoz most problémát. Azért nem, mert kívülről ez nem ismert, ezért nem is tudunk mást, csak egyetlen osztályhoz (vagy annak leszármazottaihoz) tartozó objektumokat tárolni ennek az osztálynak a segítségével.

Ha ez a megszorítás nem lenne, és nem alkalmaznánk a típusparamétert, akkor a tömbben lényegében tetszőleges típusú elemek keverhetőek lennének, például vesd össze:

Object[] array = new Object[] { 1, 'a', "hello", new Object() };

Ez magyarázza részben azt is, hogy fentebb a toArray() metódusunk miért tartalmaz egy látszólag felesleges paramétert: ahhoz, hogy a megfelelő típusú értékű tömböt vissza tudjuk adni, szükségünk van arra az információra, hogy milyen legyen. A T paraméter a fordítás után ugyanis eltűnik, és futás közben ezt már nem tudjuk másképpen megállapítani. (Ami azt illeti, itt sem állapítjuk meg, csak vakon bízunk a hívóban.)

Generikus adatszerkezetek, Java Collections

A generikus adatszerkezetek a Java nyelv nagyon fontos részét képezik. Ezeket az ún. "Java Collections" nevű keretrendszerbe foglalták össze, amelyek minél mélyebb ismerete és hatékonyabb alkalmazása nagy mértékben hozzá tud járulni kezelhetőbb és egyszerűbb programok fejlesztéséhez. Az itt megtalálható adatszerkezetek mindegyikére általánosan alkalmazható metódusok gyűjteményét a java.util.Collections osztályban találhatjuk meg.

Listák

A gyűjtemények egyik fajtáját a listák alkotják. Ezek a logikailag láncolt lista adatszerkezetet valósítják meg, viszont több különböző implementációs eszközzel. Két népszerű változata a java.util.LinkedList, amelyet a ténylegesen (kétszeres, azaz oda-vissza) láncolással és referenciákkal készítettek, valamint a java.util.ArrayList, ahol dinamikus változó méretű tömbökkel tudunk dolgozni. A műveleteikről korábban már volt szó.

Deque-ek

A deque listákhoz hasonló, elemeket lineárisan tároló adatszerkezet, amely támogatja az elemek elérést, beszúrását és törlését az elejéről és a végéről. A deque elnevezés a "double-ended queue", vagyis a kétvégő sorra utal, "dekk"-nek ejtjük. Segítségükkel lehet például a klasszikus sor (FIFO, First-In-First-Out) és verem (LIFO, Last-In-First-Out) adatszerkezeteket is ábrázolni, mivel azok tulajdonképpen a deque alesetei. A deque adatszerkezetet például a java.util.ArrayDeque és a java.util.LinkedList valósítják meg.

Halmazok

A fentebb említett equals() és a hashCode() metódusok egyik felhasználási területe a halmaz, avagy java.util.Set típus, tároló. A halmazok minden elemet csak egyszer tartalmazhatnak (hasonlóan a matematikai halmazokhoz), illetve nem kerülnek rendezésre, mivel nem számít a sorrendjük. Ennek megfelelően két halmaz egyenlőségéhez elengedő, hogy ugyanazokat az elemeket tartalmazzák. Ennek lehetséges változatai a java.util.HashSet (hatékonyabb) és a java.util.TreeSet (rendezett).

Hasítótáblák (véges leképezések)

A java.util.HashMap osztály egy hasításon vagyis a hasítókód használatán alapuló adatszerkezet, amely lehetővé teszi, hogy kulcsok és értékek párjait tároljuk el. A java.util.HashMap alkalmazásának előnye, hogy így a tárolóból minden elemet ugyannyi idő alatt tudunk elérni, amennyiben ismerjük a kulcsát. (Minden más esetben be kell járnunk, illetve ilyenkor célszerű rendezni is, ld. lentebb.)

Műveletek:

  • Létrehozás és objektumok felvétele:
    HashMap<Integer, String> map = new HashMap<Integer, String>();
    map.put(21, "Twenty One");
    map.put(21.0, "Twenty One"); // fordítási hiba: a 21.0 nem Integer
  • Értékek visszakeresése:
    Integer key = 21;
    String value = map.get(key);
    System.out.println("Key: " + key + " value: " + value);
  • Bejárás (iterálás):
    map.put(21, "Twenty One");
    map.put(31, "Thirty One");

    Iterator<Integer> keySetIterator = map.keySet().iterator();

    while (keySetIterator.hasNext()) {
        Integer key = keySetIterator.next();
        System.out.println("key: " + key + " value: " + map.get(key));
    }

    // vagy egyszerűbben, a "for each" szerkezet segítségével:

    for (Integer key : map.keySet()) {
        System.out.println("key: " + key + " value: " + map.get(key));
    }
  • Méret lekérdezése és az elemek törlése:
     System.out.println("Size of Map: " + map.size());
     map.clear(); // törli a tároló tartalmát, eltávolítja az összes elemet
     System.out.println("Size of Map: " + map.size());
  • Kulcs vagy érték jelenlétének lekérdezése:
     System.out.println("Does HashMap contain 21 as key: " + map.containsKey(21));
     System.out.println("Does HashMap contain 21 as value: " + map.containsValue(21));
     System.out.println("Does HashMap contain Twenty One as value: " + map.containsValue("Twenty One")); 
  • Üresség ellenőrzése:
     boolean isEmpty = map.isEmpty();
     System.out.println("Is HashMap empty: " + isEmpty);
  • Elemek törlése:
     Integer key = 21;
     Object value = map.remove(key);
     System.out.println("The following value has been removed from Map: " + value);
  • Rendezés: A java.util.HashMap nem rendezett, sem kulcs, sem pedig értékek szerint. Ha a benne tárolt értékek vagy kulcsok szerint akarjuk rendezni, akkor érdemes valamilyen java.util.SortedMap típussá alakítani, mint amilyen például a java.util.TreeMap. Ennek van egy olyan konstruktora, amely java.util.HashMap értéket fogad el paraméterként és a benne tárolt kulcs (vagy egy szintén paraméterként átadott java.util.Comparator) szerint rendezve tárolja el az elemeket.

    Erre azért van szükség, mert a Collections.sort() Map értékekre nem alkalmazható!

     map.put(21, "Twenty One");
     map.put(31, "Thirty One");
     map.put(41, "Thirty One");

     System.out.println("Unsorted HashMap: " + map);
     TreeMap sortedHashMap = new TreeMap(map);
     System.out.println("Sorted HashMap: " + sortedHashMap); 

Feladatok

  • Az előző gyakorlaton implementált Date osztály esetén adjuk meg az equals() és hashCode() metódusokat!

  • Készítsünk egy Time osztályt, amely tartalmaz óra, perc, másodperc adatokat (mint számokat), és valósítsunk meg rá az equals(), hashCode() metódusokat!

  • Készítsünk olyan programot, amely megszámolja, hogy a bemenetként megadott szövegben szavai hány különböző betűt tartalmaznak (kis- és nagybetűk közt ne tegyünk különbséget, az egyéb karakterekkel ne foglalkozzunk)!

  • Készítsünk egy olyan programot, amely egy szöveges állományból beolvas egy szótárt, ahol a bejegyzések szó -> szó alakban találhatóak és a szabványos bemenetről beolvas egy szöveget, közben a szótárban levő szavakat a nekik megfeleltetett szavakra cseréli le!

  • Készítsünk egy programot, amely időpontokat olvas be egy állományból Time típusú objektumokba (például a következő formátum szerint: <óra> <perc> <másodperc>) és kiírja ezeket a szabványos kimenetre, azonban minden elemet csak egyszer!

  • Írjunk egy olyan programot, amely megszámolja egy állományban az egyes szavak előfordulásainak számát! (A kis- és nagybetűket ne különböztessük meg.) A program az állomány elérési útját kapja meg parancssori argumentumként!

  • Készítsük el a hasítótáblák egy lehetséges implementációját a SimpleMap osztályban! Támogassa ugyanazokat a műveleteket, mint a többi hasítótábla!

Linkek

Kapcsolódó forráskódok
Oktatói honlap
Vissza