Kihagyás

5. gyakorlat

A gyakorlat anyaga

Töltsük be Eclipse-be a korábban már elkészített Ember osztályt!

Öröklődés

Az előző órán már találkoztunk az objektumorientáltság egyik alapfogalmával, az egységbezárással, mely szerint az adatokat és a rajtuk végzett műveleteket egységbezártuk, egy osztályba. Ezen a gyakorlaton pedig megismerkedünk az UML-ből már ismerősen hangozható öröklődéssel. Az öröklés osztályok között valósul meg, egy szülő (ős, base, parent) és gyerek (leszármazott, derived) között.

Ennek során a gyerekosztály örökli a szülőjének tulajdonságait és viselkedését. Ez jó dolog, mert nem kell újraírnunk őket, viszont a szülőtől örökölt egyes metódusokat speciálisabban is megvalósíthatjuk, felülírhatjuk őket(override), vagy akár újakat is definiálhatunk.

Javaban csak egyszeres öröklődés van!

Ez azt jelenti, hogy egy osztálynak nem lehet kettő, vagy több szülőosztálya. Azt viszont nem zárja ki, hogy egy osztálynak több gyerekosztálya legyen, vagy hogy a gyerekosztálynak lehessenek saját gyerekosztályai.

Láthatóságok - protected

Találkozhatunk egy új láthatósággal, amelynek neve protected. Ennek a láthatóságnak a segítségével biztosíthatjuk, hogy az egyes adattagok, metódusok a gyermekosztályok számára is láthatóak legyenek. A láthatóságokról egy összefoglaló táblázat:

Módosító Osztály Csomag Leszármazottak Mindenki
public Látható Látható Látható Látható
protected Látható Látható Látható Nem látható
nincs kulcsszó Látható Látható Nem látható Nem látható
private Látható Nem látható Nem látható Nem látható

A szülőosztályban nem szükséges módosításokat eszközölnünk, a gyerekosztályban viszont jeleznünk kell, hogy melyik szülőosztályból származik az adott osztály. Tekintsük az alábbi példát:

public class Torta {
    protected int szelet;
    private String iz;

    public String getIz() {
        return iz;
    }

    public void setIz(String iz) {
        this.iz = iz;
    }

    public Torta(int szelet, String iz) {
        this.szelet = szelet;
        this.iz = iz;
    }

    public void info() {
        System.out.println("Ez a torta " + this.iz + " ízű, és "
                + this.szelet + " szeletes.");
    }
}

Extends, super

Ez egy általános torta osztály, tudjuk, hogy egy torta milyen ízű és hány szeletből áll. Tortát bármilyen alkalomra vásárolhatunk, azonban lehetnek speciális alkalmak, amelyek esetében szeretnénk használni a már megírt Torta osztályunkat, de szeretnénk új, speciálisabb adatokat, metódusokat létrehozni a tortáinknak. Ilyen lehet például egy SzulinapiTorta osztály, amely torta, de szeretnénk egy új adatot is tárolni, mégpedig a rajta lévő gyertyák darabszámát.

extends - ezzel a kulcsszóval érhetjük el az öröklődést, az osztály deklarációjában, az osztály neve után kell írnunk, majd az extends kulcsszó után az ősosztály nevét írjuk.

super - a gyerekosztályból hivatkozhatunk a szülőre, annak adattagjaira (amiket látunk) és metódusaira is, ezeket super.szuloMetodusanakNeve()-szerű parancsokkal érhetjük el.

public class SzulinapiTorta extends Torta {
    private int gyertyakSzama;

    public SzulinapiTorta(int szelet, String iz, int gyertyakSzama) {
        super(szelet, iz);
        this.gyertyakSzama = gyertyakSzama;
    }

    public void kivansagotTeljesit() {
        System.out.println("Kívánságod teljesült!");
    }

    public void info() {
        System.out.println("Ez a szülinapi torta " + this.getIz() + " ízű, és "
                + this.szelet + " szeletes." + this.gyertyakSzama
                + " db gyertya van rajta");
    }

}

A szülő konstruktora pedig egyszerűen a super kulcsszó metódusként való használatával érhető el, például:

super(szelet, iz);

Ha az ősosztály paraméter nélküli konstruktorát szeretnénk meghívni, akkor a super(); hívás a gyermek osztály konstruktorában elhagyható.
Ha nem a default konstruktorát használjuk az ősosztálynak, akkor viszont kötelező a super(arg1,arg2...argn); meghívása a gyerekosztály konstruktorában!

A gyerekosztályban láthatjuk, hogy az ősből örökölt info() metódust felüldefiniáltuk (override), annak egy speciálisabb működést adtunk.

Polimorfizmus - Többalakúság

A gyerekosztály egy példánya kezelhető a szülő egy példányaként is, egy SzülinapiTorta objektumot tárolhatunk Torta példányként is (azaz egy Torta típusú referenciában), sőt akár egy ős típusú tömbben eltárolhatjuk az ős- és gyerektípusokat vegyesen. Azonban, ha ős típusként tárolunk egy gyerek típusú objektumot, akkor a gyerek típusú objektum saját osztályában definiált metódusait nem látjuk. Például:

public class TortaMain {
    public static void main(String[] args) {
        Torta csokiTorta = new SzulinapiTorta(15, "csoki", 9);
        csokiTorta.kivansagotTeljesit();    // Ez nem fog működni
    }
}

A fenti kódrészlet nem működik, mert az átlagos Torta nem tud kívánságot teljesíteni. Mivel egy Torta referenciában tároljuk a SzülinapiTorta objektumot, így csak a Tortában definiált metódusokat használhatjuk! Ennek kiküszöbölésére később látni fogunk egy módszert.

Ugyanígy nem lehetne a gyertyák számát lekérni vagy módosítani sem (még akkor sem, ha public láthatóságú lenne ez az adattag), mert az érintett Torta nem biztos, hogy rendelkezik ilyen adattaggal.

Torta[] cukraszda = new Torta[3];
cukraszda[0] = new Torta(20, "csokis-meggyes");
cukraszda[1] = new Torta(12, "epres");
cukraszda[2] = new SzulinapiTorta(12, "karamell", 12);

Fordításkor még nem tudjuk, hogy a Torta tömbben milyen típusú objektumok lesznek: Torta objektumok, vagy pedig SzulinapiTorta objektumok, esetleg vegyesen, hiszen megtehetjük, hogy egy tömbbe gyerek típusokat teszünk.
Viszont, ha meghívjuk mindegyik elem info() metódusát, azt látjuk, hogy a sima torták esetében a Torta osztályban definiált metódus fut le, míg a születésnapi torta esetében a SzulinapiTorta osztályban definiált info() metódus hívódik meg. Ennek oka pedig a kései kötés (late binding). A kései és korai kötésről bővebben itt és itt olvashatsz.

for (int i = 0; i < cukraszda.length; i++) {
    cukraszda[i].info();
}

Ennek kimenete:

Ez a torta csokis-meggyes ízű, és 20 szeletes.
Ez a torta epres ízű, és 12 szeletes.
Ez a szülinapi torta karamell ízű, és 12 szeletes. 12 db gyertya van rajta

Ahogy említettük, a gyerek típus kezelhető ősként, viszont ez fordítva nem működik! SzülinapiTorta tömbbe nem tehetünk ős típusú, azaz sima Torta objektumokat.

Final

Ha egy osztály final, akkor nem lehet gyereke. Ha egy metódus final, akkor nem lehet felülírni gyerekben.

final class EskuvoiTorta extends Torta {
}

Ennek jelentése, hogy az EskuvoiTorta osztályból nem származhat gyerekosztály. Erről bővebben itt és itt olvashatsz.

Csomagok

Osztályainkat csomagokba rendezhetjük, ahogy erről az UML esetében is említést tettünk. Java osztályok esetében ez egy fizikai és egy logikai csoportosítást is jelent, általában különböző logikai egységenként hozunk létre csomagokat, illetve megkönnyíti a láthatóságok kezelését, és kiküszöbölhetőek vele a névütközések is. Csomagokban lehetnek egyéb csomagok is, teljes csomag-hierarchiát hozhatunk vele létre (például egy csomag, ami a felhasználói felület megjelenítésével foglalkozik, ennek is lehetnek logikailag különálló részei, melyek ezen a csomagon belül helyezkedhetnek el).

Csomagokba szervezés

Ha az osztályunkat egy csomagba szeretnénk berakni, akkor egyrészt be kell tennünk fizikailag abba a mappába vagy mappaszerkezetbe, ami a csomagunkat/csomaghierarchiánkat szimbolizálja, majd a forrásfájl első nem komment sorába ezt jelölni is kell a package kulcsszó használatával.

package hu.cukraszda.tortak;

Ennek jelentése: az osztályunk a tortak nevű csomagban van, ez a csomag a cukraszda nevű csomagban van, ez pedig a hu nevű csomagban. Fizikailag az osztály a projekt gyökérkönyvtár/hu/cukraszda/tortak mappában van. A projekt gyökérkönyvára a projektben már sokszor látott src mappa.

Általában a fordított domain jelölést szokták alkalmazni csomaghierarchiák szervezésére. Bővebb információ a csomagok elnevezéséről itt olvasható.

Csomagot készíthetünk Eclipse alatt, viszont ilyenkor is az osztály elején ott kell lennie egy package csomagneve; sornak. Ezt a sort csak a ZH-n elég egyszerűen odaírni, de a valóságban figyelni kell, hogy a könyvtárszerkezet ezzel megegyező legyen. A kötelező programoknál sokszor fordul elő hiba ezzel kapcsolatban.

Az osztályoknak van egy teljes nevük, amely a teljes csomag hierarchia + osztály nevéből tevődik össze. Tehát a fenti Torta osztálynak a teljes elérési neve (fully qualified name), ha a hu.cukraszda.tortak csomagban van: hu.cukraszda.tortak.Torta. Erre néhány további példa: java.lang.String.

