Kihagyás

3. gyakorlat

A gyakorlat anyaga

Parancssori paraméterek

Parancssori paraméterek használata javac-vel való fordításnál:

java ProgramNeve param1 param2 param3...

Eclipseben a parancssori argumentumok megadása Run Configurations/Arguments alatt történhet.

Tömb bejárása a tömb length (hossz, vagyis elemszám) tulajdonságát felhasználva: (Természetesen nem csak a parancssori paraméterek tömbjének, hanem bármely tömbnek lekérhetjük az elemszámát a length tulajdonsággal.)

//parameterek bejarasa
for (int i=0; i<args.length; i++) {
    System.out.println("" + (i + 1) + ". parameter: " + args[i]);
    //szam szovegge alakitasa
    String str = "" + i;
}

Paraméterek összege

A main függvény egy sztring tömböt kap, ezt nem lehet egyszerűen felülírni. Ha számokként akarjuk kezelni a kapott paramétereket, akkor az Integer.parseInt() függvényt kell használnunk.

//parameterek osszege
int osszeg = 0;
for (int i = 0; i < args.length; i++) {
    osszeg += Integer.parseInt(args[i]);
}
System.out.println("A parameterek osszege: " + osszeg);

Adattípusok

Egyszerű (primitív) adattípusok: boolean, char, byte, short, int, long, float, double, ezek nagy része ismerős lehet C-ből. A C-vel ellentétben Java-ban létezik egy beépített primitív típus logikai érték tárolására, amelyet boolean típusnak nevezünk, és értéke csak true vagy false lehet. További eltérés a C-hez képest, hogy nincs előjeltelen típus, tehát nem használhatjuk az unsigned kulcsszót, csak és kizárólag előjeles típusokat hozhatunk létre. Bővebben ezen a linken olvashatsz a primitív adattípusokról. Egy érdekes cikk a lebegőpontos számokról, számábrázolásról.

Megjegyzés: Ha valami miatt azonban mégis szükség lenne egy előjeltelen egészre, akkor Java 8 (vagy afeletti verzió) esetén használhatjuk az Integer osztály néhány erre a célra létrehozott metódusát, mint például a compareUnsigned, divideUnsigned.

Később látni fogjuk ennek hasznát, de alapvetően minden Java-beli primitív típusnak létezik egy csomagoló osztálya, amellyel egy primitív típusú adatból objektumot készíthetünk, "becsomagolhatjuk" azt. A csomagoló (wrapper) osztályok a következők (sorrendjük megegyezik a primitív típusoknál történt felsorolás sorrendjével): Boolean, Character, Integer, Long, Float, Double. Egy összefoglaló táblázat a beépített típusokról:

Típus neve Érték Default érték Méret Értéktartomány
boolean 1 bitet reprezentál false nincs precíz definíció true/false
char 16 bites unicode karakter \u0000 2 byte 0 - 65535
byte 8 bites egész 0 1 byte -128 - 127
short 16 bites egész 0 2 byte -32768 - 32767
int 32 bites egész 0 4 byte -2147483648 - 2147483647
long 64 bites egész 0L 8 byte -9223372036854775808 -,9223372036854775807
float 32 bites lebegőpontos (IEEE 754) 0.0f 4 byte 1.40129846432481707e-45 - 3.40282346638528860e38 (pozitív vagy negatív), +/- végtelen, +/- 0, NaN
double 64 bites lebegőpontos (IEEE 754) 0.0d 8 byte 4.94065645841246544e-324d - 1.79769313486231570e+308d (pozitív vagy negatív), +/- végtelen, +/- 0, NaN

A beépített típusokat bemutató példakód elérhető a /pub/Programozas-I/nappali/gyakorlat/03/PrimitivTipusok.java útvonalon.

public class PrimitivTipusok {

    public static void main(String[] args) {
    boolean bo = true; // logikai tipus
    char c1 = 'a'; // karakter tipus
    char c2 = '\u0054'; // ket bajtos, unicode karaktereket tarol!
    byte b1 = 127; // 8 bites egesz tipus
    byte b2 = -128; // minden egesz tipus elojeles!
    short s = 1024; // 16 bites egesz
    int i = 0x7fffffff; // 32 bites egesz
    long l = 0x7fffffffffffffffL; // 64 bites egesz
    float f = 123.123f; // 32 bites lebegopontos tipus
    double d = 5.0; // 64 bites lebegopontos

    // kiiras konzolra
    System.out.println(bo);
    System.out.println(c1);
    System.out.println(c2);
    System.out.println(b1);
    System.out.println(b2);
    System.out.println(s);
    System.out.println(i);
    System.out.println(l);
    System.out.println(f);
    System.out.println(d);
    }
}

