Kihagyás

6. gyakorlat

A gyakorlat anyaga

A gyakorlaton újabb objektum-orientált konstrukciókat ismerünk meg. A példánk egy állatokkal foglalkozó alkalmazás lesz, melyben szárazföldi, vízi; ragadozó és növényevő állatok is lesznek.

Ehhez szükségünk lesz néhány osztályra, melyek írásáról, példányosításáról már korábban beszéltük, így ezeket az ismereteket ismertnek tekintjük. Akinek esetleg mégis problémája lenne az egyszerű osztályok létrehozásával, az a korábbi gyakorlati anyagokat (osztályok és az öröklődés) nézze át. Előadás anyagból pedig mindenképp érdemes megnézni az interface-ek és abstract osztályokról szóló előadást.

Mivel állatokkal fogunk foglalkozni, így szükségünk lesz egy Állat osztályra. Egy állatnak legyen neve, jóllakottsága, illetve ereje.

public class Allat {
    private String nev;
    private int jollakottsag;
    private int ero;

    public Allat(String nev) {
        this.nev = nev;
        this.jollakottsag = 100;
        this.ero = 0;
    }

    //.. getterek és szetterek

    public String hangotAd(){
        return "";
    }
}

Absztrakt

Fent láthatjuk, hogy a hangotAd() metódusnak nincs gyakorlati haszna, csupán azért hoztuk létre, hogy a gyerekosztályokban majd felüldefiniálhassuk őket, így például egy Állat tömbben meg tudjuk az elemekre hívni a hangotAd() metódust.

Absztrakt metódusok

Ezzel már korábban is találkoztunk, és igazából elkerülhetjük az ilyen helyzeteket, hiszen a hangotAd() metódusnak egyedül annyi a lényege, hogy a gyerekosztályokban felül lehessen definiálni. Azonban ezzel jelenleg két probléma is van: az Állat osztályban van működése, holott egy általánosságba véve vett állatról nem tudjuk, milyen hangot ad; illetve jelen pillanatban nem kötelező a gyerekosztályban felüldefiniálni, hiszen az örkölt metódusokat vagy felüldefiniáljuk, vagy nem, semmi sem kötelez rá, sőt, akár el is felejthetjük, ha nem vagyunk figyelmesek.

A probléma megoldására az abstract kulcsszó szolgál. Ezt odabiggyeszthetjük a metódusaink elé, cserébe nem kell őket implementálni az adott osztályban. Az abstract kulcsszóval azt mondjuk meg, hogy az adott osztályban egy (vagy több) adott metódust nem szeretnénk implementálni, csak azt szeretnénk megmondani, hogy a gyerekosztályban majd ezt az adott metódus(oka)t felül kell definiálnunk.

Absztrakt osztályok

Viszont, ha egy metódusunk elé odabiggyesztettük az abstract kulcsszót, akkor az osztálynak is kötelező absztraktnak lennie. Ennek a kulcsszava szintén az abstract, amelyet az osztály deklarációjában is ki kell tenni.

Erre azért van szükség, mert ha egy osztályunk absztrakt, akkor az nem példányosítható, és ha van egy olyan metódusunk, aminek nincs törzse, akkor azt nem nagyon kellene példányosítani. Éppen ezért, ha az osztályban van legalább egy absztrakt metódus, akkor az egész osztálynak absztraktnak kell lennie. Persze ez csak annyit jelent, hogy jelen formájában nem példányosítható, és majd a gyerekosztályban kell az absztrakt metódusokat megvalósítani (ellenkező esetben a gyerekosztálynak is absztraktnak kell lennie).

Természetesen ezen kívül az absztrakt osztályokban is lehetnek adattagok, sőt olyan metódusok is, melyek meg vannak valósítva. Írjuk át az Állat osztályt!

public abstract class Allat {
    private String nev;
    private int jollakottsag;
    private int ero;

    public Allat(String nev) {
        this.nev = nev;
        this.jollakottsag = 100;
        this.ero = 0;
    }

    // getterek, szetterek

    public abstract String hangotAd();
}

Ezáltal elértük, hogy az osztályunkban ne legyen egy olyan metódus, aminek nem tudunk normális működést biztosítani, helyette csak megmondjuk, hogy a gyerekosztályokban a hangotAd() metódust felül kell definiálni, ha olyan osztályt szeretnénk, amit tudunk példányosítani is. Ha absztrakt osztályt próbálunk meg példányosítani, fordítási hibát kapunk.

Megtehetjük azt is, hogy egy osztály öröklődik egy absztrakt osztályból, de nem feltétlen implementálja az összes örökölt absztrakt metódust, ilyenkor az az osztály is absztrakt lesz. Csináljunk két gyerekosztályt: SzárazföldiÁllat és VíziÁllat, amely szárazföldi és vízben élő állatok ősosztályai lesznek:

public abstract class SzarazfoldiAllat extends Allat{
    public SzarazfoldiAllat(String nev) {
        super(nev);
    }

    private int labakSzama;

    public int getLabakSzama() {
        return labakSzama;
    }

    public void setLabakSzama(int labakSzama) {
        this.labakSzama = labakSzama;
    }

}

public abstract class ViziAllat extends Allat {
    public ViziAllat(String nev) {
        super(nev);
    }

    @Override
    public String hangotAd() {
        return "nem hallható a víz alatt";
    }
}

Ezek után már létrehozhatunk konkrét állatokat, attól függően, hogy vízben vagy szárazföldön élnek. Csináljunk még két ősosztályt: Növényevő és Ragadozó, melyek növényevő és ragadozó állatok ősei lesznek.

public class Ragadozo {
    public void eszik(Allat kit){}
    public void pihenes(int mennyit){}
}

public class Novenyevo {
    public void eszik(){}
}

Ez így megint nem néz ki túlságosan jól. Ehelyett alkalmazhatjuk az előbb megismert trükköt, azaz absztrakttá tehetjük az osztály metódusait, és magát az osztályt is. Ez így rendben is lehet, azonban ezáltal beleütközünk a Java egyik korlátjába, a többszörös öröklődés hiányába, hiszen egy Kutya nem lehet egyszerre Állat és Ragadozó. Az absztrakttá tétel működőképes konstrukció ugyan, de ebben az esetben nem biztos, hogy a legjobb.

Interfész

Ugyanis, ha egy osztályban nincsenek sem adattagok, sem pedig megvalósított metódusok, akkor gyakorlatilag csak egy interfészről beszélünk, ami megmondja, hogy az őt kiterjesztő osztály milyen metódusokat valósítson meg, ha szeretnénk tudni példányosítani, viszont mi absztrakttá tettük. Javaban létezik az interfésznek is saját kulcsszava, amely az interface lesz. Ezáltal jelezzük a világ számára, hogy ebben az "osztályban" csak és kizárólag metódusdeklarációkat lehet találni, tehát biztosan nincs megvalósított metódus, sem pedig adattag. Erre szolgál tehát az interface kulcsszó.

public interface Novenyevo {
    public abstract void eszik();
}

public interface Ragadozo {
    public abstract void eszik(Allat kit);
    public abstract void pihenes(int mennyit);
}

Korábban szó volt arról, hogy többszörös öröklődés nincs Javaban, azonban bármennyi interfészt implementálhat egy osztály, ezáltal egy medve egyszerre lehet Állat és Ragadozó is. Sőt, azok az állatok, akik mindenevők, egyszerre implementálhatják a Ragadozó és Növényevő interfészt is.

Egy interfészben minden metódus implicit módon abstract, így ezt kiírni sem kell.

public interface Novenyevo {
    public void eszik();
}

public interface Ragadozo {
    public void eszik(Allat kit);
    public void pihenes(int mennyit);
}

Interfészeket nem örököltetünk (kivéve, ha egy interfész öröklődik egy másik interfészből), hanem implementálunk, azaz egy interfészben deklarált metódusokat megvalósítunk abban az osztályban, amely implementálja az interfészt.

public class Csirke implements Novenyevo {

    @Override
    public void eszik() {
        System.out.println(this.getNev() + " jóllakott magvakkal.");
        this.setJollakottsag(100);
    }
}

Ugyanakkor, ha egy interfészt nem szeretnénk teljesen implementálni, azt is megtehetjük, viszont így lesznek olyan metódusaink, amelyeket nem implementálunk. Így az ilyen osztályokat szintén el kell látnunk abstract kulcsszóval, hiszen absztrakt osztályok lesznek (vagyis olyan osztályok, amelyben vannak olyan metódusok, amelyek absztraktak).

A Csirke osztály SzárazföldiÁllat is és Növényevő is, így származhat a SzárazföldiÁllat osztályból, ugyanakkor implementálhatja a Növényevő interfészt is.

public class Csirke extends SzarazfoldiAllat implements Novenyevo {

    public Csirke(String nev) {
        super(nev);
        setLabakSzama(2);
    }

    @Override
    public void eszik() {
        System.out.println(this.getNev() + " jóllakott magvakkal.");
        this.setJollakottsag(100);
    }

    @Override
    public String hangotAd() {
        return "kott kott";
    }
}

instanceof, getClass()

instanceof

