Változókezelés Javában

Kategória: Standard Java.

A változók a programozási nyelvek alap építőkövei: ezekben tudjuk eltárolni a később használatos adatokat. Számos helyen hozhatunk létre változót: pl. osztályokban (ezeket attribútumoknak nevezzük), függvényekben (ezeket lokális változóknak), a függvények fejléceiben (ezek a paraméterek). A változó szintaxisa: típus változónév;

Alaptípusok

A Java az ún. erősen típusos nyelvek közé tartozik: minden változónak meg kell adni a pontos típusát. Nincsenek előjel nélküli (unsigned) típusok, csak előjelesek. A Java alaptípusai az alábbiak:

  • byte: 1 bájtos előjeles szám.
  • short: 2 bájtos előjeles szám.
  • int: 4 bájtos előjeles szám (megjegyzés: az int minden esetben 4 bájt hosszú, ellentétben a C++-szal, melyben a processzor bitszáma dönti el).
  • long: 8 bájtos előjeles szám. A L posztfixet kell megadnunk ahhoz, hogy biztosan long típust kapjunk: long l = 1234567890L;.
  • float: 4 bájtos előjeles lebegőpontos szám (a legkevésbé ismert Java kulcsszó, a strictfp erre vonatkozik: platformfüggetlen lebegőpontos kalkulációt lehet vele kikényszeríteni). Az f posztfix jelenti ezt a típust: float f = 12.34f;
  • double: 8 bájtos előjeles lebegőpontos szám. A d posztfix jelenti ezt a típust: double f = 12.34d;
  • boolean: 1 bájtos logikai értéket vehet fel: true vagy false (a C++-szal ellentétben nem lehet számot logikai értékként felhasználni úgy, hogy a 0 jelenti a hamis, minden más az igaz értéket; a Java-ban kötelező boolean típust használni, vagy olyan kifejezést, ami boolean-ra értékelődik ki, pl. i != 0)
  • char: egy karakter ASCII kódja, ami 2 bájton tárolódik (tehát mivel nem 1 bájton, belefér az összes ékezetes karakter).

Számok megadása

A számokat a következőképpen adhatjuk meg:

  • Tízes számrendszerben, mint a fenti példákban.
  • Binárisan, 0b prefix-szel pl. 0b01010101.
  • Hexadecimálisan, 0x prefix-szel, pl. 0x1E3F.
  • Oktálisan, prefix-szel, pl. 0123.
  • Exponenciális alakban, az e segítségével pl. 3e5 (melynek értéke 30000).

A Java 7-estől kezdve a számokba aláhúzást _ tehetünk az olvashatóság javítása érdekében, amit a fordító figyelmen kívül hagy, pl.:

int speedOfLight = 299_792_458;
double avogadro = 6.022_140_76e+23;
double pi = 3.141_592_654;

A szokásos műveleteket lehet végrehajtani a változókon, a szokásos precedenciával:

  • Műveletek számokkal:
    • Alapműveletek: +, -, *, /, % (maradékos osztás)
    • Növelés és csökkentés eggyel: ++, —
    • Bitenkénti műveletek: & (bitenkénti és), | (vagy), ^ (kizáró vagy), ~ (komplementer)
    • Bit tologató műveletek: « (pl. a « 2), », »> (ez utóbbi kettő között az a különbség, hogy az az első figyelembe veszi az előjelt, a második viszont mindig nullával tölti fel)
    • Értékadó utasítások: = (ez minden típusra működik), += (pl. a += b az a = a + b rövidítése), -=, *=, …
  • Logikai típust eredményező műveletek: == (egyenlő), != (nem egyenlő), <, <=, >, >=
  • Műveletek logikai típusokkal: && (és), || (vagy), ! (tagadás)

Lássunk egy példát!

public class Main {
    public static void main(String[] args) {
        int a = 123;
        int b = 0XAB;
        int c = a + b;
        System.out.println(a + " + " + b + " = " + c);
    }
}

A fent bemutatott módon futtassuk le! A a + " + " + b + " = " + c rész értelmezése: az a, b és c változók értékeit automatikusan szöveggé konvertálja, majd összefűzi a + hatására szövegtöredékekkel, így az eredmény 123 + 171 = 294 lesz.

Szöveg

A szövegek tárolását kétféleképpen tudjuk megoldani: vagy karaktertömböket használunk, vagy a String nevű belső osztályt alkalmazzuk. A gyakorlatban szinte kizárólag ez utóbbit használjuk, mivel tartalmaz számos plusz funkciót: hibakezelést, optimalizálást, valamint különböző műveleteket. A String kezelésről részletesebben lesz szó a későbbiekben, most csak magával a típussal ismerkedünk meg. A String típust ugyanúgy használjuk mint bármely más primitív típust. Az értékadásnál a szöveget idézőjelek közé kell tennünk. A Java-ban a String-eket összevonhatjuk a + művelettel (ami egyébként számos programozási nyelvben nem működik, és jelentős könnyebbséget jelent). Lássunk egy példát!

public class Main {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "123";
        String s3 = s1 + s2;
        System.out.println(s3);
    }
}

Típuskonverzió

A különböző típusok egymásba konvertálása nem nyilvánvaló. Általában a kisebb méretről a nagyobb felé a konverzió nyilvánvaló, pl. short-ból int-be, és többnyire ezt nem is kell külön jelezni a fordítónak. A másik irányban már adatvesztés léphet fel. A típuskonverzió explicit megadásakor a konvertálandó érték elé zárójelbe írva az új típust.

