7. 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);
}
public void kapirgal() {
System.out.println(super.getNev() + " kapirgal az udvarban.");
this.hangotAd();
}
@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éterként, é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
.
Ez egy logikai kifejezés, tehát használható if
-ben például, és visszaadja, hogy egy adott objektum egy adott osztály példánya-e. Természetesen az eddig tanult öröklődés szintén érvényes, tehát egy Csirke
esetén igaz lesz az is, hogy az adott csirke egy SzarazfoldiAllat
példánya, sőt, Allat
példánya. Például, 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.
Ezt nem csak osztályok esetén alkalmazhatjuk, hanem interfészek esetén is, a pipi instanceof Novenyevo
kifejezés értéke igaz lesz, hiszen a csirkék implementálják a Novenyevo
interfészt.
public class AllatMain {
public static void main(String[] args) {
Allat[] allatok = new Allat[3];
allatok[0] = new Csirke("Pipi1");
allatok[1] = new Csirke("Pipi2");
if (allatok[0] instanceof Csirke) {
System.out.println("Az adott állat Csirke típusú");
}
if (allatok[0] instanceof Novenyevo) {
System.out.println("Az adott állat növényevő");
}
if (allatok[0] instanceof SzarazfoldiAllat) {
System.out.println("Az adott állat szárazföldi");
}
}
}
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.
public class AllatMain {
public static void main(String[] args) {
Allat[] allatok = new Allat[3];
allatok[0] = new Csirke("Pipi1");
allatok[1] = new Csirke("Pipi2");
if (allatok[0].getClass() == Csirke.class) {
System.out.println("Az adott állat Csirke típusú");
}
// Sosem lehet igaz, mivel a Novenyevo egy interfész
if (allatok[0].getClass() == Novenyevo.class) {
System.out.println("Az adott állat növényevő");
}
// Sosem lehet igaz, mivel a SzarazfoldiAllat absztrakt
if (allatok[0].getClass() == SzarazfoldiAllat.class) {
System.out.println("Az adott állat szárazföldi");
}
}
}
Italos példa¶
A fenti diagram forráskódja megtalálható a /n/pub/Programozas-I/nappali/gyakorlat/07
útvonalon vagy pedig ezen a linken.
Videók¶
- Absztrakt osztályok és absztrakt metódusok (EAIJ): https://youtu.be/L25NuoP-0bM
- Interfészek (EAIJ): https://youtu.be/C-gmO9-Yoz4
Feladatok¶
Állatok kiegészítése¶
- 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! - 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:- 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.
- 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.
- 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.
- Valósítsd meg a fenti feladatokat öröklődéssel, melyek legyenek speciális
Csorda
osztályok.
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).
- Hozzuk létre az absztrakt
Fuggveny
osztály felhasználásával egySinusHiperbolicus
konkrét osztályt.- sh(x) = (e x - e -x)/2
- A
FuggvenyOsszeg
-hez hasonlóan hozzuk létre azOsszetettFuggveny
konkrét osztályt. (f(g(x)-et számolja) - Készítsd egy
FgvMain
osztályt, amelyben kipróbálod a függvényeket.
Síkidomok¶
- Í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 egyKor
, egyTeglalap
, és egyNegyzet
osztályt, melyek mindegyike implementálja aSikidom
interfészt. - Tegyél be egy tömbbe több ilyen síkidomot vegyesen, majd
- írd ki sorban a síkidomok jellemzőit,
- határozd meg a síkidomok átlag területét,
- számold meg, hány kör van a síkidomok között!
Fák¶
-
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.
-
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.
-
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.
-
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.
-
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ő.