Kihagyás

3. gyakorlat

Az UML modellezés alapjait nézzük meg, bővebben az UML osztálydiagrammal foglalkozunk a gyakorlaton. Ez a jegyzet csak vázlatszerűen tárgyalja a témát, az előadáson (és az előadásanyagban) ennél jóval részletesebb tananyag áll rendelkezésre. Az előadáson kívül érdemes lehet megnézni Tarczali Tünde: UML diagramok a gyakorlatban című jegyzetét, illetve további kapcsolódó anyagok találhatóak a Kapcsolódó linkek menüpontban.

UML alapok

  • UML - Unified Modeling Language (Egységesített Modellező Nyelv)
  • Hivatalos oldal, amelyen elérhető a nyelv teljes specifikációja és dokumentációja
  • Wikipédia leírás az UML-ről magyar, illetve angol nyelven
  • Előnyei
    • Nyílt szabvány
    • A szoftverfejlesztés ciklusainak mindegyikét támogatja
    • Hatalmas tapasztalati tudásra épít
    • Sok eszköz és sok cég támogatja, fejleszti a szabványt
  • Használhatjuk szoftverrendszer elemeinek
    • vizualizálására
    • specifikálására
    • létrehozására
    • dokumentálására
  • Modell: egy teljes rendszer leírása
  • Diagram: a rendszer egy részének, valamely szemszögből való vizuális leírása
  • Két nagy részre bontható alapvetően
    • Statikus modellek, például:
      • Osztálydiagram, leírás
      • Objektumdiagram
      • Csomagdiagram
      • Komponensdiagram
    • Dinamikus modellek, például:
      • Állapotdiagram
      • Szekvenciadiagram
      • Használati eset (use case) diagram

Az objektum