A lebegőpontos számokkal azonban óvatosan kell bánni. Erre egy tökéletes példa a Süti program. A történet a következő: ellátogatunk az Egyesült Államokba, de sajnos hamarosan indulunk is tovább, így csak a reptéri cukrászdában vásárolhatunk sütit. A sietségünket azonban kihasználja a reptéri cukrász: elcsábít minket a konkurencia elől a 0.1 dolláros sütivel, azonban minden egyes következő sütiért 0.1 dollárral többet kér, mint amennyibe az előző került. Vajon hány sütit ehetünk a maradék 1 dollárunkból? Írjunk rá egy programot, menstük el Suti.java néven.

public class Suti {

    public static void main(String[] args) {
        double penzunk = 1.00;
        int megvettSutik = 0;
        for (double ar = 0.1; penzunk >= ar; ar += 0.1) {
            penzunk -= ar;
            ++megvettSutik;
        }
        System.out.println("Megvett sütik: " + megvettSutik);
        System.out.println("Megmaradt pénz: " + penzunk);
    }
}

Hm.. valami nem stimmel. Számoljuk ki kézzel is, egy egyszerű táblázat segítségével:

Pénzünk Következő süti ára Megevett sütik száma
1.0 $ 0.1 $ 0
0.9 $ 0.2 $ 1
0.7 $ 0.3 $ 2
0.4 $ 0.4 $ 3
0.0 $ lényegtelen 4

A fenti példa által láthatjuk, hogy pl.: valuta tárolására sosem érdemes float vagy double típust választani. Bővebben erről a problémáról ezen és ezen linken.

String (szöveges) típus

String típus. A sztringeket az eddigiekkel szemben itt már szeretjük, kezelésük könnyű. Ezt a típust mindig nagy kezdőbetűvel írjuk.

Operátorok

+, -, *, /, % operátorok és használatuk ugyanaz, mint C-ben.

A + jel sztringekre konkatenációt (összefűzést) jelöl. Használata nagyon intuitív. Például "kutya" + "mutya" -> "kutyamutya"

A C-beli pointereknél használt * (dereferencia) és & (címképzés) operátor itt nem létezik.

>> - aritmetikai eltolás: megőrzi az előjelbitet.

>>> - logikai eltolás: az előjelbitet is tolja. (Tehát nem őrzi meg a szám negativitását)

Az operátorokról bővebben olvashatsz az alábbi linkeken: Summary of Operators, Java Operators Tutorial.

Vezérlési szerkezetek

Majdnem minden működik, ami C-ben is működött, szóval elvileg aki idáig eljutott, ezeket már ismeri. A reláció operátorok is ugyanúgy működnek, mint C-ben, illetve a ++ és -- operátorok is ugyanúgy használhatóak.

Létezik tehát a már megszokott módon az if() és a ?: operátor is. Az if komplexebb feltételek megadására is alkalmas. Például ha azt szeretnénk, hogy egy x változó 3 és 7 között legyen, vagy 13, vagy negatív és nem -9, akkor ezt a következő kóddal adhatjuk meg:

if ((x >= 3 && x <= 7) || x == 13 || (x < 0 && x != 9)) {
    System.out.println("A feltétel teljesül.");
}

Logikai kifejezéseket tehát összefűzhetünk az && (és) és a || (vagy) operátorok használatával. A zárójelezés ilyenkor kritikus fontosságú lehet.

A boolean típus kezelhető logikai kifejezésként, mivel pontosan az is, azaz egy igaz vagy hamis értéket tárol. Ilyenkor operátorok nélkül is igazságértéket jelképeznek.

boolean pozitiv = true;
if (pozitiv) System.out.println("A szám pozitív.");
if (!pozitív) System.out.println("A szám nem pozitív.");

A ?: operátor használata például a kiíráson belül lehet indokolt. Az előző kódot például jelentősen egyszerűsíti:

boolean pozitiv = true;
System.out.println("A szám " + ((pozitív) ? "" : "nem ") + "pozitív.");

Viszont, C-vel ellentétben itt nincs implicit konverzió int és boolean között, a Java megköveteli az összehasonlító operátorok használatát ebben az esetben:

int x = 1;
if (x) { //Javaban ez így nem működik!
  System.out.println("Minden rendben!");
}
//A helyes feltétel az alábbi:
if (x > 0) {
  System.out.println("Minden rendben!");
}

A while, do-while és switch utasítás megegyezik a C-ben tanultakkal. A for utasításnál viszont itt kényelmesebben deklarálható ciklusváltozó, mivel itt az utasításon belül is megtehetjük ezt (viszont ezesetben a cikluson kívül már nem látjuk a ciklusváltozót!). Tehát a C-vel ellentétben itt már működne a következő példa:

for (int i = 0; i < 5; i++) {
    //ciklus tartalma
}

Az egyetlen dolog, amely nem működik Java-ban, de a C-ben igen, az a goto utasítás. A program egyes részeit ugyanúgy címkézhetjük, azonban a goto parancsot nem használhatjuk. Ez Javaban ugyan ez foglalt kulcsszó, de implementáció nem tartozik hozzá. Ennek oka az, hogy a nyelv készítői kulcsszóvá tették a goto-t, azonban eddig még nem jött tömeges nyomás, nem volt olyan probléma, amelyet ezen kulcsszó nélkül ne tudtak volna megoldani, így egyelőre ez egy fenntartott kulcsszó (azaz például ilyen nevű változót nem hozhatunk létre), de semmiféle működése nincs.

Memóriaterületek

A következőkben röviden bemutatjuk a stacket, illetve a heapet, azonban ezek a témakörök az előadáson ennél jóval részletesebben be lettek mutatva. Bővebben olvashatunk a témáról a következő linkeken: JVM specifikáció ide vonatkozó részei

Stack

A stack-en tároljuk a primitív típusú adatokat. Ez a memóriaterület nagyon gyors elérésű, általában lokális változókat, rövid életű adatokat tárolunk benne. A stacken létrehozott változók élettartalma a létrehozó blokkra korlátozódik, azaz pl.: ha van egy lokális változónk egy függvényben, akkor az a változó addig él, és addig lesz a memóriában tárolva, ameddig a függvény futása be nem fejeződik. Ezt követően a foglalt memóriaterület automatikusan felszabadul. Hátránya a heap memóriával szemben az, hogy a stack jóval kisebb. Javaban csak és kizárólag primitív típusú adatokat tárolhatunk a stacken (szám, bájt, karakter vagy logikai érték).

A gyakorlatban, ha egy változó értékét stacken tároljuk (pl.: int x = 42;), akkor akármikor hivatkozunk a változóra, a változó neve gyakorlatilag egyet jelent az értékkel, érték szerint tárolódik.

Heap

A heap memórián tároljuk az objektumokat, tömböket (ezeket referencia típusúaknak hívjuk). Ez a stack-kel ellentétben egy lassabb, dinamikus futás-közbeni memóriaterület, cserébe viszont jóval nagyobb terület áll a programunk rendelkezésére. Az itt létrehozott adatokat hosszabb távon tároljuk, globálisan léteznek, nem csak egy-egy függvény belsejében, lokálisan.

Amikor létrehozunk egy objektumot a heapen, akkor az adott változó az gyakorlatilag csak egy referencia, mutató lesz a heap-beli memóriaterületre, ahol ténylegesen az objektum tárolódik, a változó maga csak ezt a referenciát tárolja. Ez ismerős lehet, a pointer fogalma hasonló. Tehát a referencia típusú változó nem közvetlenül tárolja az értéket, csak egy referenciát tárol, és a heapen tárolt értéket közvetetten, ezen a referencián keresztül érjük el.

Gyakorlati példa: Egy nagyon szemléletes példa, Bruce Eckel Thinking in Java című könyvéből: Képzeljük el, hogy van egy Televízió típusú objektumunk a heapen, melyhez tartozik egy referencia, amelyet letárolunk, és ennek a neve legyen távirányító. Ahogy a való életben is, ülünk a kényelmes fotelünkben, és nézzük a televíziót, de amikor szeretnénk a Televízió objektumunkon bármely módosítást eszközölni (halkítani, hangosítani, csatornát váltani, be-, kikapcsolni), akkor a távirányító segítségével tesszük meg ezt. És ha a szobában sétálva, lefekve bárhol szeretnénk a Televízió objektumon machinálni, akkor a távirányítót visszük magunkkal, nem pedig magát a Televíziót.

