Kihagyás

5. gyakorlat

Ismétlés

  • Osztályok (adattagok, metódusok, konstruktorok, toString)
  • Láthatóságok (private, protected, semmi, public)
  • Öröklődés (super, extends, felüldefiniálás)

Az órai anyaghoz szükséges lesz a korábbi Macskás példa is, amit szükség esetén itt tölthetsz le.

Polimorfizmus - Többalakúság

A többalakúság az objektum-orientált programozás egyik alapja. De mit is jelent pontosan a többalakúság? Hogy képzelhetjük el? Először is fontos tisztázni, hogy milyen polimorfizmusról van szó, ugyanis a polimorfizmusnak két típusa van: statikus és dinamikus polimorfizmus.

Statikus polimorfizmus

A statikus, vagy fordításidejű polimorfizmus a polimorfizmusnak az a formája, ami fordításidőben eldönthető. Ez a gyakorlatban azt jelenti, hogy több, azonos nevű, de más paraméterlistával rendelkező metódusból fordításidőben el tudjuk dönteni, hogy egy hívási helyen melyik változata fog meghívódni.

Ez esetleg overload néven lehet ismerős. Egy nagyon egyszerű példa:

public class Kalkulator {

    public int szorzas(int egesz1, int egesz2) {
        return egesz1 * egesz2;
    }

    public int szorzas(int egesz1, int egesz2, int egesz3) {
        return egesz1 * egesz2 * egesz3;
    }

    public String szorzas(String mit, int hanyszor) {
        return mit.repeat(hanyszor);
    }
}
Ezt az osztályt használjuk a main metóduson belül:

public class KalkMain {
    public static void main(String[] args) {
        Kalkulator kalk = new Kalkulator();

        System.out.println(kalk.szorzas(2, 5));
        System.out.println(kalk.szorzas(2, 5, 3));
        System.out.println(kalk.szorzas("Körte", 4));
    }
}

Természetesen ez csak egy egyszerű példa, a valóságban bármilyen osztály esetén előjöhet az overloadra az igény, ahogy azt láthattuk már például a konstruktorok esetén is, ahol több, azonos nevű konstruktorunk volt, a fordító mégis tudta, hogy a paraméterek alapján melyik konstruktort szerettük volna használni.

Az objektum-orientált programozásnál azonban talán az érdekesebb dolog a dinamikus polimorfizmus lesz.

Dinamikus polimorfizmus

Ebben az esetben a gyerekosztály egy példánya kezelhető a szülő egy példányaként is, egy Tigris objektumot tárolhatunk Macskafele példányként is (azaz egy Macskafele típusú referenciában), sőt akár egy ős típusú tömbben eltárolhatjuk az ős- és gyerektípusokat vegyesen, például macskaféléket, házi macskákat, tigriseket és kardfogú tigriseket is.

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 (új) metódusait nem látjuk. Például:

public class MacskaMain {

    public static void main(String[] args) {
        Macskafele valami0 = new Macskafele("Csöpi", 10);
        Macskafele valami1 = new HaziMacska("Cirmi", 4.9, true);
        Macskafele valami2 = new Tigris("Lüszi", 24);
        // valami2.erosebb(...) nem látható így
        Macskafele valami3 = new KardfoguTigris("Nyüzsi", 30);
    }
}

A fenti kódrészletben a kikommentelt kódrészlet nem működik, mert az átlagos Macskafele nem tudja eldönteni, hogy erősebb-e mint egy Tigris. Mivel egy Macskafele referenciában tároljuk a Tigris objektumot, így csak a Macskafeleben definiált metódusokat használhatjuk! Ennek kiküszöbölésére később látni fogunk egy módszert.

Ennek oka, hogy mivel Macskafele referenciában tároljuk az objektumot, nem tudhatjuk (most még), hogy milyen objektumra mutat az adott referencia.

        Macskafele[] macskak = new Macskafele[4];
        macskak[0] = new Macskafele("Csöpi", 10);
        macskak[1] = new HaziMacska("Cirmi", 4.9, true);
        macskak[2] = new Tigris("Lüszi", 24);
        macskak[3] = new KardfoguTigris("Nyüzsi", 30);

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

Fordításkor még nem tudjuk, hogy a Macskafele tömbben milyen típusú objektumok lesznek: Macskafele objektumok, Tigris, vagy pedig KardfoguTigris objektumok, esetleg vegyesen, hiszen megtehetjük, hogy egy tömbbe gyerek típusokat teszünk. Viszont, ha meghívjuk mindegyik elem nyavog() metódusát, azt látjuk, hogy a sima macskafélék esetében a Macskafele osztályban definiált metódus fut le, míg a házi macska esetében a HaziMacska osztályban definiált nyavog() metódus hívódik meg, stb. 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.