Az objektum egy olyan entitás ábrázolása (ez lehet valós (pl.: lámpa, autó, macska) vagy pedig elvont (pl.: matematikai függvény, zombi, unikornis), amely rendelkezik állapottal, viselkedéssel, identitással.

Állapot

Az objektum állapota egy a lehetséges létezési lehetőségei közül (a tulajdonságok aktuális értéke által meghatározva). Ez időben változó lehet, pl.: egy lámpa ami ki van kapcsolva, egy későbbi időpontban be lehet kapcsolva.

Gyakorlati példa

A házi kedvenceket nyilvántartó programban tárolunk macskákat is, köztük Foltost. Foltos egy objektum. Foltos éppen 8 kg, éppen 2 éves, barátságos és nincs rajta egy bolha sem. Ezen tulajdonságok által van meghatározva Foltos aktuális állapota, ami változhat. Foltos egy év múlva már 3 éves lesz, lehet rajta több, vagy kevesebb bolha, és természetesen a súlya is változhat, akár két nagyon közeli időpillanat között is.

Egy programban az állapot az objektum attribútumai (adattagjai) által lesz meghatározva. Ez a programban gyakorlatilag azt fogja jelenteni, hogy az adattagok egy adott típusbeli konkrét értékkel rendelkeznek (pl.: egy lámpa objektum bekapcsolt tulajdonsága egy boolean típusú attribútum, amely True vagy False értékeket vehet fel, és ez jelöli azt, hogy az adott lámpa be van-e kapcsolva; egy autó objektum egy tulajdonsága lehet, hogy éppen mekkora sebességgel halad, amelyet reprezentálhatunk egy int típusú tulajdonsággal, és adott autó konkrétan az int értéktartományán belül egy adott értékkel halad, amely természetesen változhat, és jellemzően változik is.)

Gyakorlati példa

Foltos attribútumai lehetnek például: súly (lebegőpontos), barátságos (logika), bolhák száma (egész).

Viselkedés

Az objektum viselkedése annak leírása, hogy az objektum hogy reagál más objektumok kéréseire. Az objektum kérésre csinál valamit, ami akár az állapotát is megváltoztathatja (sok esetben ez meg is történik).

Gyakorlati példa

Foltos lehetséges viselkedése lehett például a dorombolás, karmolása, evés, kóborlás.

Egy programban a viselkedést operációkkal írjuk le. Ez gyakorlatilag tagfüggvényeket (operációkat, metódusokat) fog jelenteni a programunkban, például: egy lámpa objektum bekapcsol(), kikapcsol() nevű operációi, melyek megváltoztatják az állapotát; egy autó objektum gyorsit(), lassit() nevű metódusai, melyek a pillanatnyi sebességet változtatják meg.

Identitás

Minden objektum egyedi, még akkor is, ha éppen ugyanabban az állapotban vannak, és ugyanolyan viselkedést képesek megvalósítani. Ez gyakorlatilag azt jelenti, hogy hiába van két, azonos gyártó által gyártott, ugyanolyan típusú, színű autó objektum, melyek pillanatnyilag éppen ugyanakkora sebességgel haladnak, ez a két autó objektum akkor is különböző.

Az osztály

Az osztály gyakorlatilag egy adott objektumcsoport "formai" leírása. Egészen konkrétan objektumok csoportjának leírása, amelyeknek közösek az attribútumaik, operációik, más objektumokkal való kapcsolataik és szemantikus viselkedésük. Az osztály egy adott objektum típusát jelenti. Az osztályokat csoportosíthatjuk valamilyen logika szerint, akár hierarchikusan, csomagokba.

Megjegyzés

Az osztály egy adott objektum típusa, azaz az objektum egy osztály példánya. Vegyük példának Foltost. Foltos egy Macska típusú objektum, és Foltos pontosan egy osztály példánya. Foltos tulajdonságait és viselkedését a Macska osztály írja le. Foltos a Macska osztály egy példánya. Természetesen más Macska típusú objektumot is létrehozhatunk majd. Vagy például vegyünk egy konkrét autó objektumot, amelyet a Toyota gyártott, Corolla típusú, fekete színű és éppen 46 km/h-val halad. Ez pontosan egy osztály példánya lehet, jelen esetben az Autó osztály példánya. Az Autó osztály leírja, hogy hogyan ábrázolható a gyártó (szöveg), a típus (szöveg), szín (szöveges), és éppen mennyivel halad (egész szám).

A példánkban a Macska osztályt fogjuk elkészíteni, ami a programban lévő összea Macska típusú objektum leírása. Megadja mindazt, amit a programban a macskák csinálhatnak.

A Macska osztályban fogjuk megírni a macskák tulajdonságait, viselkedését. A macskákról az alábbi tulajdonságokat (attribútumokat) tartjuk számon:

  • Név (szöveg)
  • Súly (lebegőpontos)
  • Barátságos-e (logikai)
  • Hány bolha van rajta (egész)
  • Hányszor simogatták meg (egész)

Minden macska tud nyávogni, kóborolni (bizonyos távolságot), a macskákat lehet bolhátlanítani, és tudjuk róluk, hogy kóbor macskák-e. A macskákat meg tudjuk simogatni (több-kevesebb sikerrel). Ez a leírás így elég egyszerű, nem túl egyértelmű, és alaposan meg kell nézni ahhoz, hogy megérthessük a követelményeket. Itt jönnek képbe az osztálydiagramok.

Osztálydiagram (class diagram)

Az UML osztálydiagram az osztályoknak és azok kapcsolatainak összefoglaló diagramja. Ez egy nagyon magas szintű felülnézeti képet ad a rendszerünkről, amiben láthatjuk, hogy milyen osztályaink vannak, és ezek hogy állnak kapcsolatban egymással (enélkül minden fájlt át kellene bogarásznunk ahhoz, hogy egy magas szintű képet láthassunk a rendszerünkről, ami nem hangzik annyira vészesnek, de például már 10-20 osztály esetén is rengeteg idő lehet (ami akár több ezer kódsort is jelenthet); természetesen valós rendszerekben sokkal több osztály is előfordulhat). Az osztályok csomagokba kerülhetnek itt is. Ez a diagramtípus az egyik legalapvetőbb és legtöbbet használt diagramtípus, amellyel modellezhetjük az osztályainkat, és az azok között lévő kapcsolatokat. Az osztálydiagramokról bővebben az előadáson esett szó, illetve olvashatunk ezen és ezen a linken. Egy UML osztálydiagram elemei az osztályok, és az osztályok közötti kapcsolatok. Osztályok között pedig 3 fajta kapcsolat lehet: asszociáció, aggregáció, valamint kompozíció.

Azonban mielőtt ezeket részletesen megismernénk, induljunk onnan, ahonnan általában a tervezés is indul: készítsünk osztályokat. De hogyan?

Osztály

Az osztály jele a téglalap. A téglalapnak 3 része van: a felső részbe írjuk az osztály nevét, a középső részbe kerülnek az osztályhoz tartozó attribútumok, általában típusmegjelöléssel, az alsó részébe pedig az osztályhoz tartozó operációk kerülnek.

Üres osztály

Vegyük a korábbi macskás példát. A Macska osztály fogja megadni mindazt, amit a majdani macskáink tudni fognak a programunkban. Ehhez egy téglalapot rajzolunk, majd pedig megadjuk az osztály nevét, az attribútumokat, majd pedig az operációkat. Az attribútumokat típusmegjelöléssel szoktuk megadni, ennek jele a kettőspont a tulajdonság neve után, majd pedig következik a típus, például: bolhakSzama : int.

Vegyük fel a Macska osztály tulajdonságait!

Egy félkész Macska osztály

A tulajdonságok mellett természetesen a macskákra "érvényes" műveleteket, operációkat is fel kell vennünk. Ezek a forráskódban függvényként fognak jelentkezni, így az UML osztálydiagramon valami függvényhez hasonló dologként kell megjelölünk az egyes operációkat, amit az osztály majdani példányai tudni fognak: meg kell adnunk az operáció visszatérési típusát (ha tudjuk), nevét, illetve a paraméterek típusát (és akár nevét is). Különböző eszközök ezeket különböző módon teszik lehetővé, talán a legáltalánosabb formája ennek: operációNeve(paraméterTípus1, paraméterTípus2) : visszatérésiTípus, operációNeve(név1 : paraméterTípus1, név2 : paraméterTípus2) : visszatérésiTípus.

Adjuk hozzá a félkész diagramhoz a hiányzó részeket!

Macska osztály

Tegyük fel, hogy első körben csak ennyi lenne a programunk. Ez tök jó, megrajzoltuk, de hogy lesz ebből kód?

Megvalósítása Javaban

Ahogy korábban már tárgyaltuk, a konkrét objektum(csoportok) formai leírása az osztály. Osztályokat kell létrehoznunk a kódban ahhoz, hogy majd létre tudjunk hozni a memóriában objektumokat. Osztályokkal olyan objektumokat formalizálunk, melyek azonos tulajdonságokkal és operációkkal rendelkeznek, mint például a macskák, vagy az emberek.

A programban sok különböző macska létezhet, de a macskák közös tulajdonságokkal rendelkeznek, például mindegyiknek van név, barátságos tulajdonsága.

Készítsük el a macskákat reprezentáló Macska osztályt. Az osztály létrehozásának kulcsszava a class Javaban. Általában osztályokat külön fájlokba készítünk, melynek neve megegyezik a létrehozni kívánt osztállyal, kiegészítve a .java kiterjesztéssel. Tehát készítsük el a Macska.java fájlt:

public class Macska {

}

Az UML-ben lévő attribútumok, tulajdonságok az osztályon belül vannak létrehozva. A típus alapvetően a diagramban feltüntetett típus legyen lehetőség szerint, de ha éppen olyan nyelven dolgozunk, ahol egy adott adattípus nem elérhető, akkor természetesen választhatunk egy ahhoz hasonló típust (például, ha egy osztálydiagramon char* típust látunk, akkor például Javaban simán használhatunk Stringet). A Macska osztály esetén így néz ki az osztályunk az attribútumokkal kiegészítve, amit itt már legtöbbször adattagokként emlegetünk.

public class Macska {
    String nev;
    double suly;
    boolean baratsagos;
    int bolhakSzama;
    int simogatasokSzama;
}

Elkészült az osztályunk, már csak a viselkedést kellene valahogy a forráskódban is reprezentálni. Erre a metódusok szolgálnak, melyeket az osztályba írunk bele, és ezek határozzák meg mindazt, amit az adott osztályból létrejövő objektumok képesek lesznek "csinálni".

Minden macska tud nyávogni, így szükségünk lesz egy nyavog metódusra (amit az UML osztálydiagramon is láthatunk), ennek nincs paramétere, és nem tér vissza semmilyen értékkel.

public class Macska {
    String nev;
    double suly;
    boolean baratsagos;
    int bolhakSzama;
    int simogatasokSzama;

    void nyavog() {
        String meow = "Me";
        for (int i = 0; i < suly; i++) {
            if (i % 2 == 0) {
                meow += "o";
            } else {
                meow += "O";
            }
        }
        meow += "w";
        System.out.println(nev + " nyávog: " + meow);
    }
}

Ez a metódus így az adott macska súlyától függ hosszabban vagy rövidebben nyávogni. Természetesen ezt egyszerűbben is megvalósíthatjuk, egy adott hosszúságú nyávogás kiírásával.

A többi metódust is készítsük el.

public class Macska {
    String nev;
    double suly;
    boolean baratsagos;
    int bolhakSzama;
    int simogatasokSzama;

    void nyavog() {
        String meow = "Me";
        for (int i = 0; i < suly; i++) {
            if (i % 2 == 0) {
                meow += "o";
            } else {
                meow += "O";
            }
        }
        meow += "w";
        System.out.println(nev + " nyávog: " + meow);
    }

    void koborol(double tav) {
        suly--;
        bolhakSzama += (tav / 20) + 1;
    }

    void bolhatlanit() {
        this.bolhakSzama = 0;
    }

}

Most már van néhány viselkedést leíró operációnk (metódusok), vannak attribútumaink (adattagok), így kész a Macska osztály (egyelőre).

Az így elkészült osztályból objektumokat készítünk, ezt példányosításnak hívjuk. Minden egyes objektum pontosan egy osztály példánya, azonban egy osztályból számtalan objektumpéldány is létrehozható. Hozzuk létre Foltost a main függvényben a new operátor segítségével.

    public static void main(String[] args) {
        Macska foltos = new Macska("Foltos", true);
        foltos.nyavog();
        System.out.println("Foltos súlya: " + foltos.suly);
    }

Fordítsuk, majd futtassuk le a programot. Ennek kimenete: null nyávog: Mew, és a súlya sem épp a legmegfelelőbb. Nem éppen lett Foltos, de még a nyávogása sem az igazi. Ennek az az oka, hogy az osztályban lévő adattagok nincsenek rendesen inicializálva, a típusokhoz tartozó alapértelmezett értéket vették fel (előző gyakorlat). Ahhoz, hogy az inicializálást megfelelő módon megtehessük, konstruktort kell létrehoznunk.

Konstruktor

A konstruktor segítségével hozhatunk létre példányokat (objektumokat) az osztályokból, ezzel lehet beállítani az osztályban deklarált adattagokat (változókat) a konkrét, adott objektumra jellemző értékekre (tehát egy konkrét Macska példány aktuális állapotát). Konstruktor alapból is létezik, feltéve, hogy mi nem csináltunk egyet sem (az előbb mi nem írtunk ilyet), de készíthetünk paraméteres konstruktorokat is, melyek segítségével gyorsan és könnyen tudjuk az adott objektumot inicializálni. Fontos, hogy ha mi készítünk bármilyen konstruktort, akkor a fordító nem készít default, paraméter nélküli konstruktort (ahogy azt tette korábban).

  • Nevének meg kell egyeznie az osztály nevével, minden esetben.
  • Visszatérési értéke/típusa nincs (nem is lehet, még csak a void-ot sem szabad odaírni).
  • Új objektum létrejöttekor fut le.

Alapvetően készíthetünk paraméter nélküli (alapértelmezett, default), vagy paraméteres konstruktorokat is. A konstruktorok felelnek az objektum megfelelő kezdeti inicializálásáról.

    Macska() {
        nev = "Nincs";
    }

A default konstruktort csak akkor kell a kódban ténylegesen is megírni, ha van valamilyen szerepe, teljesen üres alapértelmezett konstruktort felesleges megírni. A fenti példában például beállítjuk a nevet "Nincs"-re. Készíthetünk paraméteres konstruktorokat is.

    Macska(String nev) {
        nev = nev;
    }

A kiegészített paraméteres konstruktorban azonban van egy kis gond. Mivel a paraméterben lévő nev változó elfedi az osztályban létrehozott adattagot (szintén nev), így a nev = nev; értékadásnak semmi értelme nincs, hiszen a paraméter nev változót önmagával tesszük egyenlővé. Természetesen ezt is meg tudjuk oldani, itt jön képbe a this kulcsszó.

A this kulcsszó segítségével az objektum saját magára tud hivatkozni vele, ezzel megoldhatjuk a fenti problémát. Ennek persze nem csak a konstruktorban van haszna, minden olyan esetben jól jön, amikor egy metódusban szereplő paraméter neve megegyezik az osztályban deklarált adattag nevével, így meg tudjuk őket különböztetni (az egyik az objektum saját tulajdonsága, a másik pedig a paraméter).

    Macska(String nev, boolean baratsagos) {
        this.nev = nev;
        this.suly = 6;
        this.baratsagos = baratsagos;
        this.bolhakSzama = 0;
        this.simogatasokSzama = 0
    }

    Macska(String nev) {
        this.nev = nev;
        this.suly = 10;
        this.baratsagos = true;
        this.bolhakSzama = 0;
        this.simogatasokSzama = 0
    }

Ahogy látszik is, természetesen több paraméteres konstruktort is készíthetünk (természetesen a paraméterlistában különbözniük kell a különböző konstruktoroknak). A konstruktor függvények létrehozásában segít az IDE, ha megkérjük rá.

  • IntelliJ IDEA esetén: a munkaterületen jobb klikk és Generate, vagy pedig egyszerűen nyomjuk le az Alt + Insert billentyűkombinációt, majd pedig válasszuk a Constructor opciót.

Kényelmi okokból is, valamint azért, hogy esetlegesen ne legyen a kódban sok kódmásolat a konstruktorok között (amivel egyébként az esetleges hibákat is másolnánk), a konstruktorok első utasítása lehet egy másik konstruktor meghívása is, a this segítségével. A fenti példa átalakítva:

    public Macska(String nev, double suly, boolean baratsagos) {
        this.nev = nev;
        this.suly = suly;
        this.baratsagos = baratsagos;
        this.bolhakSzama = 0;
        this.simogatasokSzama = 0;
    }

    public Macska(String nev, boolean baratsagos) {
        this(nev, 6, baratsagos);
    }

    public Macska(String nev) {
        this(nev, 10, true);
    }

Az így elkészült osztállyal most már létrehozhatjuk Foltost.

    public static void main(String[] args) {
        Macska foltos = new Macska("Foltos", true);
        foltos.nyavog();
        System.out.println("Foltos súlya: " + foltos.suly);

        foltos.suly = -100;
    }

Most már Foltos lesz a cica, és szépen nyávog is.

Viszont van egy új probléma, amiről eddig nem esett szó, de a kódban már látható. Foltos súlya szabadon változtatható, az objektumok kívülről tetszőleges adatra módosíthatjuk. Ez így nem túlságosan helyes, hiszen az objektum nem tud a változásról, és ráadásul bármilyen lehetetlen értékre is átírhatjuk. És itt jönnek képbe a láthatóságok.

Láthatóságok

A láthatóságok segítségével tudjuk szabályozni adattagok, metódusok elérését, ugyanis ezeket az objektumorientált paradigma értelmében korlátozni kell, kívülről csak és kizárólag ellenőrzött módon lehessen ezeket elérni, használni. Része az implementáció elrejtésének, azaz akár a programot, akár az osztályt anélkül használják kívülről a felhasználók, hogy pontosan ismernék annak működését.

Mindezek mellett pedig logikus, hogy ha van egy konkrét Macska típusú példányunk, annak adattagjait módosítani kívülről elég furcsa lenne, hiszen az adott objektum nem tudna arról, hogy változott például a súlya.

Két láthatósági módosítószóval (azonban igaziból három láthatósággal) ismerkedünk meg most:

  • public - a publikus tag mindenhonnan látható
  • private - a privát tagot csak az osztály látja
  • nem írtunk ki semmit - "friendly" láthatóság, csomagon belül public, csomagon kívül private

A későbbiekben majd megismerkedünk a 3. láthatósági módosítószóval is.

  • protected - a csomag, az osztály és az azokból származtatott gyermekosztályok látják

Az UML diagramokon láthatunk attribútumok, valamint operációk előtt is néha plusz (+) és mínusz jeleket (-). A + jel azt jelöli, hogy az adott operáció, vagy tulajdonság az osztályon kívülről is használható ahhoz, hogy módosítsuk vele az objektum állapotát, Java esetén ez azt jelenti, hogy az adattag/metódus publikus láthatóságú. A - jel pedig azt jelenti, hogy csak az osztályon belüli használatra szánjuk az adott tulajdonságot/operációt. Java esetén ez lesz a privát láthatóság.

Ezzel az új tudással egészítsük ki a Macska osztályt.

Macska osztály

Így most már láthatjuk, hogy alapvetően milyen láthatóságokat szánunk az osztályunk tagjainak. Az adattagok privát láthatósággal, a metódusok pedig publikus láthatósággal rendelkeznek az osztálydiagramon. Ez egyébként általánosan is igaz, a legtöbbször el szeretnénk rejteni az egyes adattagjainkat, és csak a metódusokon keresztül kommunikálunk az objektummal, amely során természetesen változhat az objektum állapota. Mindazonáltal lehetnek olyan esetek is, amikor bizonyos adattagok publikusak, vagy éppen adott metódusok privát láthatóságúak. Igazítsuk hozzá a programunkat is.

public class Macska {
    private String nev;
    private double suly;
    private boolean baratsagos;
    private int bolhakSzama;
    private int simogatasokSzama = 0;

    public Macska(String nev, boolean baratsagos) {
        this.nev = nev;
        this.suly = 6;
        this.baratsagos = baratsagos;
        this.bolhakSzama = 0;
    }

    public Macska(String nev) {
        this.nev = nev;
        this.suly = 10;
        this.baratsagos = true;
        this.bolhakSzama = 0;
    }

    public Macska() {
        this("Kóbor cica", false);
    }

    public void nyavog() {
        String meow = "Me";
        for (int i = 0; i < suly; i++) {
            if (i % 2 == 0) {
                meow += "o";
            } else {
                meow += "O";
            }
        }
        meow += "w";
        System.out.println(nev + " nyávog: " + meow);
    }

    public void koborol(double tav) {
        suly--;
        bolhakSzama += (tav / 20) + 1;
    }

    public void bolhatlanit() {
        this.bolhakSzama = 0;
    }

}

Így most már csak az objektum kérésére változhatnak az adattagok értékei. Lássuk a korábbi main metódust.

    public static void main(String[] args) {
        Macska foltos = new Macska("Foltos", true);
        foltos.nyavog();
        System.out.println("Foltos súlya: " + foltos.suly);

        foltos.suly = -100;
    }

Így már sem elérni, sem lekérni nem tudjuk Foltos súlyát. Ehhez szükségünk lesz még néhány metódusra.

Getter/Setter

Az adattagok értékeinek lekérésére (getter), valamint inicializálás utáni módosítására (setter) használjuk a getter, illetve setter függvényeket. Ezek összefüggenek azzal, hogy egy objektum adatait csak ellenőrzött módon lehet lekérni, illetve módosítani, ezeken a függvényeken keresztül. Általában csak simán lekérésre, és beállításra használjuk, de ide tehetünk mindenféle ellenőrzéseket is például. Vagy például így érhetjük el, hogy egy tulajdonság kívülről elérhető legyen, azonban módosítani ne lehessen (úgy, csak gettert készítünk).

További előnye lehet a lekérő/beállító függvények használatának, hogy akár még módosíthatunk is az értékeken lekéréskor vagy beállításkor (mondjuk a setternek megadhatjuk, hogy ne hagyja negatívra állítani egy Macska objektum súlyát).

Hagyományos elnevezése: get + adattag neve illetve set + adattag neve, például: getSuly, vagy éppen setSuly. Boolean értékek esetében a getter függvényt általában is + adattag neve formában szoktuk elnevezni, például: isBaratsagos.

    public String getNev() {
        return nev;
    }

    public double getSuly() {
        return suly;
    }

    public void setSuly(double suly) {
        this.suly = Math.max(0, suly);
    }

    public boolean isBaratsagos() {
        return baratsagos;
    }

Az IDE kedvesen segít ezek előállításában is:

  • IntelliJ IDEA esetén: a munkaterületen jobb klikk és Generate, vagy pedig egyszerűen nyomjuk le az Alt + Insert billentyűkombinációt, majd pedig válasszuk a Getter and Setter opciót.
  • Eclipse esetén: a munkaterületen jobb klikk és Source > Generate Getters and Setters... menüpontban találjuk meg az ide kapcsolódó varázslót.

Ha elkészültünk, az adattagok elérésére a getter, míg beállítására a setter függvényeket használhatjuk a main metóduson belül.

    public static void main(String[] args) {
        Macska foltos = new Macska("Foltos", true);
        foltos.nyavog();
        System.out.println("Foltos súlya: " + foltos.getSuly());

        foltos.setSuly(-100);
    }

Így már nem lesz lehetőség nem megfelelő értékek beállítására. Azonban ha meg szeretnénk nézni Foltos adatait, egyesével ki kellene írni az adattagokat, ám ez macerás lehet, ha több helyen is szeretnénk ezt a kiíratást használni.

toString

Természetesen erre is létezik egyszerűbb megoldás, írjuk ki a Macska objektumunkat az alapértelmezett kimenetre, a System.out.println(foltos); utasítás segítségével.

Kimenet

Macska@2ef9b8bc

Ez így nem túl informatív, de szerencsére ezt is megoldhatjuk. Ha minimális munkával szeretnénk emberi nyelvű leírószövegeket adni az objektumról minden esetben, amikor például átadjuk a kész objektumot a System.out.println metódusnak, akkor felül kell definiálnunk a toString() metódust. Ez egy public láthatóságú, String-gel visszatérő metódus.

    public String toString() {
        return "A Macska neve " + this.nev +
                ", " + this.suly + " kg, " +
                this.bolhakSzama + " db bolha van rajta, " +
                this.simogatasokSzama + " alkalommal simogatták meg, és " +
                (baratsagos ? "barátságos." : "barátságtalan.");
    }

Így máris szebb lesz a kimenetünk. Azért, hogy kicsit szebb legyen a kimenet, a baratsagos tulajdonságtól függően írunk ki egy adott szöveget a háromoperandusú if-else (ternary operátor) használatával. Ebben az esetben zárójelet is kell használnunk, különben a fordító a már létező szöveghez akarná hozzáfűzni a this.baratsagos tulajdonság értékét, és az így létrejövő Stringgel szeretné meghívni a ternary operátort, aminek nem lenne értelme, hiszen a ternary operátor használatához egy logikai értékre van szükség a kérdőjel bal oldalán.

Kimenet

A Macska neve Foltos, 6.0 kg, 0 db bolha van rajta, 0 alkalommal simogatták meg, és barátságos.

Az IntelliJ Idea és az Eclipse is szívesen segít a toString elkészítésében, hogy ezt a robotikus munkát ne kelljen kézzel elvégeznünk.

  • IntelliJ Idea esetén: Code > Generate menüpont, majd pedig a felugró menüben toString(). A menüre kattintásokat kiküszöbölhetjük, ha a gyorsbillentyűjén keresztül hívjuk meg: Alt + Insert, majd toString().
  • Eclipse esetén: A munkaterületen jobb klikk és Source > Generate toString()... menüpontban találjuk meg az ide kapcsolódó varázslót.

Ez pedig valami ilyesmit fog elkészíteni helyettünk:

    public String toString() {
        return "Macska{" +
                "nev='" + nev + '\'' +
                ", suly=" + suly +
                ", baratsagos=" + baratsagos +
                ", bolhakSzama=" + bolhakSzama +
                ", simogatasokSzama=" + simogatasokSzama +
                '}';
    }

A generált toString metódus fejléce felett lesz egy @Override szöveg is, ami egy annotáció, de ezzel a következő órán fogunk megismerkedni, egyelőre csak hagyjuk ott, és ne zavarjon minket a további munkában. Az elkészült toString ugyan nem a legszebb, de minden adatot könnyedén ki tudunk belőle olvasni, így bármilyen célra hasznos lehet.

A static módosító

A következőekben megismerkedünk a static módosítószóval, ami ismerős lehet a main függvény fejlécéből. A static módosítóval ellátott tagok nem az objektumokhoz tartoznak (mint például egy hagyományos adattag, ami az objektum állapotához kapcsolódik), hanem magához az osztályhoz, a típushoz tartozik. A static tagok mindig az adott típushoz tartoznak, és ezeken a tagokon az összes példány osztozik.

Akár adattagról, akár metódusról legyen szó, ha valami statikus, akkor azt az UML diagramon aláhúzással jelöljük.

Statikus adattag

Ugyanazon a helyen tárolódik a memóriában, az összes példány esetében ugyanaz lesz az értékük, gyakorlatilag az osztály összes példánya osztozik rajtuk, példányosítás nélkül is hivatkozhatunk rájuk. Ezek az adattagok nem az objektumokhoz, hanem az osztályhoz tartoznak. Gyakorlati jelentősége lehet például akkor, ha egy változóban szeretnénk letárolni egy olyan értéket - például a létrehozott objektumok darabszámát - amelyeknek azonosnak kell lenniük minden objektumpéldány esetén, sőt, akár példányosítás nélkül is értelmesnek kell, hogy legyenek, hiszen példányosítás nélkül is használhatjuk a statikus adattagokat. Hivatkozni rájuk OsztályNév.adattag módon lehetséges, amennyiben látható az adattag az adott helyről.

Például, ha szeretnénk tárolni a programunkban a létrehozott barátságtalan macskákat, akkor erre használhatunk egy statikus adattagot. Ennek megfelelően kiegészítjük az UML osztálydiagramot.

Macska osztály

A kódban ez sok változást nem eredményez, csak egy új adattagot kell felvennünk.

public class Macska {
    private String nev;
    private double suly;
    private boolean baratsagos;
    private int bolhakSzama;
    private int simogatasokSzama;

    private static int BARATSAGTALAN_DARAB = 0;    

A static változóknak többféleképpen is lehet kezdőértéket adni, de a legegyszerűbb ott, ahol deklaráltuk őket. Ha nem adjuk meg a kezdőértéket, a típusra jellemző default értéket veszi fel (ami az egész esetében pont 0, de mi explicit módon jelezzük így, hogy nullát szerettünk volna kezdőértéknek beállítani).

A példányokhoz tartozó (tehát a nem static) adattagoknak is lehet ilyen módon értéket adni, ha szeretnénk. Ha a konstruktorban nem mondunk más kezdőértéket, a deklarációban megadott értéket fogja felvenni a tagváltozó (tehát, ha egy adattag kezdőértéke 2, de a konstruktor ezt beállítja 7-re, akkor az objektum létrejötte utána az adattag értéke 7 lesz).

Statikus metódus

Ezek a metódusok gyakorlatilag nem az objektumokhoz, hanem az osztályhoz tartoznak. Objektum példányosítása nélkül is meghívhatóak. Ezekre példát már sokat láttunk, ilyenek többek között a public static void main(), Integer.parseInt() metódusok.

Statikus metódusban csak statikus adattagok és a kapott paraméterek használhatóak, hiszen ezek nem kapcsolódnak egyetlen objektumhoz sem, azok létezése nélkül is meghívhatunk egy statikus metódust, így természetesen a this-t sem tudjuk használni. Ezek a metódusokat nem lehet felüldefiniálni, de erről a későbbiekben fogunk tanulni.

Készítsünk a Macska osztályhoz egy statikus függvényt, amely kiírja a barátságtalan macskák darabszámát.

Macska osztály

Valósítsuk meg a függvényt.

    public static void printBaratsagtalan() {
        System.out.println("Összesen " + BARATSAGTALAN_DARAB + " Macska létezik.");
    }

A Macska osztály printBaratsagtalan függvényét bárhonnan meghívhatjuk, akár létező Macska példány nélkül is a Macska.printBaratsagtalan() hívással.

UML eszközök

Feladat

Ember

Készítsd el az Ember osztályt, melynek UML osztálydiagramja a következő:

Ember osztály

A karmolasNovel függvény inkrementálja a karmolások számát. A GMAIL_FIOK_DARAB statikus változóban tartsd számon hány ember használ @gmail.com-ra végződő e-mail fiókot. A konstruktorok értelemszerűen inicializálják az adattagokat, természetesen kezdetben egy karmolás sincs a létrejövő Ember objektumokon.

Téglalap

Írj egy osztályt, amely téglalapot reprezentál, annak oldalhosszait tárolja.

Készíts neki konstruktort, amely az oldalakat inicializálja. Írj az osztálynak még egy konstruktort, amely csak egy paramétert vár és amellyel négyzetet lehet létrehozni.

Készíts metódusokat a kerület és terület kiszámítására!

Írj egy másik osztályt, amely futtatható (van benne main függvény), és a parancssori paramétereknek megfelelően létrehoz téglalap objektumokat a Téglalap osztályból, és kiszámolja a Téglalapok területének és kerületének átlagát.

Példa a main függvényre: számhármasok, az első szám jelöli, hogy 1 vagy 2 paraméterből inicializálódik a téglalap, azaz négyzetet vagy téglalapot szeretnénk létrehozni, majd az ezt követő 1 vagy 2 szám tartalmazza a téglalap oldalhosszait: java TeglalapMain 1 5 2 10 22 2 9 8 1 100. Ennek jelentése: Először létrehozunk egy négyzetet, 5-ös oldalhosszal, majd téglalapot 10, 22 oldalhosszakkal, majd megint téglalapot 9 és 8 oldalhosszakkal, majd egy négyzet, melynek 100 az oldalhossza.

Kapcsolódó linkek

Objects, More on Classes

Classes

Practical UML

UML 2 Class Diagrams

Kódolási stílus

Oracle Code Conventions for the Java Programming Language

Java Programming Style Guide

Java Programming Style Guidelines

Google Java Style Guide

Twitter Java Style Guide


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