Alapértelmezett kimenetek

Ahogy már beszéltünk róla, a Java nyelvben szöveget írhatunk ki a képernyőre a System.out.println() paranccsal. Ez viszont nem feltétlenül mindig a terminált jelenti. A System.out az alapértelmezett kimenet, ami esetünkben a terminálra volt irányítva. Ezt át is lehet állítani, hogy máshova írjunk vele, például egy fájlba.

Az eddig tanult kiíratáshoz hasonló az alapértelmezett (vagy default) hibakimenet, melynek fogalma szintén ismerős lehet. Erre a System.err.println() paranccsal tudunk írni a már látott módon. Ez hibaüzenetek közlésére szolgál. Egyetlen szemmel látható különbsége az alapértelmezett kimenettől, hogy az Eclipse konzolján a szöveg pirossal jelenik meg.

A két kimenet viszont lehet két különböző helyre is irányítva.

A kimenetek működése aszinkron. Ez azt jelenti, hogy nem feltétlenül pont akkor írja ki a dolgokat, amikor mi utasítjuk rá: ha a kimenet éppen nem tudja ellátni a feladatát, akkor a kiírandó adat várakozik. Tehát a következő kódnak két lehetséges kimenetele lehet:

System.out.println("Most hibaüzenet következik:");
System.err.println("Ez a hibaüzenet.");

Az egyik kimenetel:

Most hibaüzenet következik:
Ez a hibaüzenet.

A másik pedig:

Ez a hibaüzenet.
Most hibaüzenet következik:

Ebből következik, hogy a hibakimenet NEM használható a szöveg egyszerű színezésére, mert összekavarodhat a mondanivalónk. Attól persze nem kell félni, hogy a System.out-ra küldött két üzenet összekeveredik, ezek sorrendben várakoznak.

A gyorsabb munka érdekében az Eclipse megkönnyíti a kimenetre írást. A default kimenet eléréséhez egyszerűen beírhatjuk a sysout szót, majd Ctrl+Space hatására kiegészíthetjük System.out.println()-né, a syserr szót pedig System.err.println()-né.

Java és C-beli különbségek, hasonlóságok

Az egyes szekciókban tárgyalt és azokon túlmutató hasonlóságokról és különbségekről bővebben olvashatsz itt és itt.

Feladatok

  1. Írd ki a parancssori paramétereket a konzolra
  2. Írd ki a parancssori paramétereket a konzolra, fordított sorrendben
  3. Írd ki a parancssori paramétereket úgy, hogy az n. sorban az első n darab parancssori paramétert írja ki: első sorba csak az elsőt, a másodikba az első kettőt szóközzel elválasztva, a harmadikba az első hármat, stb..

    java Main egy ketto kutya cica fagyi

    egy
    egy ketto
    egy ketto kutya
    egy ketto kutya cica
    egy ketto kutya cica fagyi
    
  4. Írd ki a parancssori paraméterek közül a legnagyobbat, legkisebbet, valamint az értékek átlagát.

  5. A parancssori paraméterek alapján döntsd el, hogy egy a bemenő számok számtani, mértani sorozatot alkotnak-e, vagy esetleg egyiket sem. Feltehetjük, hogy mindegyik egész szám, és legalább 3 db paraméterünk van. Az összegképletek:

    • számtani: an = a1 + (n – 1) * d
    • mértani: an = a1 * q n – 1
  6. Számítsd ki a parancssoron kapott két időpont (óra perc óra perc) között eltelt időt, és írasd ki a konzolra (óra perc formában). A program elkészítése során ügyelj az adatok helyességére

    • bemenő paraméterek száma
    • az órák 0-23 intervallumba kell, hogy essenek
    • a percek 0-59 intervallumba kell, hogy essenek

Kapcsolódó linkek

Learning the Java Language

A gyakorlat anyaga

Tömbök

