Java SE 8 Documentation
Feladatok

Interfészek

Az objektumok a külvilággal mindig azokon az adattagokon és metódusokon keresztül tudnak kommunikálni, amelyeket a külvilág számára elérhetővé tesz. Ezek az adattagok és metódusok alkotják az objektum interfészét. (Lényegében eddig is létezett az objektumainknak interfészeket, csak nem nevesítettük.) Az altípusosság segítségével azonban elérhetjük azt is, hogy az interfészeket mint önálló típusokat hozhassuk létre és ennek altípusaiként adjuk meg azokat az osztályokat, amelyek megvalósítják ezeket. A megvalósítás vagy implementáció itt azt jelenti, hogy az interfész csak a külvilág számára kötelezően elérhetővé teendő adattagokat és metódusokat sorolja fel (és így tulajdonképpen logikailag is csoportosítja), az interfész altípusaiként megadott osztályok pedig ezen metódusokhoz tartozó törzseket tartalmazzák.

Az interfész tehát úgy is tekinthetjük mint egy új referencia típust, metódusok szignatúráinak és osztályszintű konstansok gyűjteménye. Ezzel egy absztrakciós szintet vezetünk be, és lehetőségünk van függetleníteni a programjainkat a konkrét megvalósításoktól.

Az interfészeket az osztályokhoz hasonlóan definiáljuk, azonban itt az interface kulcsszót kell alkalmaznunk:

interface Comparable<T> {
    int compareTo(T object);   // public static abstract
}

Az interfész tagjait szintén az osztályokhoz hasonlóan adjuk meg, viszont ügyelnünk kell az alábbiakra:

  • Minden adattag csak és kizárólag public, static és final lehet. Ezt nem emiatt nem is kötelező kiírni, de az olvashatóság miatt hasznos lehet. (A final módosítóra hamarosan visszatérünk.)

  • Minden metódus csak és kizárólag public és abstract. (Az abstract módosítóra később még visszatérünk.)

A fentiek következménye, hogy minden adattagot inicializálni kell (különben fordítási hibát kapunk) egy előre ismert értékkel.

interface I {
    int a = b; // hibás!
    int b = 0; // public static final
    int c;     // hibás!
}

Továbbá a definícióban nem szerepelhetnek a this és super refenciák sem, hiszen mindezek miatt lényegéen osztályszintű adattagokkal és metódusokkal dolgozunk.

Az interfészek főleg tulajdonságok, viselkedés leírására, hozzáadására használhatóak, egyszerre akár többet is megvalósíthatunk belőlük. Azt, hogy egy osztály megvalósít egy interfészt, az implements kulcsszó után adhatjuk meg ott, ahol az öröklődést jelző extends kulcsszót is.

Például:

class Time extends Object implements Comparable<Time> {
    private int hour;
    private int minute;
    private int second;

    /* ... */