Egy példán illusztrálom a típuskonvertálás problémáját: (5/2)*2. Azt várnánk, hogy az eredmény 5 lesz, a valóságban viszont 4, mert az osztás egész osztás, maradékkal, az eredmény 2, és 2*2=4, ráadásul a fordító még csak nem is figyelmeztet erre. Megoldás: először az 5-öt kell lebegőpontosra konvertálni, így az osztás a lebegőpontos aritmetikával történik, majd a végeredményt vissza egésszé: (int)(((float)5/2)*2). Ebből a példából láthatóak az erős típusosság hátulütői: a nehezen felderíthető hibák, valamint a helyenként nehezen olvasható kód.

Memóriaterületek

Fontos kérdés, hogy hol helyezkednek el a változók a memóriában. Alapvetően két memóriaterület létezik:

  • Stack (verem): ide kerülnek a lokális változók, melynek legfontosabb oka az, hogy a függvény hívási láncban minden függvény a saját változóit lássa. A memóriaterület felszabadítása akkor következik be, ha a függvény futása befejeződött. A verembe csak primitív változók kerülhetnek, valamint referenciák az objektumokra.
  • Heap (kupac): itt az objektumok helyezkednek el. Felfoghatjuk úgy, mint egyfajta lokális felhő, a pontos felépítése (ellentétben a veremmel) megvalósítás függő. Ha tehát létrejön egy objektum, akkor a hivatkozás rá a verembe kerül, míg maga az objektum a kupacba. Egy objektumra több hivatkozás is lehet (pl. egy függvény hívási láncban, ha paraméterül kerül átadásra, ill. az objektumok is hivatkozhatnak egymásra attribútumaik által). Az objektum által elfoglalt memóriaterületet "kézzel" felszabadítani nem lehetséges (ellentétben a C++-szal, ahol kötelező kézzel, azaz függvényhívással felszabadítani, különben előbb-utóbb elfogy a memória), ezt az ún. garbage collector (szemétgyűjtő) végzi, automatikusan. A Java virtuális gép dönti el, hogy mikor fusson le, de programból lehet javaslatot tenni a lefutásra (amit a JVM nem feltétlenül fogad el). Működése nem nyilvánvaló, pl. az elérhetetlen körkörös hivatkozásokat is fel kell tudnia oldani.

volatile

Létezik egy nehezen érthető kulcsszó: a volatile, amit tetszőleges változó elé írhatunk. Ez nem Java specifikus dolog, de itt is megtalálható. A lényege a következő. A fordítók nagyon sok belső optimalizációs végeznek. Vegyük pl. a következő programrészletet:

int a;
int b;
...
a = 2;
b = a + 3;

Elképzelhető, hogy a fordító "rájön" arra, hogy az utolsó utasításban a b változó értéke csakis 5 lehet, és a gyorsítás érdekében eleve azt az értéket írja be, ahelyett, hogy kiolvasná az a értékét, hozzáadna 3-at, és beleírná a b-be. Viszont tegyük fel, hogy az a változó értéke bármikor megváltozhat (pl. egy mikrokontroller esetén egy megszakítás megváltoztatja, vagy Java-ban egy másik szálon, mégpedig oly módon, hogy arra nem jön rá a fordító); ez esetben hibás eredményre vezet. A volatile kulcsszó azt jelenti, hogy bármikor megváltozhat a változó értéke (pl. akár a fenti két utolsó utasítás között is megváltozhat az a változó), tehát nem számíthat a fordító arra, hogy a pillanatnyi érték lesz benne, így ne hajtsa végre az adott változóra vonatkozó optimalizálásokat, hanem mindenképpen hajtsa végre a fent felsorolt műveleteket. Ezt a következőképpen tudjuk Java-ban megadni: a fenti példa első sorának a következőképpen kell kinéznie: volatile int a;.

Tömb

Ahogy szinte minden programozási nyelvben, a Java-ban is hozhatunk létre tömböket. A tömb sorszámozása a 0-ról indul. A tömbök elemeit kapcsos zárójelekkel adhatjuk meg, míg egy-egy elemre szögletes zárójellel hivatkozhatunk:

// Main.java
public class Main {
    public static void main(String[] args) {
        int[] fibonacci = {0, 1, 1, 2, 3, 5, 8, 13};
        System.out.println(fibonacci[3]);
    }
}

Ez a kódrészlet 2-t ír ki.

Egy tömb elemei lehetnek tömbök:

int[][] matrix = {{1,2,3}, {4,5}, {6,7,8,9}};

Az is lehetséges, hogy az elemek felsorolása helyett lefoglaljuk a memóriaterületet, és a szögletes zárójeles módszerrel adunk kértéket egyesével (vagy ciklusban) az elemeknek:

int[] array = new int[5];
array[2] = 4;

Ez utóbbinak az a nagy előnye, hogy a tömb mérete nem feltétlenül csak konstans, lehet változó is, pl. new int[n];

var

A 10-es Java verzióban megjelent a típus nélküliséget jelölő var kulcsszó. Ennek lényege a következő: a típusokat az esetek döntő többségében ki lehet következtetni, miért kelljen tehát azt kiírni? Vegyünk egy példát: String fruit = "apple";. Valójában abból, hogy "apple", tudjuk, hogy egy String, a fordító is tudja, így használhatjuk anélkül is: var fruit1 = "apple";.

Ha a típus kiírása segíti az olvashatóságot, akkor érdemes továbbra is kiírni, de ha gátolja, akkor használhatjuk a var-t. Ugyanakkor azt is érdemes tudnunk, hogy nem minden esetben működik a var.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License