Korábban már volt szó konkrét tömbökről (pl.: parancssori paraméterek tömbje). Az akkor tárgyaltak természetesen általánosságban is igazak a tömbökre. Egy rövid összefoglaló: A tömbök azonos típusú, de egynél több érték tárolására vannak kitalálva. Az elemek száma azonban rögzített, tehát adott számú, azonos típusú elemet tartalmazó adattípusról van szó. Az indexelés 0-val kezdődik, ahogy az ismerős lehet Programozás alapjai tárgyból.

Megjegyzés: Habár Javaban, C-ben 0-tól indexelünk, nem feltétlenül van ez így minden nyelvben. Vannak olyan nyelvek, melyek egytől kezdik az indexelést. Ezen nyelvekről itt találsz egy összefoglaló listát.

Fontos lehet, hogy a C-vel ellentétben itt nagyon szeretjük a tömböket, hiszen a Java (mivel menedzselt nyelvről van szó, azaz a kód nem közvetlenül a hardveren fut, hanem a JVM-en, ahogy ezt korábban tárgyaltuk) teljes körű indexhatár-ellenőrzést végez, kivétellel jelzi, ha alul- vagy túlindexelés történik.

A Java-beli tömböket többféleképpen is létre lehet hozni. Statikusan, ha előre tudjuk a tömb elemeit, vagy pedig dinamikusan, amikor nem feltétlen tudjuk a tömb elemeinek értékét. Mind primitív típusból, mind pedig osztályokból létrejövő objektumok esetén használhatunk tömböket. Tömböket dinamikusan, a new operátorral lehet létrehozni, mérete a már ismert length tulajdonsággal kérdezhető le.

Egydimenziós tömbök

Az egyetlen kiterjedéssel (dimenzióval) rendelkező tömböket szokás vektornak is nevezni, ahogy ez a kifejezés matematikából ismerős lehet. A tömböt deklarálni a következőképp lehet:

típus tömbnév[];
típus[] tömbnév;

A két deklaráció között semmilyen különbség sincs. Futás közben a már említett new operátorral tudjuk definiálni:

tömbnév = new típus[méret];

Egy konkrét példa:

int[] x = new int[5];

Ennek a tömbnek az elemei: x[0], x[1], x[2], x[3], x[4]. A tömböt legegyszerűbben egy for ciklussal tölthetjük fel.

A Java lehetővé teszi, hogy a tömböket a definiálás során konstans értékekkel töltsük fel. Ilyenkor a tömböt a fordító hozza létre.

típus[] tömbnév = { konstans1, konstans2 } 
// egy konkrét példa:
int[] arr = { 1, 2, 3, 4, 5 };

Adott tömb mérete minden esetben egy nemnegatív egész szám, melynek értéke nem lehet nagyobb, mint a lehetséges legnagyobb index. Ez a JVM-től függ, és mivel az indexelés int értékekkel történik, ezért az elméleti legnagyobb elemszám Integer.MAX_VALUE (ez egy definiált konstans), azonban ez a gyakorlatban egy ettől 5-8 értékkel kisebb szám. Derítsük ki, hogy a saját JVM-ünk esetében mekkora ez a szám: ehhez hozzunk létre egy tömböt, és próbáljuk meg lefordítani, és lefuttatni az elkészült fájlt:

String[] tomb = new String[Integer.MAX_VALUE - 4];

Többdimenziós tömbök

Analóg módon az egydimenziós tömbökhöz, illetve a korábban, Programozás alapjain tanultakhoz:

típus[][]...[] tömbnév;
típus tömbnév[][]...[];

Két dimenziós tömb létrehozására egy példa:

típus[][] tömb;
tömb = new típus[méret1][méret2];

Egy konkrét példa kétdimenziós tömb létrehozására és feltöltésére. A tömböt legegyszerűbben egy for ciklussal tölthetjük fel.

int[][] tomb;
tomb = new int[10][9];

for (int i=0; i < tomb.length; i++){
    for (int j = 0; j < tomb[i].length; j++) {
        tomb[i][j] = (i+1)*(j+1)*9;
    }
}

A Java lehetővé teszi, hogy a tömböket a definiálás során konstans értékekkel töltsük fel. Ilyenkor a tömböt a fordító hozza létre

int[][] matrix = { { 1, 2 }, { 3, 4 } };

Tömbök másolása

