Kihagyás

Interfészek

Az előadás videója elérhető a itt.

A programozási nyelvekben és a típuselméletben a polimorfizmus egy egységes interfészre utal, amit különböző típusok valósítanak meg. Jellemzően egy ősosztály típusú változó hivatkozhat ugyanazon közös ősosztályból származó (vagy ugyanazon interfészt megvalósító) osztályok példányaira. Az interfészek használata szétválasztja a mit a hogyantól.

Példa

Adott a következő osztály:

1
2
3
4
5
6
7
8
9
class Hang {
    private int magassag;
    private Hang(int m) {
        magassag = m;
    }
    public static final Hang C = new Hang(0);
    public static final Hang D = new Hang(1);
    public static final Hang E = new Hang(3);
}

Azaz minden hang objektumnak van egy magassága, amit a konstruktorban állítunk be! Kis érdekesség, hogy jelen esetben a konstruktor láthatósága private, azaz csak az osztályból tudunk létrehozni Hang objektumokat. Ez meg is történik, jelen esetben 3 hangot tartalmaz az osztály. Ráadásul minden hang objektum final módosítóval van ellátva, ami azt jelenti, hogy inicializálásukkor kapnak értéket, ami azután nem módosítható. De ez természetes is, hiszen ezek az ÁBéCés hangok fixek a való életben is, és meghatározott frekvenciával, vagy most a feladat kedvéért magassággal rendelkeznek. Valamennyi, az osztályban definiált Hang objektum static módosítóval is el van látva. Így ezek osztály adattagok lesznek, az osztállyal együtt inicializálódnak, ráadásul az osztály példányosítása nélkül el is érhetőek. (Ha nem így lenne, gondban lennénk, hiszen példányosítani más osztályból nem tudjuk az osztályt, mivel az egyetlen konstruktor, ami definiálva van benne, az private.)

Legyen adott a Hangszer osztály is.

1
2
3
4
5
class Hangszer {
    public void szolj(Hang h) {
        System.out.println("Hangszer.szolj()");
    }
}

A Hangszer osztály publikus szolj metódusa egy Hang objektumot vár paraméterként. Ezt ugyan fel nem használja jelenleg, annyit csinál csak, hogy kiírja a "Hangszer.szolj()" szöveget a standard outputra, ezzel jelezve, hogy adott esetben pontosan ez a metódus fut le.

A Zongora osztály származik, öröklődik a Hangszer osztályból, vagy ha jobban tetszik, specializálja azt. Valójában annyit csinál, hogy ezt az előbbi szólj metódust módosítja, hogy látszódjon, hogy konkrétan a Zongora osztályhoz tartozó szolj metodus hívódik meg adott esetben:

1
2
3
4
5
class Zongora extends Hangszer {
    public void szolj(Hang h) {
        System.out.println("Zongora.szolj()");
    }
}

A Hangolo osztály lesz a kulcs osztályunk a polimorfizmus működésének megértésében:

1
2
3
4
5
class Hangolo {
    public static void hangolj(Hangszer h) {
        h.szolj(Hang.C);
    }
}

Ennek az osztálynak a statikus hangolj metódusát tetszőleges Hangszer típusú paraméterrel meghívhatjuk. Lehet ez konkrétan egy Hangszer objektum, de lehet egy Zongora is, vagy bármi, aminek osztálya direkt, vagy indirekt módon származik a Hangszer osztályból. A kérdés, hogyha a paraméterként kapott h Hangszer objektum szolj() metódusát meghívjuk, akkor mely metódus fog meghívódni? A Hangszer osztály szólj metódusa? Vagy konkrétan annak az osztálynak a szólj metódusa, aminek objektumát átadtuk a hangolj metódusnak?

Teszteljük:

1
2
3
4
5
6
7
public class HangszerPelda {
    public static void main(String args[]) {
        Zongora z = new Zongora();
        //Hangszer z = new Hangszer();
        Hangolo.hangolj(z);
    }
}