A forráskód kimenete:

Kimenet

Cirmi nyávog: MeoOoOow

Lüszi üvölt: RaaAaAaAaAaAaAaAaAaAaAaAaAwR

Nyüzsi üvölt: RaaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAaaAAwR

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

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 ennek használatával megismerkedhetünk a kulcsszó nélküli, package private láthatósággal, é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.szte.macskak;

Ennek jelentése: az osztályunk a macskak nevű csomagban van, ez a csomag a szte nevű csomagban van, ez pedig a hu nevű csomagban. Fizikailag az osztály a projekt gyökérkönyvtár/hu/szte/macskak 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 IDEA segítéségével (jobb klikk az src mappán majd New > Package), ilyenkor az osztály elején ott kell lennie a package csomagneve; sornak. Figyelni kell, hogy a könyvtárszerkezet ezzel megegyező legyen. A kötelező programoknál sokszor fordul elő hiba ezzel kapcsolatban. Ha nem megfelelő csomagban van a forrásfájlunk, akkor a legtöbb IDE egyébként segítséget is nyújt a rendezésben (fájl mozgatásával, vagy a csomagmegjelölés módosításával).

Minden osztálynak van egy teljes neve, amely a teljes csomag hierarchia + osztály nevéből áll. Tehát a fenti Macskafele osztálynak a teljes elérési neve (fully qualified name), ha a hu.szte.macskak csomagban van: hu.szte.macskak.Macskafele. Erre néhány további példa: java.lang.String, ˙java.util.Scanner.

Ugyanakkor ilyen hosszú nevet mi sosem írtunk, ha egy Stringet (vagy Scannert) 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.

A csomagokba szervezést természetesen az UML-ben is jelölhetjük, a kapcsolódó osztályokat (vagy egyéb csomagokat) egy UML-beli csomagba tehetjük, ahogy látható az alábbi ábrán is.

Csomag az UML-ben

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 (a programban végig), hogy a fordító tudja, mire gondolunk.

import hu.szte.macskak.Macskafele;
import hu.szte.macskak.KardfoguTigris;

Amikor sok oszályt importálunk egy csomagból, akkor az egyesével való importálás helyett beimportálhatunk minden csomagban lévő osztályt, a csillag karakter segítségével:

import hu.szte.macskak.*;

Az importjainkat rendezhetjük az IDEA segítéségével, amire a Ctrl+Alt+O (vagy az Optimize Imports menüpont) segítségével

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ályaira). Kivétel ezalól a java.lang összes osztálya, melyekbe például az összes csomagoló osztály (Integer, Float, stb.) 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, sőt igaziból minden mást is, csak komment lehet a package csomagneve; sor előtt.

Láthatóságok

UML jelölés 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ó

Garbage collection - Szemétgyűjtés

Objektumok élettartama Java-ban: (élettartam = objektum létrejöttétől a memória felszabadításáig) Amikor létrehozunk egy objektumot a new kulcsszóval, az objektum a heapen jön létre (ellentétben a primitív típusokkal). A memória felszabadítása automatikus Javaban, a garbage collector (Szemétgyűjtő) végzi el a feladatokat. A Java szemétgyűjtőről bővebben ezen, ezen és ezen az oldalon olvashatunk. Ha egy objektumot nem használunk tovább, beállíthatjuk null-ra, ez a kulcsszó jelöli, hogy az adott referencia nem hivatkozik egyetlen objektumra sem (pl. tigris = null;) Garbage collector hívása manuálisan (nem biztos hogy lefut):

System.gc();

Lehetőségünk van az osztályainknak egy finalize()-nak nevezett metódust készíteni. Ez a metódus akkor fog lefutni, amikor a szemétgyűjtő felszabadítja a feleslegesnek ítélt memóriát, és egy adott objektum, amit már nem használunk, törlődni fog. Azt természetesen nem tudni, hogy mikor fog lefutni, vagy hogy le fog-e egyáltalán. A metódus célja az objektum által használt valamilyen erőforrás felszabadítása (erre később látunk példát).

Feladatok

Kocsmaszimulátor elkészítése

Videók

Kapcsolódó linkek

Packages


Utolsó frissítés: 2024-03-27 14:54:36