    // Az Object metódusai:
    @Override
    public boolean equals(Object other) {
        if (other instanceof Time) {
            Time t = (Time) other;
            return (this.compareTo(t) == 0);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return timeOfDay();
    }

    // A Comparable<Time> metódusa:
    @Override
    public int compareTo(Time other) {
        return new Integer(timeOfDay()).compareTo(other.timeOfDay());
    }

    private int timeOfDay() {
        return (hour * 3600) + (minute * 60) + second;
    }
}

Az interfészek között is lehetséges öröklődés az osztályoknál megismert extends kulcsszó által, ez viszont az ott tapasztaltaktól eltérően hivatkozhat több ősre is:

interface ThreadPolicy extends ThreadPolicyOperations, Policy, IDLEntity {}

Ezt az esetet nevezzük többszörös öröklődésnek. Ilyet osztályok esetében nem tudunk tenni. (Ezzel akarták nyelv tervezői elkerülni az ún. rombusz problémát, vagyis azt az esetet, amikor a többszörös öröklődés miatt több ősből is megkapjuk ugyannak a metódusnak a definícióját.) Valamint érdemes hozzátenni, ha viszont az öröklődési gráf kört tartalmaz, akkor az fordítási hibát eredményez.

Az interfészek esetében azt is hasznos tudni, hogy nincs közös ősük, tehát egy ős nélküli interfészt hozunk létre, akkor annak nem lesz semmilyen eleme. Ennek látszólag nincs semmi értelme — de tekintve, hogy az osztályok is majd több interfészt lesznek képesek megvalósítani, az ilyen üres interfészeket felhasználhatjuk az osztályok megjelölésére. Erre példa lehet a java.io.Serializable interfész, amely azt adja meg, hogy az adott típus átalakítható byte-sorozattá, vagy sem.

interface Serializable {}

Mivel az interfész egyben őstípusa is lesz az ezt megvalósító osztálynak, készíthetünk olyan metódusokat, amely nem konkrét osztályok objektumait várják paraméterként, hanem csak adott tulajdonságú objektumokat. Tekintsük például a java.util.Collections osztály sort() metódusát:

public static <T extends Comparable<? super T>> void sort(List<T> list);

Ez a szignatúra azt jelenti, hogy paraméterként bármilyen olyan tetszőleges típus elfogadható, amely tud tetszőleges T típusú elemeket tároló listaként viselkedni. A listákat a java.util.List interfész fogja össze. Mivel viszont rendezni akarjuk a "lista" elemeit, A T, vagyis a lista eleimnek típusára egy további megszorítást is teszünk: a T típus legyen rendezhető. Itt ezt konkrétan úgy írjuk le, hogy a T valósítsa meg a Comparable<> interfészt (itt sajnos nem intuitív módon az extends kulcsszót kell használni) olyan tetszőleges típussal (ezt jelöli a ?), amely a T-nek őstípusa (super).

A példában szereplő List<T> interfész (hézagos) definíciója az alábbi:

interface List<T> extends Collection<T>, Iterable<T> {
    boolean add(T e);
    void add(int index, T element);
    boolean addAll(Collection<? extends T> c);
    boolean addAll(int index, Collection<? extends T> c);
    void clear();
    boolean contains(Object o);
    boolean containsAll(Collection<?> c);
    T get(int index);
    boolean isEmpty();
    Iterator<T> iterator();
    T remove(int index);
    boolean removeAll(Collection<?> c);
    T set(int index, T element);
    int size();
    List<T> subList(int fromIndex, int toIndex);
    <E> E[] toArray(E[] a)
    /* ... */
}

Itt felfedezhetünk további három interfészt is. Az egyik az ősinterfész, a java.util.Collection, amely az objektumok tárolására alkalmas adatszerkezeteknek műveleteinek egy, a listákénál általánosabb leírása. A másik kettő pedig a java.util.Iterator, amelyet a java.lang.Iterable interfész által előírt metódus, az iterator() tud létrehozni:

interface Iterable<T> {
    Iterator<T> iterator();
}

interface Iterator<T> {
    boolean hasNext();
    T next();
    void remove();  // opcionális
}

Ez azt teszi lehetővé, hogy be tudjuk járni az adatszerkezetet, illetve opcionálisan törölni tudjuk belőle. Az iterátor mindig az adatszerkezet elejéről kell, induljon és a next() metódus hívogatásával kaphatjuk meg egyesével az elemeket és léphetünk tovább az adatszerkezetben. A remove() hívásával pedig az aktuális pozíción levő elemet törölhetjük.

Az iterátorok egyszerűbb változata az egyszerű felsorolás, amely annyiban különbözik, hogy teszi lehetővé az aktuális elem törlését. Ez a java.util.Enumeration interfész. Ez viszont már elavultnak tekinthető a Java újabb változataiban, helyette mindenhol az Iterator használatát javasolják. Ha nem kívánjuk támogatni a remove() műveletét, akkor azt az UnsupportedOperationException kiváltásával jelezhetjük.

Érdemes megjegyezni, hogy a "for each" szerkezet is lényegében egy iterátort használ. Vagyis az alábbi kifejezés:

List<Integer> list = new ArrayList<Integer>();

for (Integer n : list) {
    System.out.println(n);
}

mindig átírható így (és a fordító ezt is teszi):

Integer n;
for (Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(n)) {
    n = (Integer) iterator.next();
}

Továbbá még észrevehetjük, hogy ez tulajdonképpen ez a felület jól jellemzi a korábban megismert LinkedList<T> és ArrayList<T> műveleteit is. Sőt, ezek az osztályok eleve úgy készültek, hogy implementálják ezt az interfészt. Ennek következményeképpen a List<T> helyére ezek mindig beírhatóak (az altípusosság miatt).

List<Integer> listOfIntegers = new ArrayList<Integer>();

Ezért ilyen deklarációk esetén célszerű az egyenlőségjel bal oldalára mindig egy absztrakt (interfész) típust írni, a jobb oldalára pedig egy konkrét implementációt, altípust. Mivel a List<T> egy interfész, az valójában nem is használható a jobb oldalon!

A final módosító

Láthattuk, hogy az interfészekben szereplő adattagoknak a final módosítóval kell rendelkezniük. Adattagok esetén ez azt jelenti, hogy a változó lényegében konstansnak tekinthető, vagyis legfeljebb csak egyszer kaphat értéket, később már nem. Tehát annak a módosítónak a segítségével konstansokat tudunk létrehozni, amelyeket konvenció szerint csupa nagybetűvel írunk. Mivel az így megadott értékeket nem lehet megváltoztatni, nyugodtan tehetjük akár publikussá is. De ha konstansokat hozunk létre, akkor felesleges azt példányszintűként meghagyni (kivéve a speciális helyzeteket), helyette inkább osztályszintűvé szoktuk tenni.

Például:

public static final int MIN_AMOUNT = 0;
public static final int MAX_AMOUNT = 100;

A final módosító másik alkalmazási lehetősége az, amikor le akarjuk tiltani, hogy az osztályunból származtatni lehessen, vagy metódusok esetén ne engedjük a felüldefiniálását.

final class Unextendable {}
class Naive extends Unextendable {}  // nem lehetséges!

illetve:

class Some {
    final int unoverridable() { return 42; }
}

class Naive extends Some {
    @Override
    int unoverridable() { return -1; }  // nem lehetséges!
}

Feladatok