Hozzunk létre egy Csorda osztályt, amelyben állatok vannak. Minden állat objektumnak van egy konkrét típusa, hiszen az Állat osztály nem példányosítható, viszont a memóriában ott vannak a tényleges, létrehozott állatok. Készítsünk továbbá egy csordábaFogad() metódust, ami egy állatot vár paraméterül, és abban az esetben, ha van még hely a csordában, kerüljön be az adott állat, ellenkező esetben pedig ne.

public class Csorda {
    private Allat[] allatok;
    private int csordaLetszam;
    private int jelenlegiLetszam;

    public Csorda(int csordaLetszam) {
        this.csordaLetszam = csordaLetszam;
        this.jelenlegiLetszam = 0;
        allatok = new Allat[csordaLetszam];
    }

    public boolean csordabaFogad(Allat ki) {
        if (jelenlegiLetszam < csordaLetszam) {
            allatok[jelenlegiLetszam] = ki;
            jelenlegiLetszam++;
            return true;
        }
        return false;
    }

    @Override
    public String toString() {
        String str = "A csordaban vannak:";
        for (int i = 0; i < jelenlegiLetszam; i++) {
            str += allatok[i].toString();
        }
        return str;
    }

Előfordulhat, hogy szükségünk van egy ős típusú tömb (vagy bármi, később látni fogjuk) elemeinek konkrét típusára. Ez nem probléma, hiszen futásidőben tudni fogjuk minden egyes tömbelemről, hogy ő konkrétan milyen típusú, hiszen a memóriában ott van a tényleges objektum.

Ennek lekérdezésére az instanceof kulcsszó használható, melynek szintaxisa elsőre különös lehet: objektum instanceof Osztály.

Allat pipi = new Csirke("Pipi");
boolean igazHogyCsirke = pipi instanceof Csirke; // true

Ez egy logikai kifejezés, tehát használható if-ben például, és visszaadja, hogy egy adott objektum konkrétan adott osztályból származik-e, vagy valamelyik őséből, tehát ha van egy Állat típusú, allatok nevű tömb, akkor annak bármely elemére igazat adna vissza a allatok[i] instanceof Állat kifejezés.

getClass()

A getClass() egyike az Object nevű ősosztályból örökölt metódusoknak (itt megtalálhatjuk az Object osztály javadocját), ami egy beépített típussal, Class típussal tér vissza, és ezt a metódust a gyerekekben felül sem definiálhatjuk, mivel egy final metódusról van szó. Ha nem objektumunk van, hanem osztályunk, annak is lekérhetjük a típusát, az Osztály.class utasítással. Egy objektumnak tehát lekérhetjük a tényleges osztályát az objektum.getClass() metódussal. Ez a metódus csak és kizárólag a tényleges osztállyal történő összehasonlítást engedélyezi, tehát ha van egy Állat típusú, allatok nevű tömb, akkor annak bármely elemére hamisat adna vissza az allatok[i].getClass() == Állat.class kifejezés, hiszen az Állat a mi esetünkben egy absztrakt osztály, amit mivel nem lehet példányosítani, nem lehet egyetlen objektum tényleges típusa sem.

Italos példa

Italok

A fenti diagram forráskódja megtalálható a /n/pub/Programozas-I/nappali/gyakorlat/07 útvonalon vagy pedig ezen a linken.

Videók

Feladatok

Állatok kiegészítése

  1. Hozz létre három tetszőleges állatot az allatok csomagba, ezek között legyen szárazföldi, vízi, növényevő, ragadozó. Implementáld az összes szükséges metódust!
  2. Jelen állapotában a Csorda osztály bármilyen állatot tartalmazhat (például egy csordába tartozhat a cápa, az oroszlán és a csirke is). Ez a való életben nem biztos hogy megállja a helyét, így javítsd ki:
    1. Valósítsd meg, hogy egy csordában csak szárazföldi, vagy csak vízi állatok lehessenek. Mégpedig úgy, hogy az első állatot vizsgáljuk meg aki a csordába szeretne kerülni, és ha az szárazföldi állat, akkor a továbbiakban csak szárazföldi állatok kerülhessenek a csordába, ha vízi volt, csak vízi állatok.
    2. Valósítsd meg, hogy egy csordában csak növényevő, vagy csak ragadozó állatok lehessenek. Mégpedig úgy, hogy az első állatot vizsgáljuk meg aki a csordába szeretne kerülni, és ha az növényevő állat, akkor a továbbiakban csak növényevő állatok kerülhessenek a csordába, ha ragadozó volt, csak ragadozó állatok.
    3. Valósítsd meg az előző két feladatot ömlesztve, azaz egy csordába csak azonos helyen élő (szárazföldi, vízi), azonos életmódot folytató (növényevő, ragadozó) állatok kerülhessenek.
    4. Valósítsd meg a fenti feladatokat öröklődéssel, melyek legyenek speciális Csorda osztályok.

Függvények

Függvények

A fenti diagram forráskódja megtalálható a /n/pub/Programozas-I/nappali/gyakorlat/07 útvonalon vagy pedig ezen a linken.

A feladat elkészítéséhez segítséget nyújt a JDK-ban megtalálható Math osztály (Math.E, Math.PI konstansok, Math.pow() függvény).