Ha a z objektum típusa Zongora, és így példányosítjuk, akkor a hangolj metódus a Zongora osztályban definiált szolj() metódust fogja meghívni. Ha a z objektumot Hangszerként példányosítjuk, akkor a Hangszer osztály szolj metódusa fog meghívódni.

(Értelemszerűen ezt a HanszerPelda osztályt egyszer fordítsuk, és futtassuk úgy, ahogy az az ábrán is látszódik, majd a kommentet helyezzük át az aktív példányosítás elé, és úgy is fordítsuk le, és próbáljuk ki!)

"Elfelejteni a típust"

A Hangolo.hangolj(z) hívás során "elveszik a típus", hisz mindegy, hogy Hangszer, vagy konkrétan Zongora típusú objektumot adunk ennek a metódusnak, ő mindenképp Hangszert vár, a kapott paraméterre Hangszerként tekint. Ennek megfelelően csak olyan metódusait tudja a paraméterben kapott objektumnak meghívni, amit a Hangszer osztály definiál.

Megcsinálhatnánk persze azt is, hogy minden egyes hangszernek, amit származtatunk a Hangszer osztályból, készítünk egy külön hangolj metódust, és minden speciális hangszerre megvalósítjuk, de ez idővel nehézkessé tenné a kód karbantartását, mert minden új osztály felvételekor, amely a Hangszer osztályból származik, kellene egy megfelelő hangolj metódust létrehozni a Hangolo osztályban. Ha ezt esetleg elfelejtenénk, akkor a Hangolo működése nem volna teljes, nem megfelelő hanszerrel meghívva akár fordítási hibát is kaphatunk.

Kései kötés

Amikor futás közben meghívódik a szolj() metódus, akkor az objektum konkrét típusa alapján (azaz azon típus alapján, amivel példányosítottuk) fog vagy a Hangszer, vagy a Zongora osztály szolj() metódusa meghívódni.

Bővíthetőség

A polimorfizmusnak köszönhetően így tetszőleges számú Hangszert specializálhatunk (pl. Hegedu, Fuvola, Dob, ...), és ha bármelyikből példányosítunk egy hangszert, és azt adjuk át a Hangolo osztály hangolj() metódusának, akkor a megfelelő osztály szolj() metódusa fog meghívódni. Természetesen akkor, ha a gyerek osztályban a szolj() metódus felül volt írva.

Absztrakt osztályok és metódusok

Valójában a Hangszer osztály metódusa(i) nem olyan metódusok, amiket normál esetben meg szeretnénk hívni, hiszen minden speciális hangszer speciális módon szól, így szükségszerűen meg kell valósítani valamennyiben a szolj() metódust. De ha ez így van, akkor minek kell a Hangszer osztályban megvalósítani a szolj() metódust, ha úgyis tudjuk, hogy nem fogjuk használni? Valójában nem kell!

Ha a szolj() metodus elé betesszük az abstract módosítót, akkor nem kell definiálnunk ebben az osztályban a szolj() metodust. Ennek persze követezményei vannak.

  • Ha van legalább egy absztrakt metódus az osztályban, akkor az osztálynak is abstract-nak kell lennie.
  • Olyan osztály, ami abstract, nem példányosítható közvetlen, azaz nem lehet meghívni a konstruktorát.

Természetesen egy osztály úgy is lehet absztrakt, hogy nincs absztrakt metódusa. Ennek az értelme az, hogy így a fordító figyelmeztet, ha esetleg direktben próbálnánk példányosítani az osztályt.

Érdekes elgondolkodni pár tulajdonságán az absztrakt metódusoknak. Mivel absztrakt, így szükséges, hogy valaki felülírja, ebből adódóan viszont nem lehet előtte a final jelző, illetve private sem lehet, mert akkor a gyerek osztályban létre tudnánk hozni egy hasonló kinézetű (hasonló nevű és paraméterezésű) metódust, de az egy teljesen új metódusnak számítana, és nem az ős metódusának felülírása lenne. Ez viszont azt jelentené, hogy a vezérlés adott esetben ráfuthatna egy olyan metódusra, amelynek nincs törzse. Ez hibához vezetne. Ergo, nem lehet absztrakt metódus private.