Mivel a tömbök referencia típusúak (előző gyakorlat), ezért egy egyszerű értékadás esetén csak a referenciát (azaz a tömb címét) másoljuk át egy másik változóba, a konkrét, memóriában létező tömböt nem. Tényleges tömbmásolást kézzel is megvalósíthatunk, egyszerű for ciklus segítségével, de létezik egy, a JDK-ban már előre elkészített metódus épp erre a célra: a tényleges másolás a System.arraycopy() metódussal lehetséges. Ennek fejléce (javadoc):

public static void arraycopy(Object forrás, int forrásKezdetiElem, Object cél, int kezdetiPozíció, int hossz)

Egy konkrét példa tömbök másolására:

   int tomb1[] = { 30, 31, 1, 2, 3, 4, 5, 6, 7 };
   int tomb2[] = { 29, 0, 0, 32, 33 };

   System.arraycopy(tomb1, 0, tomb2, 1, 2);

   for (int i=0; i<tomb2.length; i++){
       System.out.print(tomb2[i] + " ");
   }
   System.out.println();

Ennek kimenete: 29 30 31 32 33.

Osztályok létrehozása

Ahogy korábban már tárgyaltuk, a konkrét életbeli objektum(csoportok) formai leírása lesz az osztály. Osztályokat kell létrehoznunk 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 az emberek. Sok különböző ember létezik, de mégis rendelkeznek közös tulajdonságokkal: van név és kor tulajdonságuk, valamint mindenki tudja magáról, hogy férfi-e. Készítsünk egy ilyen osztályt, melynek kulcsszava a class. Á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 az Ember.java fájlt:

public class Ember {

}

Az embereknek van néhány közös tulajdonságuk, amelyet UML-ben tulajdonságnak, attribútumnak hívtunk. Most a név, kor , valamint a férfi-e tulajdonságot szeretnénk a programunkban használni. Ezek legyenek String, int és boolean típusú változók.

public class Ember {
    String nev;
    int kor;
    boolean ferfi;
}

Elkészült az osztályunk, már csak a viselkedést kellene valahogy a forráskódban is reprezentálni. Erre metódusok lesznek a segítségünkre, melyeket az osztályba írunk bele, és ezek azt jelentik, hogy az adott osztályból létrejövő objektumok milyen viselkedéssel rendelkezhetnek. Például, az emberek tudnak köszönni, ez operációjuk. Készítsük el a koszon metódust.

public class Ember {
    String nev;
    int kor;
    boolean ferfi;

    public void koszon(){ 
        System.out.println("Szia! " + nev + " vagyok és " + kor + " éves, mellesleg " + (ferfi ? "férfi." : "nő.")); 
    }
}

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 sázmtalan objektumpéldány is létrehozható. Hozzuk létre Józsit a main függvényben a new operátor segítségével.

public static void main(String[] args){
    Ember jozsi = new Ember();
    jozsi.koszon();
}

Fordítsuk, majd futtassuk le a programot. Ennek kimenete: Szia! null vagyok és 0 éves, mellesleg nő. Nem éppen lett Józsi, még férfi sem igazán. Ennek az az oka, hogy az osztályban lévő adattagok nincsenek rendesen inicializálva, a típusokhoz tartozó default értéket vették fel (előző gyakorlat). Ahhoz, hogy ezt megfelelő módon megtehessük, konstruktort kell létrehoznunk.

Konstruktor

Ez a speciális függvény fogja létrehozni a példányokat (objektumokat) az osztályokból, beállítani az osztályban deklarált változókat a konkrét, adott objektumra jellemző értékekre. Ilyen alapból is létezik (hiszen az előbb mi sem írtunk ilyet), de mi paramétereseket is megadhatunk, ami segítségével gyorsan és könnyen tudjuk az adott objektumot inicializálni. Megjegyzendő, hogy ha mi készítünk bármilyen konstruktort, akkor a fordító nem készít egy default, paraméter nélküli konstrukotrt (ahogy azt tette korábban).

  • A konstruktor nevének meg kell egyeznie az osztály nevével
  • Visszatérési értéke/típusa nincs (nem is lehet!)
  • Új objektum létrejöttekor fut le
/*
  default konstruktor

  Ez alapból létezik, nem muszáj kiírni, hacsak nem szánunk neki speciális szerepet.
*/
public Ember() {}