  1. Hozzuk létre az absztrakt Fuggveny osztály felhasználásával egy SinusHiperbolicus konkrét osztályt.
    1. sh(x) = (e x - e -x)/2
  2. A FuggvenyOsszeg-hez hasonlóan hozzuk létre az OsszetettFuggveny konkrét osztályt. (f(g(x)-et számolja)
  3. Készítsd egy FgvMain osztályt, amelyben kipróbálod a függvényeket.

Síkidomok

  1. Írj egy Sikidom interfészt, ami tartalmazza a terület- és kerületszámítás metódusait, valamint a számításokhoz szükséges PI konstanst. Készíts egy Kor, egy Teglalap, és egy Negyzet osztályt, melyek mindegyike implementálja a Sikidom interfészt.
  2. Tegyél be egy tömbbe több ilyen síkidomot vegyesen, majd
    1. írd ki sorban a síkidomok jellemzőit,
    2. határozd meg a síkidomok átlag területét,
    3. számold meg, hány kör van a síkidomok között!

Megoldás letöltése

Fák

  1. Hozd létre a Fa osztályt!

    • Tároljuk el a magasságát, törzsének átmérőjét, azt, hogy kipusztult-e, illetve a leveleinek méretét egy tömbben.
    • Legyen egy paraméteres konstruktora, ami várja a magasságot, törzs átmérőjét és a maximális levelek számát. Alapértelmezetten a fa nincs kipusztulva, és nincs levele.
    • Készíts egy novekszik() metódust, ami megnöveli a fa magasságát 1-gyel.
    • Készíts egy idoteles() metódust, ami egy egész számot vár és nem ad vissza semmit. A metódus az osztályban ne legyen megvalósítva.
    • Definiáld felül az osztályban a toString() metódust.
  2. Hozd létre a Fenyofa osztályt, ami egy speciális fa!

    • Egy fenyőfáról tároljuk el, hogy a tűlevelei milyen típusúak.
    • Készíts egy megfelelő paraméteres konstruktort, ami a tűlevelek típusát is beállítja.
    • Definiáld felül a novekszik() metódust, ami a fenyőfa törzsének átmérőjét is megnöveli 1%-kal.
    • Valósítsd meg az idoteles() metódust, ami meghívja a novekszik() metódust annyi alkalommal, amennyi a paraméter értéke. Emellett adj a fához ugyanennyi levelet.
    • Definiáld felül az osztályban a toString() metódust.
  3. Hozd létre a Lombhullato interface-t, amelyben egyetlen metódus szerepel:

    • lombhullatas(), amely nem vár paramétert és egy egész számot ad vissza.
  4. Hozd létre a Tolgyfa osztályt, ami egy fa! A tölgyfákról tudjuk, hogy lombhullatók.

    • Egy tölgyfáról tároljuk el, hogy mekkorára nőhet maximum.
    • Készíts egy megfelelő paraméteres konstruktort, ami a maximális méretet is beállítja.
    • Definiáld felül a novekszik() metódust, ami az alábbi módon működik: ha a tölgyfa magassága még nem érte el a maximális méretet, akkor növeld meg a magasságát 5%-kal. A törzsének átmérőjét mindenképpen növeld 3%-kal. Ha a fa elérte a maximális méretét, akkor 20% eséllyel pusztuljon ki.
    • Valósítsd meg a lombhullatas() metódust, ami az összes levél méretét 0-ra állítja.
    • Valósítsd meg az idoteles() metódust, ami az alábbi módon működjön:
      • Ha a fa kipusztult, akkor ne történjen semmi
      • Ha a fán nincs levél, akkor hozz létre 50 új levelet (vagy ha ennél kevesebb lehet a fán, akkor annyit), majd hívd meg a novekszik() metódust annyi alkalommal, amennyi a paraméter értéke
      • Ha a fán van levél, akkor hullassa le őket
    • Definiáld felül az osztályban a toString() metódust.
  5. Hozz létre egy main függvényt, amiben létrehozol egy 5 méretű, véletlenszerűen fenyőfákkal vagy tölgyfákkal feltöltött tömböt, majd szimulálod az idő múlását.

A feladat megoldásáról készült videó itt elérhető.

Kapcsolódó linkek

Abstract Methods and Classes

Interfaces

Abstract class vs Interface

Abstract class vs Interface 2


Utolsó frissítés: 2024-04-11 07:54:27