Példa (folyt.)

Az előbbi példát tehát nyugodtan átírhatjuk úgy, hogy a Hangszer osztályt absztrakttá tesszük.

1
2
3
abstract class Hangszer {
    abstract public void szolj(Hang h);
}

Egyetlen változás ezen kívül, hogy a HangszerPelda osztály main metódusában ezután már nem példányosíthatjuk a z objektumot Hangszerként, azaz a

1
Hangszer z = new Hangszer();

utasítás fordítási hibát okozna.

Interfészek

Mi van akkor, ha egy absztrakt osztály minden metódusa absztrakt. Ilyenkor osztály helyett érdemes interface-t létrehozni. Gyakorlatilag ez annyit jelent, hogy a class kulcsszó helyett interface-t írunk. Bár ilyenkor nem kell kiírni eléjük módosítót, azokra tekinthetünk úgy, mintha impliciten publikusak és absztraktak lennének. Ha egy interface-ben mezőket is definiálunk, akkor azok impliciten publikusak, statikusak és final-ok lesznek. Erre azért van szükség, mert az interface-ek nem példányosíthatóak, konstruktoruk sem lehet, így a legvalószínűbb eset az, hogy az adattagokat mindenki számára elérhetőként szeretnénk tenni (public), ha nem lehet őket objektum példányhoz kötni, akkor csak az osztályhoz köthetjük őket (static), és hogy mindenképp legyenek inicializálva konstruktor hiányában is (final).

Az interface így egy protokollt valósít meg, azaz leírja, hogy milyen módon lehet megszólítani azokat az osztályokat, akik az adott interface-t megvalósítják, és akik majd a konkrét működését meghatározzák egy-egy metódusnak.

Érdekes megemlíteni, hogy a Java 8 bevezetéséig az interface-ek szigorúan törzs nélküli metódusokat tartalmaztak, azonban a Java 8-tól lehetővé vált az is, hogy bizonyos esetekben egy-egy metódusnak legyen megvalósítása. Erre azért volt szükség, mert sok-sok interface esetében felmerült, hogy új lehetőségeket (metódusokat) kellene elérhetővé tenni bennük. Ha csak úgy kiegészítették volna ezeket az interface-eket újabb metódusokkal, akkor már az új interface-ek nem lettek volna kompatibilisek régebbi kódokkal, hiszen azokban az implementáló osztályokban nem feltétlen van az új metódusoknak megfelelő megvalósított metódusok.

A default kulcsszó engedélyezi, hogy az interface-ben deklarált metódusnak törzse is lehessen. Ilyenkor ezeket a metódusokat akár persze felül is írhatjuk. Ha az interface metódusa elé ezzel szemben a static jelzőt tesszük be, az az implemetáló osztályban nem írható felül, a metódusra az interface nevével tudunk hivatkozni. Vagyis ha nem írható felül, akkor nem is lehet a megvalósító osztályban, ergo csak az interface-ben lehet ezeket definiálni, tehát kell rendelkezzenek törzzsel.

Ezzel a megoldással azonban az interface majdnem olyan, mint egy absztrakt osztály. Mi értelme így az absztrakt osztályoknak? Illetve mit érdemes készítenünk? Absztrakt osztályt, vagy interface-t? Habár a Java 8-tól így interface-ben is lesznek/lehetnek törzzsel rendelkező metódusok is, azért az interface-ek különböznek az absztrakt osztályoktól. Például előbbiben nem lehet konstruktor. Az újítások ellenére még mindig igaz, hogy interface-ek célja, hogy teljes absztrakciót biztosítsanak, míg az absztrakt osztályok csak részleges absztrakciót adnak. Az interface egy lenyomatot ad, hogy mi az, amit az implementáló osztályok megvalósítanak, a default metódusok megjelenésével csupán extra funkciókat adhatunk az interface-ekhez, amelyek a működését nem befolyásolják a végfelhasználó osztályoknak.