Ugyanakkor ilyen hosszú nevet mi sosem írtunk, ha egy Stringet szerettünk volna létrehozni. És ezt a továbbiakban is elkerülhetjük, ha a szükséges csomagok tartalmát importáljuk a programunkba.

Csomagok importálása

Másik csomagban lévő osztályokra hivatkozás előtt be kell őket importálni, vagy pedig teljes nevet kell használni, hogy a fordító tudja, mire gondolunk.

import hu.cukraszda.tortak.Torta;
import hu.cukraszda.tortak.SzulinapiTorta;

Ez vonatkozik saját osztályainkra, de az egyéb, nem általunk megírt osztályokra is (például a JDK kész osztályai). Kivétel ezalól a java.lang összes osztálya, melyekbe például az összes csomagoló osztály vagy a String osztály tartozik. Ezt a csomagot eddig sem importáltuk soha, és tudtunk String példányokat létrehozni.

Használhatunk statikus importot is, ám ez sok esetben csak ront a kód olvashatóságán, érthetőségén, karbantarthatóságán. A statikus importról bővebben itt olvashatsz.

Java fájljainkban a csomag jelölése (ha létezik ilyen) mindig megelőzi az importálásokat.

Feladatok

Kocsmaszimulátor part 2:

A meglévő Kocsmáros osztályunkat örököltessük az Ember-ből, mivel a kocsmárosok is emberek. Ettől automatikusan megkapja az Ember osztályban deklarált adattagokat (változókat) is. A Kocsmároshoz írjunk egy új paraméteres konstuktort, ami az Ember (tehát az ős) konstruktorát is meghívja (super kulcsszó). Elvileg a Kocsmáros így működőképes lesz, ha nem, akkor hozzuk működőképes állapotba további hibaelhárítással.

Az Ember osztályunkból örököltessünk egy Diákot is, aki egy különleges ember lesz. A diák rendelkezzen privát ösztöndíj változóval, ami megadja, hogy mennyi pénzt keres a tanulással. Legyen egy tanul() metódusa, amitől az ösztöndíja mennyiségével nő a pénze. Legyen egy paraméteres tanul(int mennyit) metódusa is, amivel be lehet állítani, mennyit tanuljon, és ennek megfelelő pénzt kap.

Az Emberből származzon még egy Kidobó is. Neki legyen egy privát boolean dolgozik adattagja. Legyen egy static változója is, ami azt fogja megadni, hogy hány olyan kidobó van, aki dolgozik éppen. Ez alapból 0. Új kidobó példányok konstruktorába rakjuk ennek a változónak a növelését, és változtassuk mindig megfelelően, ha a dolgozik változó értéke változhat (setter, szolgalatbaAll(), szolgalatbolKilep()). Legyen tehát szolgalatbaAll(), és szolgalatbolKilep() metódusa is, amelyek a dolgozik értékét állítják. Írjuk felül az iszik függvényét úgy, hogy ellenőrizze le, az adott kidobó éppen dolgozik-e, mivel szolgálatban nem ihat alkoholtartalmú italt, azonban 0 alkoholtartalmút igen. Ha nincs szolgálatban, hívjuk meg az Ember (tehát super) iszik függvényét.

A Diák, és a Kidobó is rendelkezzen paraméteres konstruktorral.

A Diák, Kocsmáros, és Kidobó toString()-jét írjuk felül, hívjuk meg benne az Ember toString()-jét is, valamint írjuk ki az adott osztályra jellemző egyéb adatokat is.

Az Ember rendelkezzen egy kötözködik(Ember kivel) metódussal, ami egy másik embert vár paraméterül. Le kell ellenőrizni, hogy van-e szolgálatban lévő kidobó, ha igen, az első embert hazaküldjük. Ha nincs, kiírjuk, hogy jót kötözködött.

Italok

Az Ital osztályból örököltessünk egy Sör, Bor, és egy Kevert osztályt.

A Bor rendelkezzen egy privát int évjárat változóval. A Sörnek ne legyen új változója. Ezek kapjanak megfelelő konstruktort.

A Kevert osztálynak legyen 2, 3, 4 paraméteres konstruktora is, ami italokat vár paraméterül, például: public Kevert(Ital it1, Ital it2, Ital it3, Ital it4)

Ezek az italok lehetnek sima Ital, Sör, vagy Bor tipusúak, ezt nem kell külön lekezelnünk, mert erre is jó az öröklődés, mivel ezek mind kezelhetőek Italként.

A konstruktor a Kevert ital alkoholtartalmát a megadott italok átlag-alkoholtartalmára állítsa be. Egy példa a konstruktor hívására:

Kevert kevert1 = new Kevert(sor1, sor2, bor1, ital1);

Minden osztály privát adattagjainak legyenek getterei és setterei.

A main függvényben játsszunk kicsit a beépített új funkciókkal is.

Kapcsolódó linkek

Oracle: Controlling Access to Members of a Class

Java access specifiers

Packages

Inheritance

Inheritance & Polymorphism