/*
  paraméteres konstruktor

  minden esetben létre kell hoznunk (ha szeretnénk paraméteres konstruktort).
*/
public Ember(String nev, int kor, boolean ferfi) {
      this.nev = nev;
      this.kor = kor;
      this.ferfi = ferfi;
}

this kulcsszó - az objektum saját magára tud hivatkozni vele, ennek akkor van gyakorlati haszna például, amikor egy metódusban szereplő paraméter neve megegyezik az osztályban deklarált adattag nevével, akkor valahogy meg kell tudnunk különböztetni, hogy melyik az objektum tulajdonsága, és melyik a paraméter.

A paraméteres konstruktorral könnyen, egy sorban létrehozhatjuk és értéket is adhatunk a példányoknak, ami energia- és helytakarékos módszer a paraméter nélkülihez képest.

Az így kiegészített osztály után hozzuk létre most már valóban Józsit.

public static void main(String[] args){
    Ember jozsi = new Ember("Jozsi", 20, true);
    jozsi.koszon();
}

A program kimenete: Szia! Jozsi vagyok és 20 éves, mellesleg férfi.

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.

Eddig ismert láthatóságok:

public - mindenhonnan látható

private - csak maga 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

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. 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.

Ezekkel tulajdonképpen elrejtjük a változókat és akár még módosíthatunk is az értékeken lekéréskor vagy megadáskor (mondjuk setternek megadhatjuk, hogy ne hagyja 0-ra állítani a változó értékét (például egy osztás nevezőjét))

Hagyományos elnevezése: get + adattag neve illetve set + adattag neve.

//getter
public String getNev() {
    return this.nev;
}

//setter
public void setNev(String nev) {
    this.nev = nev;
}

//setter
public void setKor(int kor) {
    if (kor > 0) {
        this.kor = kor;
    } else {
        System.err.println("A kor csak pozitív szám lehet!");
    }
}

Boolean értékek esetében a getter függvényt általában is + adattag neve formában szoktuk elnevezni.

/*
Kiegészítés: boolean érték gettere
A boolean értékű gettert get kulcsszó helyett is-zel szokás jelölni.
*/
public boolean isFerfi(){
    return this.ferfi;
}

Az Eclipse kedvesen segít ezek előállításában is (de a ZH-n ki kell írni őket), a munkaterületen jobb klikk és Source > Generate Getters and Setters... menüpontban találjuk meg az ide kapcsolódó varázslót.

toString metódus

Írassuk ki az elkészült jozsi nevű, ember típusú objektumunkat:

System.out.println(jozsi);

A program kimenete valami hasonló: Ember@677327b6. Ez nem túl szerencsés, pláne nem olvasható. Ezt megoldhatjuk úgy, hogy az Ember osztály összes adattagját egyesével kiírogatjuk, ám ez macerás lehet, ha több helyen is szeretnénk ezt a kiíratást használni.

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 "Ez egy ember, neve " + this.nev +
        ", szuletett " + this.szuldatum +
        ", ferfi=" + this.ferfi;
}

Ami ugyanolyan mintha egy sorba írtuk volna a return-t, csak így nem fut ki a sor a végtelenbe. Ezek után nincs más dolgunk, nézzük meg mi történik a következő sor hatására:

System.out.println(jozsi);

Ennek eredménye: Ez egy ember, neve Jozsi, kora 20, ferfi=true

Az Eclipse kedvesen segít ezek előállításában is (de a ZH-n ki kell írni őket), a munkaterületen jobb klikk és Source > Generate toString()... menüpontban találjuk meg az ide kapcsolódó varázslót.

Static

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. Tulajdonképpen 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 értelmesek.

Hivatkozni rájuk Osztály.adattag módon lehetséges, amennyiben látható az adattag az adott helyről.

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.

Ezek a metódusokat nem lehet felüldefiniálni, de erről a későbbiekben fogunk tanulni.

Final

Adattag

A final adattag kezdeti értékét nem változtathatjuk meg a futás során. Ehhez persze adni kell kezdőértéket, vagy a létrehozás helyén (pl.: final int TOMEG = 10) vagy pedig a konstruktorban. Ha a final értéken változtatni próbálnánk meg, akkor fordítási hibát kapunk. Próbáljuk ki a következőt.