Példa (folyt.)

A hangszeres példánk akár úgy is megvalósítható, hogy maga a Hangszer absztrakt típus nem osztály, hanem interface (hiszen nincs egyetlen egy megvalósított metódusa sem). Ekkor a Hangszer-t a következő módon kell megadni:

1
2
3
interface Hangszer {
    void szolj(Hang h); //impliciten public és abstract
}

Illetve innentől a Zongora osztály nem származik a Hangszerből, hanem implementálja azt:

1
2
3
4
5
class Zongora implements Hangszer {
    public void szolj(Hang h) {
        System.out.println("Zongora.szolj()");
    }
}

"Többszörös öröklődés"

Javaban egy osztály több interface-t is megvalósíthat, és akár így több interface-en keresztül megkaphatja azt a leírást, hogy egy adott metódust az osztálynak meg kell valósítania, de mivel az adott osztály megadja az adott metódus megvalósítását, ezzel nincs gond. Osztályból viszont csak egy osztályból származhat egy adott osztály, így ott nem fog előfordulni a többszörös öröklődés.

A default metódusok bevezetése azonban ezt a koncepciót befolyásolja, hiszen mi van, ha egy adott osztály megvalósít két olyan interface-t is, amelyek tartalmaznak ugyanolyan deklarációjú default metódust is? Ilyen esetben az implementáló osztálynak kötelezően felül kell írnia ezeket a metódusokat, ezzel megszüntetve a többszörös öröklődés problémáját.

"Többszörös öröklődés" példa

Legyen adott három különböző interface-ünk, három különböző metódussal:

1
2
3
4
5
6
7
8
9
interface Kuzdo{
  void kuzdj();
}
interface Uszo {
  void usszal();
}
interface Repulo {
  void repulj();
}

Legyen továbbá egy osztályunk, amely a fenti metódusokból egyet meg is valósít:

1
2
3
class Szereplo {
    public void kuzdj() { /**/ }
}

A következő osztály származzon az előző osztályból, és valósítsa meg mindhárom interface-t. Ekkor meg kell valósítani azokat a metódusokat, amiket az interface-ek leírnak:

1
2
3
4
class AkcioHos extends Szereplo implements Kuzdo, Uszo, Repulo {
    public void usszal() {}
    public void repulj() {}
}

Mivel a Szereplo osztály már megvalósította a kuzdj() metódust, ezt az AkcioHos is örökli, így nem feltétlenül kell azt már felülírnia.

Konstansok csoportosítása

Az interface-ekben mivel minden adattag szükségszerűen static és final, azaz egy példányban lesznek jelen a memóriában, illetve az értékük inicializálás után nem változhat, így jó alternatíva lehet, hogy valamilyen szempontból kapcsolódó konstansokat csoportosítsunk az interface-ek segítségével.

A C/C++ enumja könnyen megvalósítható az alábbi módon:

1
2
3
4
5
6
7
interface Months {
  int
    JANUARY = 1, FEBRUARY = 2, MARCH = 3, 
    APRIL = 4, MAY = 5, JUNE = 6, JULY = 7, 
    AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
    NOVEMBER = 11, DECEMBER = 12;
}

Ezzel az interface-szel pl. a május hónap hivatkozható a Months.MAY kifejezéssel, az értéke pedig 5, ami megfelel a hónap sorszámának. Ha szeretnénk kiíratni ezt az értéket, akkor a következő kóddal ez meg is tehetjük:

1
2
3
4
5
public final class HonapPelda {
    public static void main(String[] args) {
        System.out.println(Months.MAY);
    }
}

Enumerációk

A Java 1.5-től kezdődően nyelvi elem lett az enum, ezután az interce-es megvalósítás helyett ez is írható:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum Months {
    JANUARY,
    FEBRUARY,
    MARCH,
    APRIL,
    MAY,
    JUNE,
    JULY,
    AUGUST,
    SEPTEMBER,
    OCTOBER,
    NOVEMBER,
    DECEMBER
}

