progamozási nyelvek 3. gyakorlatkami/oktatasi_anyagok... · a this pszeudováltozó az...
TRANSCRIPT
3. gyakorlat
Objektumorientált programozás
Amiről ma szó lesz
Objektumok
Csomagok
Öröklődés
Debugger használata
OOP vs. egyéb programozási módszerek
Az objektumorientált programozás abban különbözik leginkább
más programozási módszerektől, hogy a programrészeket,
hasonló feladatokat, de főleg hasonló feladatokat leíró adatokat és
az azokat felhasználó metódusokat csokorba foglaljuk.
Ezáltal az objektum-orientált programozás jobban igyekszik
közelíteni a világban lejátszódó valódi folyamatokhoz.
Az objektum-orientált programozás tehát jóval hatékonyabb
megoldást képes nyújtani a legtöbb problémára.
Az osztályok újrafelhasználásának támogatásával nagymértékben
tudja csökkenteni a szoftverek fejlesztéséhez szükséges időt.
OOP vs. procedurális programozás
Procedurális programozás esetén a feladatokat változókra,
adatszerkezetekre és eljárásokra tagoljuk.
Objektum-orientált programozás esetén a feladatok elvégzését
objektumokra tagoljuk, amelyek adatokat és eljárásokat
enkapszulálnak.
Tehát a procedurális programozás eljárásokat használ, amelyek
adatokon/adatszerkezeteken dolgoznak, az objektum-
orientált programozás pedig objektumokat használ, amelyek a
saját adataikon dolgoznak.
v.push(e) vs. push(v,e)
Az OOP alapfogalmai
Osztály
Objektum, példány, egyed
Osztálydefiníció
példányváltozók
osztályváltozók
metódusok (lehetnek osztály- és példányszintűek)
konstruktor
destruktor
inicializáló blokk (lehet osztály vagy példány inicializáló)
Láthatósági szintek
Egységbezárás
Adatelrejtés
Objektum, Osztály
Objektum:
Program entitás: állapot és funkcionalitás
Egyedi, zárt (jól definiált interfésszel rendelkezik, amelyen
keresztül az állapota változtatható, műveletei elérhetők)
Kapcsolatok objektumok között → osztályok
Osztály:
Hasonló objektumok gyűjteménye:
Strukturális vagy funkcionális hasonlóság
Objektumok reprezentációjának megadása
Osztály: egy (felhasználói) típus
Példányosítás: osztály → objektum
Objektum létrehozása
Egy egyed egy osztály megvalósítása, más szóval példányosítása.
Javaban ezt így érjük el:
Object a = new Object();
A példányosítás a memóriaterület lefoglalásával és az arra
mutató referencia beállításával kezdődik.
Ilyenkor lefut az osztály konstruktora, ahol meghatározhatjuk a
kezdőértékeket, inicializálhatjuk az objektumot.
Az a referencia a new operátorral létrehozott objektumra mutat.
Ha elmarad a példányosítás, nincs objektum!
Object objektum;
objektum.valamittesz(); //hiba, kivéve, ha az adott
metódus static
Osztály létrehozása
Egy osztálynak lehetnek
Példányváltozói, példánymetódusai
Osztályváltozói, osztálymetódusai
Előbbiekből minden egyes példányosított egyedhez
kerül egy-egy, míg az utóbbiakból az összes azonos
osztályból példányosított egyedhez csak egy.
Java esetén egy osztály definíciója és deklarációja,
azaz az eljárások feje és megvalósítása nem
szétválasztható, mint például C++-ban.
Példa - Autó
Valósítsunk meg egy Autó osztályt Javaban!
package jarmuvek; // az osztályt tartalmazó csomag
public class Auto { //az osztály kezdete
// az osztály megvalósítása
} //az osztály vége
Autó leírása (UML) Auto
- benzinar: int
- tipus: String
- kmOra: double
- benzin: double
- ferohely: int
- fogyasztas: double
+ Auto(tipus: string, fogyasztas: double, ferohely: int)
+ Auto(tipus: string, fogyasztas: double, ferohely: int, kmOra: double)
+ koltseg(km: double) : double
+ halad(km: double)
+ tankol(liter: double)
+ toString() : String
(UML: Általános célú modellező nyelv)
Példa – Autó - változók
Ahogy C++-ban, úgy a Javában is felsorolhatunk változókat,
de a Java megengedi, hogy inicializáljuk is azokat,
konstruktoron kívül.
Változó deklarációja:
[módosítók] típus változó [inicializáció] public class Auto {
static private int benzinar=250; //statikus változó
private String tipus;
private double fogyasztas;
private double kmOra=0; //példa kezdőértékre
private double benzin=0;
private int ferohely;
//további metódusok
}
Konstruktor
Programkód, ami a példányosításkor automatikusan
végrehajtódik
Ennek neve – ahogy C++-ban is – megegyezik az
osztály nevével, nincs visszatérési értéke.
Hasonlít a metódusokra, de nem pont ugyanolyan (nem
tag, mert pl. nem öröklődik)
Minden osztálynak van konstruktora. Ha mi nem
írtunk, a fordító automatikusan létrehozza és a
példányosításkor meghívódik.
Konstruktorból lehet több is, túlterhelésükre ugyanaz
vonatkozik, mint a függvények túlterhelésére.
Példa – Autó - Konstruktor public Auto(String tipus, double fogyasztas, int ferohely){
this.tipus=tipus;
this.fogyasztas=fogyasztas;
this.ferohely=ferohely;
}
public Auto(String tipus, double fogyasztas, int ferohely, double kmOra){
this.tipus=tipus;
this.fogyasztas=fogyasztas;
this.kmOra=kmOra;
this.ferohely=ferohely;
}
A this pszeudováltozó
Az osztálydefiníción belüli példánymetódusokban a this névvel
hivatkozhatunk az adott objektumra.
Ez persze az osztály szintű metódusokban nem használható.
Ha több konstruktort is definiálunk, az egyes konstruktorok elején,
minden más utasítás előtt, van lehetőség valamelyik másik
konstruktor végrehajtására a this kulcsszóval:
public Auto(String tipus, double fogyasztas, int ferohely,
double kmOra){
this(tipus,fogyasztas,ferohely);
this.kmOra=kmOra;
}
Metódusok
Alprogramok, melyek objektumokhoz vannak kapcsolva.
Érdekesség, hogy a metódus neve megegyezhet egy
változóéval is.
Az osztály metódusait, ahogy láttuk a konstruktornál, szintén túl lehet
terhelni.
Túlterhelés: ugyanolyan azonosítójú(nevű), de különböző
szignatúrájú függvények létrehozása.
Szignatúra: azonosító, paraméterlista
Függvény feje a Javaban: módosítók, a visszatérési érték típusa,
azonosító név, paraméterlista, kivételek (melyek dobásra kerülhetnek)
Metódus definíciója: fej(specifikáció)+törzs
[módosítók] típus Név([paraméterek]) [kivételek] {törzs}
Metódusok az Autó osztálybana – Autó -
Metódusok public void tankol(double liter){
benzin+=liter;
}
private double benzinigeny(double km){
return ((double)km/100)*fogyasztas;
}
public double koltseg(double km){
return benzinigeny(km)*benzinar;
}
Példa – Autó - Metódusok public double halad(double km) {
if(benzinigeny(km)<=benzin){
benzin-=benzinigeny(km);
kmOra+=km;
System.out.println("Megtettunk " + km + "km utat!");
return km;
}
else {
System.out.println("Nincs eleg benzin " +km+ "km
uthoz!");
return 0;
}
}
Módosítók összessége I.
Osztály:
public: Az osztály bárki számára látható
final: Végleges, nem lehet belőle leszármazni. Az öröklődésnél
lesz róla szó bővebben.
abstract: Az öröklődésnél lesz róla szó, csak leszármazni lehet
belőle, példányosítani nem (a tervezés eszköze)
(Üres): A csomagon (package) belül bárki számára látható
Amik kombinálhatóak:
public + final, public+abstract
Amik nem kombinálhatóak:
final+abstract
Módosítók összessége II.
Változó, illetve objektum:
public: Az osztály példányait használó bármely kód számára közvetlenül hozzáférhető.
private: Csak azon osztály objektumai számára elérhetők, melyben meghatározták
őket.
protected: A definiáló osztály és leszármazottainak objektumai számára és a
csomagon belül látható
final: Végleges, azaz konstans érték (nem ugyanaz mint a C++ const! Hiszen itt a
referencia az az érték, ami nem változtatható, de ettől még a hivatkozott objektum
állapota változtatható)
static: Osztályváltozó, egy osztályhoz csak egy tartozik (vagyis az osztály minden
példányához is ugyanaz a változó tartozik)
Amik kombinálhatóak:
Láthatóság + final + static
Amik nem kombinálhatóak:
Láthatósági szintek, egyszerre csak egy láthatósági szint használható (nyílvánvalóan)
Módosítók összessége III.
Metódus esetén:
public: Az objektumot használó bármely kód számára közvetlenül hozzáférhető.
private: Azon osztály objektumai számára elérhető (meghívható), melyben
deklarálták azt
protected: A definiáló osztály és leszármazottainak objektumai számára és a
csomagon belül látható
static: Osztálymetódus, példányosítás nélkül használható, csak statikus változókat
használhat
final: Végleges, a leszármazott nem írhatja felül. (Öröklődésnél bővebben)
abstract: Absztrakt osztálynál használható, kötelező felülírnia, azaz
megvalósítania a leszármazott osztálynak.
Amik kombinálhatóak:
Láthatósági szint + static + abstract
Amik nem kombinálhatóak:
abstract + final
A static kulcsszó
Javaban minden objektum – maga az osztály is egy
objektum.
A static kulcsszó ennek a felfogásnak a nyelvi
megvalósítása, mivel ezzel az osztályhoz, mint
objektumhoz rendelünk egy változót.
Ugyanez elmondható a static metódusokról is.
Mivel a static metódus az osztályhoz, mint
objektumhoz tartozik, így nem használhat fel (olvashat,
írhat) változókat az osztály példányaiból, azaz egy static
metódusnak az osztály minden egyes példányán
meghívva pontosan ugyanazt az eredményt kell
biztosítania, függetlenül az egyes példányokban tárolt
információktól.
Példa a static használatára
A benzin minden autó számára ugyanannyiba kerül, típustól
függetlenül
Ennek megfelelően természetesen 10 liter benzin
megvásárlása is ugyanannyi pénz minden autótulajdonos
számára, függetlenül attól, hogy ő például ezzel hány
kilométert tud megtenni
static private int benzinar=250;
static public int mennyibeKerul(int
ennyiLiterBenzin){
return benzinar*ennyiLiterBenzin;
}
Getter & Setter
Osztályok leírásánál legtöbbször elrejtjük a belső
változókat a külvilág elől, mert pl:
nem akarjuk, hogy megváltoztassák (integritás)
módosítottan akarjuk megjeleníteni vagy átadni
a változtatásra reagálni akarunk (esemény)
vagy egyszerűen nem akarjuk, hogy lássák
Ezért használtuk a private módosítószót a változóknál.
Ilyenkor az elérést a getter és setter függvények biztosítják.
Getter & Setter - Példa
public double getBenzin(){
// itt hajthatjuk végre a konverziót,
// ellenőrzést stb
return benzin;
}
public double getkmOra() {
return kmOra;
}
Getter & Setter - gyorsan
Bal oldalt az osztály nevére jobbklikk
→ Source
→ Generate Getters and Setters...
Majd a listából kiválasztjuk, hogy mely változókhoz
szerenénk getter vagy setter függvényt generálni.
Ugyanígy konstruktorokat is lehet generálni.
toString()
A Javában minden egyednek van egy függvénye, ami szöveges információt hordoz a példányról, ez a public String toString() függvény.
Ez a metódus lényegében tehát megadja az adott objektum String reprezentációját. Így ezután például lehetséges a következő: System.out.println(objektum);
Ha nem hozzuk létre, az objektumunk örökli az ősosztálytól (erről bővebben később).
public String toString(){
return tipus + ", benzin: " + getBenzin() + ", km: " + getkmOra();
}
Az üres referencia, objektum
élettartama
Ha egy változó értéke null, akkor nem mutat objektumra.
A null referencia minden osztályhoz használható.
Példányváltozók automatikus inicializációjához ezt használja a Java.
Nincs olyan utasítás Javaban, amivel egy objektumot explicit módon meg lehet szüntetni.
Ha már nem hivatkozunk egy objektumra többet, akkor a referenciát null-ra lehet állítani. De ez magát az objektumot nem szünteti meg.
Szemétgyűjtés
Inicializáló blokkok – érdekesség I.
Inicializáló blokk : Utasításblokk a tagok (példány- és
osztályszintű változók és metódusok) és konstruktorok
között, az osztálydefiníción belül.
public class A{
int [] array=new int[10];
{
for(int i=0; i<10; i++){
array[i]=(int)Math.random();
}
}
}
Inicializáló blokkok – érdekesség II.
Az előbb példány szintű inicializáló blokkot láttunk.
Ugyanez megtörténhet osztály szinten is:
public class A{
static int i=10;
static int ifact;
static
{
ifact=1;
for(int j=2; j<=10; j++){
ifact*=j;
}
}
}
Inicializáló blokkok – Miért is?
Osztályszintű inicializátor: az osztály inicializációjakor fut le, az
osztályszintű konstruktorokat helyettesíti (hiszen olyanok nincsenek).
Példányinicializátor: példányosításkor fut le, a konstruktorokat
egészíti ki; pl. oda írhatjuk azt, amit minden konstruktorban végre kell
hajtani (névtelen osztályoknál is jó, mert ott nem lehet konstruktor).
Több inicializáló blokk is lehet egy osztályban.
Végrehajtás (mind osztály-, mind példányszinten): a változók
inicializálásával összefésülve, definiálási sorrendben; nem
hivatkozhatnak később definiált változókra.
Nem lehet benne return utasítás.
A példányinicializátor az osztály konstruktora előtt fut le, de az
ősosztályok konstruktora után.
Nem szoktuk a "tagok" közé sorolni
Csomagok
Csomagokkal tudjuk Javaban kifejezni a modularitást.
A típusainkat/osztályainkat csomagokba soroljuk, ezzel
összetartozásukat fejezzük ki. Az egy csomagba kerülő
osztályok logikailag jobban összetartoznak, mint a
különböző csomagokba kerülő osztályok.
A csomagok hierarchiába szervezhetők.
Minősített hivatkozás pontokkal:
java.util
Egy típus teljes neve tartalmazza az őt befoglaló csomag
nevét is:
java.util.Vector
Csomagok megadása
A típusdefiníció elé egyszerűen beírjuk, hogy melyik
csomagba tartozik:
Példa:
A package kulcsszó segítségével. package raktár;
public class Csavar{
...
}
Csomag hierarchiákat is létrehozhatunk:
Példa: package raktar.adatok → egy adatok alcsomag a raktár csomagon
belül.
(1 csomag → egy könyvtár a fájlrendszerben.)
Láthatósági szintek csomagokban ism.
A láthatósági módosítószavak (az osztályokra
vonatkozóan):
public – mindenki látja
Ha nem írunk módosítót az osztály elé, akkor a
láthatósági szintje „félnyilvános”, azaz
csomagszintű (package-private).
Az azonos csomagban levő osztályok hozzáférnek
egymás félnyilvános tagjaihoz. (A protected lényegében
ennek a kiterjesztése.)
Hivatkozás más csomagokra
Ha egy forrásfájlban használni akarunk egy típust egy másik
csomagból:
Írjuk ki a teljes nevét: java.util.Vector v=new java.util.Vector();
Importáljuk import utasítással: import java.util.Vector;
import java.util.*;
Vector v=new Vector();
(import java.util.* : nem csak egy adott osztályt tölt be, hanem az adott csomagban
található összes osztályt.)
Fordítási egység
Az, amit oda lehet adni a fordítónak, hogy lefordítsa.
Java-ban egy fordítási egység tartalma:
package utasítás (opcionális)
import utasítások (opcionális)
típusdefiníció (egy vagy több)
A főprogram működése
Egy Java program egy osztály "végrehajtásának" felel
meg: ha van statikus main nevű metódusa.
A java-nak megadott nevű osztályt inicializálja a virtuális gép
(pl. az osztályváltozóit inicializálja), és megpróbálja elkezdeni
a main-jét. Ha nincs main: futási idejű hiba.
Végetérés: ha a main végetér, vagy meghívjuk a
System.exit(int)-et. (Kilépési állapot, nulla szokott a
normális állapot lenni.)
Pár szó a JVM-mel kapcsolatban
Kódolás után vannak forrásfájljaink, melyeket lefordítva .class
bájtkódokat kapunk. Amikor elindítjuk az alkalmazásunkat a
ClassLoader avagy az osztálybetöltő egy adott osztályra való
első hivatkozáskor betölti az osztálynak megfelelő .class fájlt a
JVM-re. Ezután következik a bájtkód ellenőrző, majd ha minden
rendben van, akkor az osztályt leíró elemek bekerülnek a
java.lang.Class osztályba. Ekkor fut le az osztályszintű
inicializáló blokk.
A java.lang.Class tehát egy olyan osztály, ami a futó java alkalmazásban
használt osztályok reprezentációit tartalmazza. A java.lang.Class
osztálynak van egy olyan metódusa is (forName(String name))
mellyel a megfelelő nevű osztály bájtkódját betölthetjük a JVM-re rá
való hivatkozás vagy példányosítás nélkül. Ez történik pl.
különböző driverek betöltése esetén.
Összefoglalás I.
Amit eddig megtanultunk:
Mi is az az OOP?
Osztály, objektum
Példányváltozók, példánymetódusok
Osztályváltozók, osztálymetódusok (static)
Konstruktorok, getterek és setterek, toString()
Láthatósági szintek, és hogy miért használjuk ezeket
Inicializáló blokkok
java.lang.Class
Csomagok
Hogyan működik egy Java program
Öröklődés - Motiváció
Új osztályt szeretnék egy meglévő alapján megírni.
Egy osztály adattagjait, metódusait felhasználva szeretnénk egy új
funkcionalitást létrehozni.
Szeretnénk a meglévő osztályainkat bővíteni új adattagok és
metódusok felvételével.
Szeretnénk már meglévő metódusokat, adattagokat felülírni az új
funkciónak megfelelően.
A Java megoldás
class Child extends Parent { /* Child törzse */ }
Extend = kiterjeszteni
Az így módon megadott ún. leszármazott osztály
(Child) örökli a szülője (Parent) tulajdonságait, azaz a
belső állapotát leíró adatszerkezetét és a viselkedést
megvalósító metódusait.
Természetesen az osztály törzsében ezeket tovább
lehet bővíteni, a viselkedését módosítani, ezt sugallja
az extends kifejezés a definíciónál.
Öröklődés, mint osztályok közötti kapcsolat
Az előző dia már sugallja, de fontos külön kiemelni, hogy a Javaban az
öröklődés az, ami C++ esetén a publikus öröklődés. Javaban
tehát az extends kulcsszó az is-a reláció kifejezése:
Class Child extends Parent {/* osztály törzse*/}
Child c;
→ c típusa Parent, azon belül pedig Child
Ez tehát azt is jelenti, hogy a leszármazott osztály létrehozásával
egy altípust képzünk.
(Javaban a tartalmazási relációt, a has-a relációt nem lehet öröklődés
útján kifejezni.)
Tehát összefoglalva
Az öröklődés révén kód-újrafelhasználás jön létre, ami:
A kód redundanciáját csökkenti
nem csak a programozást könnyíti meg, hanem az
olvashatóságot és a karbantarthatóságot is növeli
Nem csupán csak kódmegosztást jelent, hanem altípus
képzést is
→ tervezési szintű fogalom
Fontos fogalmak
Osztályhierarchia
Konstruktorok és az öröklődés
Felüldefiniálás vs. túlterhelés
Elfedés
Absztrakt osztályok
Véglegesítés
Polimorfizmus
Statikus és dinamikus típus
Dinamikus kötés
Osztályhierarchia
Az öröklődési reláció irányított gráf reprezentációját
osztályhierarchiának is nevezik.
Javaban van egy "univerzális ősosztály", az Object, minden osztály
ennek a leszármazottja.
Javaban csak egyszeres öröklődés van, tehát az osztályhierechia Java
esetén egy gyökeres fa.
Ha nem adunk meg extends-et, akkor is implicit extends Object
értendő.
Object: predefinit, a java.lang-ban van definiálva, olyan metódusai
vannak, amelyekkel minden objektumnak rendelkeznie kell.
Minden, ami nem primitív típusú (int, char, stb.), az Object
leszármazottja.
Mik a lehetőségeink? – I.
A leszármazott osztályban közvetlenül használhatjuk a
szülőosztály public és protected metódusait, tagváltozóit
(egy leszármazott osztály örökli a szülő osztály tagjait, így
a szülő osztály private tagjaiból is van példánya, de
ezekhez nem férhet közvetlenül hozzá).
Deklarálhatunk olyan nevű adattagokat, amelyekkel a
szülő osztály már rendelkezik, elfedve a szülőosztálybeli
elemeket.
Írhatunk példánymetódust, mely szignatúrája megegyezik
egy szülőosztálybeli példánymetódussal, felüldefiniálva azt.
Túlterhelhetünk egy szülőosztályban meglévő metódust.
Mik a lehetőségeink? – II.
Írhatunk osztálymetódust, mely szignatúrája megegyezik
egy szülőosztálybeli osztálymetódussal, elfedve azt.
Írhatunk teljesen új metódusokat.
Deklarálhatunk teljesen új adattagokat.
A super kulcsszóval hivatkozhatunk szülőosztálybeli
elemekre (pl. konstruktorra, hiszen az nem öröklődik,
vagy olyan függvényre, melyet a leszármazottban
felüldefiniáltunk vagy elfedtünk.)
Példa: Taxi osztály Autó osztályból
Hozzunk létre egy Taxi osztályt a már meglévő Auto
osztályból.
Vegyünk fel újabb adattagokat a Taxi osztályba:
pénztárca: A sofőr pénzének regisztrálása
Kilométerdíj: a fuvarozás díja kilométerenként
public class Taxi extends Auto{
protected double penztarca;
protected double kmdij;
// függvények
}
Példa: Taxi konstruktorok
A leszármazott nem örökli a szülő konstruktorát. De van
lehetőségünk a leszármazott konstruktorának !első! sorában
meghívni a szülő valamelyik konstruktorát a super(...)
kulcsszóval.
Ha ezt nem tesszük meg, vagy ha nem is definiálunk konstruktort,
akkor is végrehajtódik a szülő paraméter nélküli
konstruktora (ha van ilyen, ellenkező esetben fordítási hiba!!!).
public Taxi(String tipus, double fogyasztas, int ferohely, double
kmdij) {
super(tipus, fogyasztas, ferohely);
this.kmdij = kmdij;
this.penztarca = 0;
}
Konstruktorok: lefutási sorrend
A lefutási sorrend példányosítás esetén a következő:
Ősosztály static inicializáló blokkja
Osztály static inicializáló blokkja
Ősosztály inicializáló blokkja lefut
Ősosztály konstruktora
Osztály inicializáló blokkja lefut
Osztály konstruktora
Amit ebből mindenki jegyezzen meg: egy leszármazott osztály
konstruktora mindig a szülő osztály konstruktora után fut le. (Még
akkor is, ha az nincs explicit meghívva!!!)
Nézzük meg ehhez a lefutasok csomag tartalmát!
És többek között vegyük észre, hogy bár nem hívtuk meg explicit
módon a leszármazottban a szülőosztály konstruktorát, mégis
lefutott az ősosztály paraméter nélküli konstruktora!
Példa Taxi: felüldefiniálás I.
Taxi esetén máshogy számoljuk a költségeket (ehhez hozzájön a
kilométer díj), továbbá mivel van már pénztárcánk, így a benne levő
pénzt kezelni kell tankolás során, végül az osztály String
reprezentációja is változik, tehát új toString() metódus is kell.
Felüldefiniálás
Írhatunk a leszármazott osztályban olyan példánymetódust, mely
szignatúrája megegyezik egy szülőosztálybeli
példánymetódussal. Ezt nevezzük felüldefiniálásnak.
Példa Taxi: felüldefiniálás II.
A felüldefiniálás szabályai:
Szignatúra megegyezik (különben túlterhelés lenne).
Visszatérési érték típusa megegyezik, vagy a felüldefiniált metódus
vissztérési értékének altípusa (kovariáns vissztérési érték).
Láthatóság nem szűkíthető.
Specifikált kiváltható kivételek nem bővíthetőek.
Ezután a felüldefiniált metódus a super kulcsszó segítségével
továbbra is elérhető marad.
Túlterhelés emlékeztető
Ugyanazt az azonosítót használjuk különböző
paraméterezésű (tehát szignatúrájú) függvények esetében.
Egy névhez több funkciót rendelünk hozzá.
Fontos szabály: egyező azonosítójú függvények esetében a
paraméterezésnek, tehát lényegében a függvény
szignatúrájának különbözőnek kell lennie. Ez az egyértelműség
miatt fontos.
A visszatérési érték típusának meg kell egyeznie! Hiszen
visszatérési érték alapján nem lehet egy függvényt túlterhelni.
A függvényhívásnál a paraméterezés dönti el, hogy melyik függvény-
implementáció fusson le.
Leszármazott osztályban lehetőségünk van túlterhelni egy
szülőosztálybeli metódust.
Példa Taxi: Felüldefiniálás III. public double koltseg(double km){
return super.koltseg(km)+kmdij*km;
}
public void tankol(int liter){
benzin+=liter;
penztarca-=liter*benzinar;
}
public String toString(){
return super.toString()+", a fuvarozo penze:
"+penztarca;
}
Példa Taxi: új metódusok
Írhatunk a leszármazott osztályban új metódusokat:
Fuvarozást megvalósító függvény
Olyan költség függvény, ami egy főre megmondja az utazás
költségét, ha egyszerre több utast fuvaroz a taxi.
(A halad() függvényt ne írjuk felül, hiszen két fuvar között a Taxi
rendes autóként közlekedik.) public double fuvar(double km){
if(halad(km)==km){
penztarca+=koltseg(km);
return km;
}
else return 0;
}
public double koltseg_per_fo(double km, int fo) throws Exception{
if(fo>ferohely+1){
throw new Exception("Tobb utas, mint ferohely");
}
return koltseg(km)/fo;
}
Van egy új osztályunk!
Amit még lehetett volna: elfedések
Szülőosztályban meglévő adattagokkal megegyező nevű adattagok
deklarálása a leszármazott osztályban (elfedve azokat).
Szülőosztályban lévő osztálymetódussal megegyező szignatúrájú
osztálymetódus írása a leszármazott osztályban (itt ugyanazok a
kitételek érvényesek, mint a metódus felüldefiniálás esetén).
Ezekre ez esetben nem volt szükség.
Nézzük meg a Taxi osztály kódját!
Elfedés Példa class Alma{
protected static int kgAr=200;
public static int getKgAr(){
return kgAr;
}
}
class GranatAlma extends Alma{
protected static int kgAr=500;
public static int getKgAr(){
return kgAr;
}
}
Final kulcsszó
Osztály esetén: olyan osztályt jelöl, ami végleges, tehát nem lehet már
belőle örökölni.
Ilyen lesz majd a Busz osztályunk.
Metódus esetén: olyan metódust jelöl ami végleges, tehát öröklődés
során már nem definiálható felül.
Az Auto osztály halad és benzinigény függvénye legyen ilyen, hiszen ez
a függvény minden autó (legyen az taxi, busz stb.) esetén helytálló
(kiszámítunk egy benzin igenyt, és ha ez megvan, megtesszük az utat).
Adattag esetén: konstans.
Példa: Busz osztály
Hozzuk létre a Busz osztályt a Taxi osztályból!
A buszon egy vonaljeggyel lehet utazni, szintén van kilométerdíj, amit a sofőr
kap (sofőr fizetése). → új osztályváltozó: jegyár
A költség ugyanúgy kerül kiszámításra, ahogy a taxinál, ellenben a fuvar
esetében az üzemeltető cég pénztárcáját tekintsük, azaz a jegyárból vonjuk
ki a buszvezető díját is!
Az osztály legyen végleges (final).
Az alábbi metódusokat kell megvalósítani:
public Busz(String tipus, double fogyasztas, int ferohely, double kmdij)
public double fuvar(double km) → felüldefiniálás
public double fuvar(double km, int fo) → túlterhelés
public double Haszon(double km, int fo) → új metódus
public double koltseg_per_fo(double km, int fo) → felüldefiniálás
Példa: Busz osztály megvalósítás I.
public final class Busz extends Taxi {
private static int jegyar = 270;
public Busz(String tipus, double fogyasztas,
int ferohely, double kmdij){
super(tipus,fogyasztas, ferohely, kmdij);
}
//egyéb metódusok
}
Példa: Busz osztály megvalósítás II. //felüldefiniálás
public double fuvar(double km){
if(halad(km)==km){
penztarca+=jegyar - kmdij*km;
return km;
}
else return 0;
}
//túlterhelt metódus
public double fuvar(double km, int fo){
if(halad(km)==km){
penztarca+=fo*jegyar - km*kmdij;
return km;
}
else return 0;
}
Példa: Busz osztály megvalósítás III. //többi felüldefiniált metódus public double koltseg_per_fo(double km, int fo){ return jegyar; } public String toString(){ return "A busz típusa: "+ tipus+", a vállalat penze: "+penztarca; } //új metódusok public double Haszon(double km, int fo){ return fo*jegyar - koltseg(km); } public static double getJegyar(){ return jegyar; }
Van egy újabb osztályunk
Kész van a Busz osztály. Nézzük meg a kódját a
jarmuvek csomagban, és a main függvényt!
Eddig specializáltunk. Mi van ha generalizálni
akarunk?
Absztrakt osztályok I.
A tervezés eszközei (generalizálást, azaz általánosítást
tesznek lehetővé).
Az altípus reláció megadása tervezés szempontjából sokszor
megkívánja.
Absztrakt osztály: hiányosan definiált osztály. → Vannak benne
olyan funkciók, amelyekről még nem tudjuk, hogy hogyan fognak
működni, amelyek megvalósítását a leszármazottakra bízzuk.
abstract módosító szó (osztály, és a nem implementált metódus előtt).
Azt az osztályt, amiben van legalább egy abstract módosítóval jelölt függvény,
szintén abstract módosítóval kell ellátni.
Nem lehet belőle példányosítani, de lehet belőle örökölni.
Absztrakt osztályok II.
Megjegyzés: Absztrakt osztály leszármazottja lehet maga is
absztrakt, ha nem implementálja az ősosztályban specifikált
függvényeket, vagy ha önmaga is tartalmaz nem implementált
függvényeket. Absztrakt osztályt egyébként úgy is
létrehozhatunk, hogy a szülő nem absztrakt.
Tegyük fel, hogy szeretnénk az autó mellé egy újabb járművet
definiálni, a biciklit.
Definiáljunk egy absztrakt Jarmu osztályt, majd ebből örököltessük le
az Auto és a Bicikli osztályainkat.
Most tehát a specializálással ellentétes folyamatról van szó, az
általánosításról!
Példa: absztrakt osztályok tervezése
Bicikli
- tipus: String
- kmOra: double
- ferohely: int
+ Bicikli(tipus: string, ferohely: int)
+ Bicikli(tipus: string, ferohely: int, kmOra: double)
+ halad(km: double)
+ toString() : String
Auto
- benzinar: int
- tipus: String
- kmOra: double
- benzin: double
- ferohely: int
- fogyasztas: double
+ Auto(tipus: string, fogyasztas: double, ferohely: int)
+ Auto(tipus: string, fogyasztas: double, ferohely: int, kmOra: double)
+ koltseg(km: double) : double
+ halad(km: double)
+ tankol(liter: double)
+ toString() : String
Nézzük meg, mely közös attribútumokkal tudjuk jellemezni a két különböző járművet: tipus, kilométer óra, férőhely, és a haladást megvalósító függvény
Példa: absztrakt jármű osztály
public abstract class Jarmu {
protected String tipus;
protected double kmOra;
protected int ferohely;
public abstract double halad(double km);
public String toString(){
return tipus+ ", " + kmOra + " km, férőhely: "+ferohely;
}
}
Ezután ügyeljünk arra, hogy az Auto osztályban már ne legyen a tipus, a
kmOra és a ferohely újra deklarálva.
A bicikli osztály
A Bicikli osztályból további értelmes járművet már nem tudunk
leszármaztatni, ezért ez legyen final. Tehát legyen ez egy olyan osztály,
ami végleges, így nem lehet belőle már örökölni.
public final class Bicikli extends Jarmu {
public Bicikli(String tipus, int ferohely, double kmOra){ this.tipus=tipus; this.ferohely=ferohely; this.kmOra=kmOra; } public Bicikli(String tipus){ this(tipus,1,0); } public double halad(double km) { this.kmOra+=km; System.out.println("Megtettünk "+km+" km utat."); return km; }
}
Absztraktosztálybólleszármaztatvaaleszármazottkötelesimplementálniazabsztrakt
osztálybanspecifikáltfüggvényeket!(különbenmagaisabsztraktkellmaradjon)
A polimorfizmus
A polimorfizmus, vagy más néven többalakúság
az a jelenség, hogy egy rögzített típusú változó
több típusú objektumra hivatkozhat.
Ez az ún. altípusos polimorfizmus (van másfajta
is, pl. Ada-ban). Auto a=new Auto("Subaru", 10, 4, 0);
Auto b=new Taxi("Honda", 5, 5, 300);
Egy A típus altípusa egy B típusnak, ha minden olyan
helyzetben, ahol B típusú objektum használható, A
típusú objektum is használható.
Statikus típus, dinamikus típus
Statikus típus: a deklarációnál megadott típus.
Dinamikus típus: a változó által mutatott objektum tényleges típusa.
A dinamikus típus vagy maga a statikus típus, vagy egy leszármazottja (különben fordítási hibát kapunk).
A statikus típus állandó, a dinamikus típus változhat a program futása során.
Statikus típus, dinamikus típus példák
Helyes:
Auto a=new Auto("Subaru",10,4,0);
Auto b=new Taxi("Honda",5,5,300);
Jarmu j=new Bicikli("Magellan");
Attól még, hogy az absztrakt osztály nem példányosítható,
attól még mint típust használhatjuk!
Csak maga a példányosítás tiltott, nem írhatunk olyat, hogy:
Jarmu j=new Jarmu() → hiba
Helytelen (hibát is kapunk rá):
Auto a=new Bicikli("Magellan");
Taxi b=new Auto("Honda",5,5,300);
Statikus típus, dinamikus típus, szerepük
A statikus típus dönti el azt, hogy milyen műveletek végezhetők a
referenciaváltozón keresztül.
A dinamikus típus dönti el azt, hogy a végrehajtandó metódusnak melyik törzse
fusson le.
Ld. később: dinamikus kötés
Auto a=new Auto("Subaru",10,4,0);
Auto b=new Taxi("Honda",5,5,300);
System.out.println(a.toString());
System.out.println(b.toString()); //A Taxi toString()-je fog lefutni
b.fuvar(100); // hibát kapnánk, hiszen az Autónak nincs fuvar azonosítójú
függvénye.
Hogyan tudnánk mégis ezek után elérni valahogy a dinamikus típusnak megfelelő
műveleteket?
→ típuskonverzió, később.
Dinamikus kötés
Dinamikus kötésnek nevezzük azt a jelenséget mely során egy objektumon a
statikus típus által megengedett műveletet meghívva, a műveletnek mindig a
dinamikus típusnak megfelelő implementációja fog lefutni.
Ezt már láthattuk az előző dián.
A dinamikus kötés jelensége természetesen csak akkor jön létre, ha a leszármazott
osztályban van felüldefiniált (és nem túlterhelt!) metódus, vagy elfedett
osztálymetódus.
Javaban minden metódus olyan, mint C++-ban a virtual kulcsszóval
ellátottak. Itt a dinamikus kötés automatikusan létrejön! Auto a=new Auto("Subaru",10,4,0);
Auto b=new Taxi("Honda",5,5,300);
System.out.println(a.toString());
System.out.println(b.toString());
Az eredmény:
Subaru, benzin: 0.0, km: 0.0, férőhely: 4
Honda, benzin: 0.0, km: 0.0, férőhely: 5, a fuvarozo penze: 0.0
Implicit és explicit típuskonverziók - primitív típusok
Automatikus típuskonverzió (de nem mindig injektív):
byte -> short -> int -> long -> float -> double
Mindig bővítő! (hiszen így biztonságos)
double d;
int i=6;
d=i;
d=d+i;
Explicit típuskonverzió: cast
A programozó explicit megmondja, hogy milyen típusú legyen a
változó: (ez így viszont már nem biztonságos, adatot veszthetünk)
double d;
int i=6;
d=i;
i=(int)d+4;
Implicit típuskonverziók - objektumok
Altípusok esetén:
Szűkebb halmazba tartozó objektumokat
bővebb halmazba tartozó objektumokként
kezelünk.
Tehát egy leszármazott osztályba tartozó
objektumot az ősosztályba tartozóként
kezelünk.
Ekkor automatikus típuskonverzió jön létre. Auto b=new Taxi(''Honda'', 4,4,300);
Taxi c=new Busz(''Ikarus'',6,30,200);
Explicit típuskonverziók – objektumok I.
Auto b=new Taxi(''Honda'', 4,4,300);
Taxi c=(Taxi)b;
c.fuvar(100);
A fenti példában nem kapnánk hibát, hiszen a b dinamikus
típusa Taxi, így átkonvertálhatjuk a referenciát gond nélkül.
Figyelem: a konverzió nem magát az objektumot
érinti!
De ha elnézünk valamit, és a b változónk mégsem Taxi
típusú objektumra mutat, akkor
java.lang.ClassCastException-t kaphatunk.
Ha ezt ki akarjuk védeni, használjuk az instanceof
operátort.
Explicit típuskonverziók – objektumok II.
Az instanceof operátor egy megadott objektumot hasonlít
egy megadott típushoz:
if(b instanceof Taxi){
Taxi c=(Taxi)b;
c.fuvar(100);
}
Fontos: objektumok esetén cast-olásra akkor van szükségünk, ha egy
olyan műveletet szeretnénk elérni az objektumon, ami a statikus
típusban nincs definiálva. Egyéb esetekben a dinamikus kötés miatt
úgyis a felüldefiniált metódus futna le.
A konverzió csak a referencia típusát érinti, magát az
objektumot nem. Nem lehet objektum referenciák típusát
akármivé átalakítani, csak az öröklődési láncban felfelé vagy
lefelé.
Feladat
Nézzük meg ehhez a dinkotes csomagban a Main osztály
main függvényét!
Debugger használata
Debugger: hiba kereső eszköz
Miért hasznos?
Lehetőséget nyújt arra, hogy a programunkat lépésről lépésre
futassuk, és közben láthassuk az egyes változóink pillanatnyi
értékeit.
Segítségével általában hatékonyan tudunk hibát keresni a
programunkban.
Amikor hibás a program, általános módszer, hogy egyes helyeken
kiíratjuk a változóink értékét, vagy kiíratunk üzeneteket, hogy
lássuk, hol keletkezett a hiba. Debugger alkalmazásakor ezt nem
kell megtennünk, hiszen külön ablakon keresztül láthatjuk
változóink aktuális értékeit, továbbá lépésenként
végigkövethetjük, hogy éppen mely sorok végrehajtása történik.
Debugger használata - lehetőségek
A debugger lényege, hogy előre elhelyeztt ún. breakpointok elérésekor a program
végrehajtása szünetelni kezd. Ezután a programozó eldöntheti, hogy hogyan
történjen a breakpoint által jelölt utasítás végrehajtása. Erre 3 lehetőség kínálkozik:
step into: Az utasítás végrehajtása sorról sorra. Ha ezen a ponton egy
függvényhívás van, akkor belépünk a függvénytörzsbe és lépésről lépésre
végrehajtjuk az abban foglaltakat.
step over: A jelölt sorban egy függvényhívás van, de nem lépünk be a
függvénytörzsbe, hanem továbblépünk.
step return: Ha egy függvényben vagyunk, a return utasításra ugrik.
run to line: Általunk megadott sor végrehajtására ugrik tovább.
Egyéb lehetőségek:
watch pointok elhelyezése: Egy adott változóra egy watch pointot állíthatunk. Ez esetben
a debugger minden egyes alkalommal megáll, amikor a változót elérik, vagy
megváltoztatják.
Debugger használata – Példa -I.
Vegyük az autó projektet:
Helyezzünk el tetszőleges sorok mentén breakpointokat:
Az adott sor mentén a kód melletti bal oldali keskeny sávra dupla
kattintás.
Debugger használata – Példa - II.
Ha ez megvan helyezzünk el az osztály adattagjaira
watch pointokat (változó szintjében a keskeny sávra
dupla kattintás).
Debugger használata – Példa - III.
Majd indítsuk el a debuggert. (kisbogár ikon)
(Akár külön alkalmazásként is elindíthatjuk.)
Debugger használata – Példa - IV.
Mikor breakpoint-hoz érünk, próbáljuk ki a
step into
step over
step return
run to line (jelöljük ki kurzorral a sort, ahova ugrani akarunk, majd Ctrl+R)
lehetőségeket.
Kiemelve láthatjuk, hogy éppen melyik sor végrehajtása történik.
Összefoglalás II.
Mi az az öröklődés, mihez biztosít nekünk eszközöket?
Öröklődéssel kapcsolatos fogalmak és jelentésük:
Elfedés
Felüldefiniálás vs. Túlterhelés
Véglegesítés
Specializáció vs. generalizáció, absztrakt osztályok
Konstruktorok és az öröklődés
Debugger
1. Feladat I. rész
Írjunk egy iskolát modellező alkalmazást! Először is az iskolába járnak emberek, akiknek van nevük, születési évük és
nemük.
Az iskolában tanítanak tantárgyakat, amelyeknek van nevük, leírásuk és hozzá vannak rendelve évfolyamok, amikor ezeket oktatni kell.
Az iskolába járó emberek között vannak diákok, akik egy osztályba tartoznak, akiknek van egy tanulmányi átlaguk, és akiknél el kell tárolni a szülő/nevelő elérhetőségeit.
Az iskolában tanítanak tanárok, akikhez bizonyos tárgyak vannak hozzárendelve. A tanárok között vannak osztályfőnökök, akiknek van osztályuk. A tanároknak van heti óraszámuk, órabérük ami alapján a havi fizetésük kiszámolható.
A tanárok között van egy igazgató, akinek a fizetése nem csak az órabér és a heti óraszám alapján képződik (van valamifajta plusz fizetés).
Végül van az iskola, amiben vannak tanulók, tanárok, igazgató, aminek van egy címe, neve és ahol kiszámolható, hogy havonta összesen mennyi fizetést kell kiosztani.
1. Feladat II. rész
Tanár: +tantárgyak +órabér +hetióraszám +osztálya,havan +havifizetéstmegadófüggvény +paraméterbenmegadottórákutánjárófizetés
Tanuló(végleges): +osztály +tanulmányiátlag +szülőelérhetőségei
Ember: -név -nem -születésiév
Tantárgy: -név -leírás -évfolyamok,melyekbentanítják
Iskola: -iskolábanlevőemberek(tanár,diák,igazgató)(tömb) -haviszintűkiadástmegadófüggvény(fizetések) -cím -igazgatótmegadófüggvény -tanárikartmegadófüggvény -diákokatmegadófüggvény -iskoláhozkötődőösszesszemélytmegadófüggvény
Igazgató(végleges): +jutalék +havifizetéstmegadófüggvény
1. Feladat III. rész Végül írj egy alkalmazást, melyen keresztül bemutatod az iskolát (iskola
adatai, tanári kar, diákok).
Egy lehetséges megoldás: Az iskola toString() függvényével add meg a címet, a nevet,
a tanulók, tanárok számát és az igazgató nevét. Ezt írasd ki. Majd listázz ki minden
személyt, aki az iskolához kapcsolódik (tanárok, igazgató, diákok). Ne csak a név
jelenjen ekkor meg, hanem a megfelelő osztály toString() függvényének eredménye.
Majd írasd ki a tanárok havi fizetését.
Adj hozzá még diákot, tanárt az iskola nyilvántartásához.
Kérdezd le, hogy az iskola egy hónapban mennyi fizetést oszt ki.
Tehát nagy vonalakban mutasd be az általad létrehozott osztályok funkcióit.
(Az egy hónapot tekintsük most az egyszerűség kedvéért 4 hétnek.)
Az előbbi dián listázott metódusokon kívül természetesen, ha indokolt,
legyenek implementálva getter és setter metódusok is, továbbá
legyenek implementálva olyan metódusok, amelyekkel az objektum
állapotait tudjuk megváltoztatni, mint pl. új tanár felvétele az Iskola
osztály esetén stb.
2. Feladat I. rész
Tervezzünk meg és írjunk meg egy alkalmazott osztályt!
Útmutatás:
Gondold át, hogy egy alkalmazottat egy tetszőleges vállalatnál milyen adatokkal
lehet reprezentálni.
Ha ez megvan, gondold át azt, hogy mik lehetnek azok az eljárások/műveletek,
amiket ehhez az alkalmazott osztályhoz hozzá lehetne kapcsolni. Gondold át,
hogy a meglévő adattagokon milyen értelmes műveleteket/lekérdezéseket
lehetne végrehajtani.
Gondold át, hogy a fentiek összességéből melyek azok, amelyek minden
alkalmazott esetén ugyanazok/ugyanazt az eredményt/hatást érik el, és melyek
azok, amelyek minden alkalmazott esetén különbözőek.
Majd gondold át, hogy mi az, ami hozzátartozna az osztály publikus interfészéhez,
és mi az, amit érdemes elrejteni.
Ha mindez megvan, a megfelelő nyelvi elemeket felhasználva írd meg az
alkalmazott osztály!
2. Feladat II. rész
A megírt Alkalmazott osztályból leszármaztatva valósítsunk
meg egy Főnök osztályt!
Útmutatás:
Melyek azok a változók, amelyeket célszerű lenne elfedni?
Milyen új változókat lehetne bevezetni?
Melyek azok a példánymetódusok, melyeket felül kéne definiálni?
Melyek azok az osztálymetódusok, melyeket el kéne fedni?
Milyen új metódusokat kellene megírni?
Majd ezután írj egy olyan main függvényt, melyben kipróbálod a
polimorfizmus és a dinamikus kötés lehetőségeit!
Házi feladat – I.
Készítsd el a Sikidom absztrakt osztályt. Tulajdonságai: kerulet, terulet.
Műveletei: kerulet, terulet, toString – ezek legyenek publikusak;
Absztrakt műveletei: keruletSzamit, teruletSzamit – legyenek leszármazottak
számára kezelhetőek.
Valósítsd meg hogy a kerulet és a terulet függvények adják visszatérési értékül a
kerulet-et illetve a terulet-et, amelyek a keruletSzamit illetve a teruletSzamit
értékeivel bírnak. A toString-be irassuk ki a teruletet és a kerületet.
A Sikidom leszármazottja a Kor osztály, aminek sugar a mezője. Hozzuk létre az
osztály egyparaméteres konstruktorát, definiáljuk a keruletSzamit és a
teruletSzamit(a sugár alapján), toString metódusokat. A toString írja le az osztály
állapotát: miről van szó, sugár, területe, kerülete.
A Sikidom másik leszármazottja a Trapez osztály, 4 paraméteres konstruktora van.
Ezért 4 mezővel bír, azaz a,b,c,d oldalak vannak(leszármozottai lesznek, figyeljünk a
legjobb láthatóságra). Írjuk meg erre is a keruletSzamit és a teruletSzamit
metódusokat az oldalak alapján. A toString írja le ismét az osztályt-t(mezők, terület,
kerület).
Házi feladat – II.
Hozzuk létre a Paralelogramma osztályt , ami egy speciális trapéz. Mezői bővüljenek
a szog-el. 3 paraméteres konstruktora van: a,b oldal és a szög. A szög segítségével
határzozzuk meg a terültet. Alkossuk meg ismételten a keruletSzamit, teruletSzamit,
toString függvényeket().
Készítsünk Teglalap osztályt, ami a Paralelogramma gyermeke. 2 paraméteres
konstruktora van. Minimalizáljuk a megírandó függvényeket, tudjuk leírni a
fentiekhez hasonlóan az osztályt.
Valósítsuk meg a Negyzet osztályt, aminek egyparaméteres konstruktora van.
Cselekedjünk az előbbi szerint.
Végül alkossuk meg a Futtathato osztályt, amelyben main-ként szolgál. Legyen
benne egy 6-7 elemű tömb, amibe tudunk tenni kört, trapézt, paralelogrammát,
téglalpot, négyzetet egyaránt. Ezek mindegyike szerepeljen benne egyszer, a
maradék helyet töltsük fel olyan elemekkel, amik dinamikus kötést használnak.
Írassuk ki a tömb elemeit foreach-el, értelmezzük a látottakat.