private final int tomeg = 10;
tomeg=20; // Fordítási hiba!!

Metódus

Ezeket a metódusokat a gyerekosztályban nem lehet felüldefiniálni. Erről a későbbiekben fogunk tanulni, de itt érdemes visszaemlékezni az UML-nél tanultakra.

Osztály

Nem lehet gyerekosztálya.

Konstans

Nincs rá külön kulcsszó, az eddigiekből merítve azonban könnyen kitalálható: valódi konstans létrehozása a static és final kulcsszavak együttes használatával, hiszen az így létrejövő változó: statikus lesz, bármely objektumpéldány esetén ugyanazt az egy konkrét adatot látjuk/módosítjuk; valamint a final miatt a kezdőérték nem változtatható meg.

Konstans változók nevét általában csupa nagybetűvel szoktuk írni, szóhatárnál aláhúzást teszünk.

public class Alma {
 public static final int ALMA_TOMEG = 10; 
}

Ennek elérése kívülről:

int almaTomege = Alma.ALMA_TOMEG;

Garbage collection - Szemétgyűjtés

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

System.gc();

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

Feladatok

  1. Hozzunk létre egy 7*10-es int-eket tartalmazó tömböt, töltsük fel őket, az alábbi séma szerint: tömb[x][y] = x*y; (pl.: tömb[5][8] = 40;)
  2. Hozzunk létre egy karakter tömböt 't' 'e' 'l' 'e' 'f' 'o' 'n' karakterekkel. Másoljuk egy új tömbbe a 'l' 'e' karaktereket!
  3. Í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.

Kocsmaszimulátor part 1:

Bővítsük ki a már létező Ember osztályt egy privát pénz, és részegség int, és egy kocsmában boolean változókkal. Legyen egy új konstruktor, ez fogadjon a már meglévő paramétereken kívül egy pénz paramétert is, amit állítson be az Ember pénzének. A részegség 0, a kocsmában false legyen alapértelmezetten. Legyen az Embernek egy iszik(Kocsmáros kocsmaros) metódusa, ami egy Kocsmárost vár majd. Ha ezt meghívják, akkor ha az illető a kocsmában van, fogyjon 1 a pénzéből, nőjön 1-gyel a részegsége, generáljon 1 koszos poharat, és adjon 1 pénzt a kocsmárosnak, akit paraméterül kapott. Majd látjuk, hogy a poharat hova kell eltárolni, és mi a Kocsmáros. Ha nincs a kocsmában, akkor írjon ki egy üzenetet erről. Legyen egy alszik() metódusa is, ami nullázza a részegséget és kiírja, hogy elaludt, egy hazamegy() metódusa, ami false-ra állítja a kocsmában változót, és egy jön() metódusa, ami true-ra. Ezekről is történjen kiírás.

Legyen egy Kocsmáros osztály is. Neki is legyen privát pénze, amit konstruktorban is meg lehet adni. Az összes kocsmáros ugyanazokon a koszos poharakon osztozzon (static), és legyen egy elmos() metódusa, ami csökkenti eggyel a koszos poharak számát, és kiírja, hogy elmosott egy poharat. Ha nincs koszos pohár, akkor azt írja ki.

Legyen egy Ital osztály is, aminek a következő privát tulajdonságai lesznek: ár, alkoholtartalom.

Az Embernek legyen egy olyan iszik metódusa is, aminek fejléce iszik(Kocsmáros kocsmáros, Ital ital), azaz italt is tud fogadni. Ekkor az ital árát adja át az Ember a Kocsmárosnak 1 helyett. Az Ember részegsége az ital alkoholtartalmával nőjön.

Ha a részegség eléri a 40-et, akkor az Ember mindkét iszik() függvényénél automatikusan aludjon el.

Az összes osztály privát változóihoz legyenek getter, setter metódusok, és az osztályokhoz értelmes toString metódus.

Legyen egy main függvény, mondjuk Main nevű osztályban, itt írjatok egy rövidke futtatást, amiben eljátszogattok egy kicsit az emberekkel, bemutattok pár esetet, igyanak, aludjanak, stb...

Kapcsolódó linkek

Tömbök, Class Arrays, Tömbök tutorial

Tömbméret a bájtkódban

Enum Types

Objects, More on Classes

Classes

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