  • A korábban definiált Store<T> osztállyal valósítsuk meg a List<T> interfész (egy egyszerűsített változatá)t!

  • Az interfész részeként adjunk meg egy iterátort a Store<T> osztályra!

  • Készítsünk egy Stack<T> generikus interfészt, amely egy verem műveleteit definiálja (elem elhelyezése a verem tetején, elem kivétele a verem tetejéről, legfelső elem lekérdézése, iterátor létrehozása, méret lekérdezése, ürítés, maximális méret mint konstans, kivételek dobása hibák esetén)!

  • Valósítsunk meg egy ArrayStack<T> és LinkedStack<T> osztályt, amelyek a Stack<T> interfész két különböző implementációját adja meg! Az előbbi egy tömbbel ábrázolja a vermet és fix mérettel tud dolgozni, az utóbbi pedig láncolt listával dinamikusan növekszik!

  • A Stack<T> és annak valamelyik implementációjával készítsünk egy egyszerű veremszámológépet! A veremszámológép egy postfix formában levő kifejezést tudja kiértékelni a verem felhasználásával. Például így néz ki egy postfix jelöléssel megadott kifejezés:

    1 2 + 3 *

    amely lényegében a következő infix jelölésű kifejezésnek felel meg:

    (1 + 2) * 3
  • A korábban már használt Date osztály esetén valósítsuk meg Comparable<> interfészt! (Egészítsük a múltkor megadott equals() és hashCode() metódusokat!)

Linkek

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