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:
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.
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:
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:
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:
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.
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
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 interfészt 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 interfészben mezőket is definiálunk, akkor azok impliciten publikusak, statikusak és finalok lesznek. Erre azért van szükség, mert az interfészek 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 interfész í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 interfészt megvalósítják, és akik majd a konkrét működését meghatározzák egy-egy metódusnak.
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 interfész (hiszen nincs egyetlen egy megvalósított metódusa sem). Ekkor a Hangszer-t a következő módon kell megadni:
Illetve innentől a Zongora osztály nem származik a Hangszerből, hanem implementálja azt:
class Zongora implements Hangszer {
public void szolj(Hang h) {
System.out.println("Zongora.szolj()");
}
}
Interfészek és a default és static metódusok¶
Érdekes megemlíteni, hogy a Java 8 bevezetéséig az interfészek 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 interfész esetében felmerült, hogy új lehetőségeket (metódusokat) kellene elérhetővé tenni bennük (leginkább a tároló típusú objektumok hatékonyabb kezelése miatt volt égető ez az igény, erre később kitérünk a jegyzetben). Ha csak úgy kiegészítették volna ezeket az interfészekeket újabb metódusokkal, akkor az új interfészek nem lettek volna kompatibilisek a régebbi kódokkal, hiszen azokban az implementáló osztályokban nem feltétlen létezett korábban az új metódusoknak megfelelő megvalósított metódus.
A default kulcsszó engedélyezi, hogy az interfészben deklarált metódusnak törzse is lehessen. Ezzel jelezzük, hogy ezek a metódusok nem abstract metódusok, azaz bár ilyenkor ezeket a metódusokat akár persze felül is írhatjuk, ez nem kötelező. Ha az interfész metódusa elé ezzel szemben a static jelzőt tesszük be, az az implemetáló osztályban felül sem írható, és ráadásul a metódusra az interfész nevén keresztül tudunk hivatkozni. Ha nem írható felül, akkor nem is lehet a megvalósító osztályban, vagyis csak az interfészben lehet ezeket definiálni, tehát kell rendelkezzenek mindenképp törzzsel.
Ezzel a megoldással azonban az interfész már 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 interfészt? Habár a Java 8-tól így interfészben is lesznek/lehetnek törzzsel rendelkező metódusok is, azért az interfészek 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 interfészek célja, hogy teljes absztrakciót biztosítsanak, míg az absztrakt osztályok csak részleges absztrakciót adnak. Az interfész 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 interfészekekhez, amelyek a működését nem befolyásolják a végfelhasználó osztályoknak.
A Java nyelv folyamatosan változik annak érdekében, hogy magát a nyelvet egyszerűen és kényelmesen lehessen használni. A változtatások pedig értelemszerűen újabb és újabb változtatások magjai lesznek. Azzal, hogy az interfészben bekerülhettek a törzzsel rendelkező metódusok, nyilván elveszik az a tulajdonság, hogy egy interfészbeli metódus nem lehet private, hisz az nem lehetne elérhető a származtatott osztályokban. A default és statikus metódusokon keresztül immár a private metódusok elérhetőek lesznek, így tovább fejlődve a nyelv a Java 9 verziótól kezdve már ezt is megengedi az interfészben.
public interface Jarmu {
/*abstract*/ void fordul();
// Java 8-tól:
default void indul() {System.out.println("A jarmu indul -- " + defaultSzoveg());}
static void tankol() {System.out.println("A jarmu tankol -- "+ staticSzoveg());}
// Java 9-től:
private String defaultSzoveg() {return "default";}
private static String staticSzoveg() {return "static";}
}
public class Auto implements Jarmu {
@Override
public void fordul() {System.out.println("A jarmu fordul -- overriden");}
public static void main(String args[]){
Auto auto = new Auto();
auto.indul(); // default
auto.fordul(); // overriden / implementált
Jarmu.tankol(); // statikus
}
}
Kimenet
A jarmu indul -- default
A jarmu fordul -- overriden
A jarmu tankol -- static
A Jarmu interfészben definiált indul metódus anélkül használható ezután egy, az interfészt implementáló Auto osztályban, hogy az Auto osztály megadná ennek a metódusnak a működését. Illetve a statikus tankol metódus pedig az interfész, vagy az ezt implementáló osztályok példányosítása nélkül is elérhető lesz. Emellett pedig megengedett az is, hogy legyenek az interfészben private láthatóságú metódusok is, ezeket a default, illetve statikus metódusokból el tudjuk innentől érni.
"Többszörös öröklődés"¶
Javaban egy osztály több interfészt is megvalósíthat, és akár így több interfészen 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 interfészt 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ő interfész-ünk, három különböző metódussal:
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:
A következő osztály származzon az előző osztályból, és valósítsa meg mindhárom interfészt. Ekkor meg kell valósítani azokat a metódusokat, amiket az interfészek leírnak:
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 interfészekben 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 interfész-ek segítségével.
A C/C++ enumja könnyen megvalósítható az alábbi módon:
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 interfésszel 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:
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 interfészes megvalósítás helyett ez is írható:
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.
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 interfészek é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 érté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.
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