Azonban ha most hivatkozunk a Months.MAY elemre, annak értéke nem egy egész szám, hanem a "MAY" szöveg.

Hasonlóan azonban a C/C++-os enum felsorolás típushoz, egy enum érték lehet a switch utasítás szelektor kifejezése, így könnyen írhatunk olyan kódokat, ami a különböző enum értékekre különbözőképpen reagá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
25
26
27
28
29
30
31
enum Months {
    JANUARY,
    FEBRUARY,
    MARCH,
    APRIL,
    MAY,
    JUNE,
    JULY,
    AUGUST,
    SEPTEMBER,
    OCTOBER,
    NOVEMBER,
    DECEMBER
}

public final class HonapPelda2 {
  public static void main(String[] args) {
    Months m = Months.MARCH;
    switch (m) {
    case MARCH:
      System.out.println("Erkezik a tavasz :)");
      break;
    case JULY:
      System.out.println("Hurra, nyaralas! :)");
      break;
    default:
      System.out.println("Atlagos honap...");
      break;
    }
  }
}

A HonapPelda2 osztály main metódusában definiált m enum érték attól függően, hogy az a márciusnak, vagy júliusnak felel meg mást ír ki, mint mindent egyéb esetben.

A Javaban az enum azonban sokkal többet tud, mint a C/C++-os enum. Olyan kicsit az enum típus, mint az interface-ek és osztályok egyesítése olyan értelemben, hogy definiálhatunk bennük konstansokat, meghatározott értékkel, ugyanakkor ezek a konstansok nem egy egész éréket képviselnek, hanem az enum adattípus egy-egy konkrét objektumait. És mint ilyen, az enum minden objektuma rendelkezhet saját adattaggal, adattagokkal, illetve metódusokkal, valamint konstruktorokkal. Amikor felsoroljuk az enum értékeket (ezt egyébként rögtön az enum definiálásának elején meg kell tegyük), akkor mögöttük egy paraméter listával megadhatjuk azokat a paramétereket, amelyek ahhoz kellenek, hogy az enum konstruktora meghívódhasson az egyes értékek inicializálásakor. Minden enum a java.lang.Enum osztályból származik, ahonnan megörökli még a values() metódust, ami egy vektorban visszaadja az adott enum értékeit, amit vagy egy hagyományos for ciklussal bejárhatunk, vagy a foreach szintaxis segítségével, amelyet éppen azért vezettek be a nyelvbe, hogy az ilyen felsorolások elemeit könnyebb legyen egyesével bejárni.

 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
enum Months {
    JANUARY(31), FEBRUARY(28), MARCH(31),
    APRIL(30), MAY(31), JUNE(30),
    JULY(31), AUGUST(31), SEPTEMBER(30),
    OCTOBER(31), NOVEMBER(30), DECEMBER(31);

    private final int napokSzama;

    Months(int napokSzama) {
        this.napokSzama = napokSzama;
    }

    public int get() {
        return napokSzama;
    }

    public static void main(String args[]) {
        Months[] m = Months.values();
        for (int i = 0; i < m.length; ++i) {
            System.out.println(m[i] + " napjainak a szama " + m[i].get());
        }
        // foreach szintaxissal (lásd később)
        for (Months p: Months.values()) {
            System.out.println(p + " napjainak a szama " + p.get());
        }
    }
}

Kiegészitve a Months enumunkat egy main metódussal, tesztelhetjük is az enum elemeinket. A mainben levő két for ciklus ugyanazt csinálja, csak különböző szintaktikával. Minden egyes hónap megnevezése mellett kiírja az adott hónap napjainak a számát:

Kimenet

JANUARY napjainak a száma 31
FEBRUARY napjainak a száma 28
MARCH napjainak a száma 31
APRIL napjainak a száma 30
MAY napjainak a száma 31
JUNE napjainak a száma 30
JULY napjainak a száma 31
AUGUST napjainak a száma 31
SEPTEMBER napjainak a száma 30
OCTOBER napjainak a száma 31
NOVEMBER napjainak a száma 30
DECEMBER napjainak a száma 31


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