Vianoce v ITnetwork sú tu! Dobí si teraz kredity a získaj až 80 % extra kreditov na e-learningové kurzy ZADARMO. Zisti viac.
Hľadáme nové posily do ITnetwork tímu. Pozri sa na voľné pozície a pridaj sa k najagilnejšej firme na trhu - Viac informácií.

4. diel - Referenčné a primitívne dátové typy

V predchádzajúcom cvičení, Riešené úlohy k 3. lekcii OOP v Jave, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.

Začíname pracovať s objektmi a objekty sú referenčnými dátovými typmi, ktoré sa v niektorých ohľadoch správajú inak než typy primitívne (napr. int). Je dôležité, aby sme presne vedeli, čo sa vo vnútri programu deje, inak by nás v budúcnosti mohlo všeličo prekvapiť.

Pre istotu si ešte raz zopakujme, čo sú to primitívne typy. Všeobecne sú to jednoduché štruktúry, napr. jedno číslo, jeden znak. Väčšinou sa chce, aby sme s nimi pracovali čo najrýchlejšie, v programe sa ich vyskytuje veľmi veľa a zaberajú málo miesta. V anglickej literatúre sú často popisované slovami light-weight. Majú pevnú veľkosť. Príkladom sú napr. int, float, double, char, boolean a ďalšie.

Aplikácia (resp. jej vlákno) má operačným systémom pridelenú pamäť v podobe tzv. zásobníka (stack). Ide o veľmi rýchlu pamäť s priamym prístupom, jej veľkosť aplikácia nemôže ovplyvniť, prostriedky sú prideľované operačným systémom. Táto malá a rýchla pamäť je využívaná na ukladanie lokálnych premenných primitívneho typu (až na výnimky pri iteráciách, ktorými sa nebudeme zaoberať). Premennú si v nej môžeme predstaviť asi takto:

Zásobník pamäte počítača - Objektovo orientované programovanie v Jave

Na obrázku je znázornená pamäť, ktorú môže naša aplikácia využívať. V aplikácii sme si vytvorili premennú a typu int. Jej hodnota je 56 a uložila sa nám priamo do zásobníka. Kód by mohol vyzerať takto:

int a = 56;

Môžeme to chápať tak, že premenná a má pridelenú časť pamäte v zásobníku (veľkosti dátového typu int, teda 32 bitov), v ktorej je uložená hodnota 56.

Vytvorme si novú konzolovú aplikáciu s názvom napríklad ReferenceTypes a pridajme si k nej jednoduchú triedu, ktorá bude reprezentovať používateľov nejakého systému. Pre názornosť vypustím komentáre a nebudem riešiť viditeľnosti:

public class User {
    public int age;
    public String name;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name;
    }
}

Trieda má dva jednoduché verejné atribúty, konštruktor a prepísanú metódu toString(), aby sme používateľov mohli jednoducho vypisovať. Do nášho pôvodného programu (metóda main()) pridajme vytvorenie inštancie tejto triedy:

int a = 56;
User james = new User("James Brown", 28);

Premenná james je teraz referenčného typu. Pozrime sa na novú situáciu v pamäti:

Zásobník a halda v pamäti počítača - Objektovo orientované programovanie v Jave

Vidíme, že objekt (premenná referenčného dátového typu) sa už neukladá do zásobníka, ale do pamäte zvanej halda. Je to z toho dôvodu, že objekt je spravidla zložitejší ako primitívny dátový typ (väčšinou obsahuje hneď niekoľko ďalších atribútov) a taktiež zaberá viac miesta v pamäti.

Zásobník aj halda sa nachádzajú v pamäti RAM. Rozdiel je v prístupe a veľkosti. Halda je prakticky neobmedzená pamäť, ku ktorej je však prístup zložitejší a tým pádom pomalší. Naopak zásobník je pamäť rýchla, ale veľkostne obmedzená.

Premenné referenčného typu sú v pamäti uložené vlastne dvakrát, raz v zásobníku a raz v halde. V zásobníku je uložená iba tzv. referencia, teda odkaz do haldy, kde sa potom nachádza skutočný objekt.

Napr. v C++ je veľký rozdiel medzi pojmom ukazovateľ a referencia. Java žiadne ukazovatele našťastie nemá a používa termín referencie, tie sa paradoxne princípom podobajú skôr ukazovateľom v C++. Pojmy ukazovateľ a referencia tu spomínané teda znamenajú referenciu v zmysle Javy a nemajú s C++ nič spoločné.

Môžete sa pýtať, prečo je to takto urobené. Dôvodov je hneď niekoľko, poďme si niektoré vymenovať:

  1. Miesto v stacku je obmedzené.
  2. Keď budeme chcieť použiť objekt viackrát (napr. ho odovzdať ako parameter do niekoľkých metód), nemusíme ho v programe odovzdávať ako kópiu. Odovzdáme iba malý primitívny typ s referenciou na objekt namiesto toho, aby sme všeobecne pamäťovo náročný objekt kopírovali. Toto si o chvíľu ukážeme.
  3. Pomocou referencií môžeme jednoducho vytvárať štruktúry s dynamickou veľkosťou, napr. štruktúry podobné poľu, do ktorých môžeme za behu vkladať nové prvky. Tie sú na seba navzájom odkazované referenciami ako reťaz objektov.

Založme si dve premenné typu int a dve premenné typu User:

int a = 56;
int b = 28;
User james = new User("James Brown", 28);
User jack = new User("Jack White", 32);

Situácia v pamäti bude nasledovná:

Referenčné hodnoty v Jave v pamäti počítača - Objektovo orientované programovanie v Jave

Teraz skúsme do premennej a priradiť premennú b. Rovnako tak priradíme aj premennú jack do premennej james. Primitívny typ sa v zásobníku len skopíruje, pri objekte sa skopíruje iba referencia (čo je vlastne aj primitívny typ), ale objekt máme stále len jeden. V kóde vykonáme teda toto:

int a = 56;
int b = 28;
User james = new User("James Brown", 28);
User jack = new User("Jack White", 32);
a = b;
james = jack;

V pamäti bude celá situácia vyzerať nasledovne:

Referenčné hodnoty v Jave v pamäti počítača - Objektovo orientované programovanie v Jave

Presvedčme sa o tom, aby ste videli, že to naozaj tak je :) Najprv si necháme všetky štyri premenné vypísať pred a po zmene. Pretože budeme výpis volať viackrát, napíšem ho trochu úspornejšie. Mohli by sme dať výpis do metódy, ale ešte nevieme, ako deklarovať metódy priamo v súbore s metódou main() (v tomto prípade súbor ReferenceTypes.java) a spravidla sa to ani veľmi nerobí, pre vážnejšiu prácu by sme si mali urobiť triedu. Upravme teda kód nasledovne:

        // variable declaration
        int a = 56;
        int b = 28;
        User james = new User("James Brown", 28);
        User jack = new User("Jack White", 32);
        System.out.printf("a: %s%nb: %s%njames: %s%njack: %s%n%n", a, b, james, jack);
        // assignment
        a = b;
        james = jack;
        System.out.printf("a: %s%nb: %s%njames: %s%njack: %s%n%n", a, b, james, jack);

Na výstupe programu zatiaľ rozdiel medzi primitívnym a referenčným typom nespoznáme:

Konzolová aplikácia
a: 56
b: 28
james: James Brown
jack: Jack White

a: 28
b: 28
james: Jack White
jack: Jack White

Avšak vieme, že zatiaľ čo v a a b sú naozaj dve rôzne čísla s rovnakou hodnotou, v james a jack je ten istý objekt. Poďme zmeniť meno používateľa jack a podľa našich predpokladov by sa mala zmena prejaviť aj v premennej james. K programu pripíšeme:

        // change
        jack.name = "John Doe";
        System.out.printf("james: %s%njack: %s%n", james, jack);

Zmenili sme objekt v premennej jack a znova vypíšeme james a jack:

Konzolová aplikácia
a: 56
b: 28
james: James Brown
jack: Jack White

a: 28
b: 28
james: Jack White
jack: Jack White

james: John Doe
jack: John Doe

Spolu so zmenou premennej jack sa zmení aj premenná james, pretože premenné ukazujú na ten istý objekt. Ak sa pýtate, ako vytvoriť skutočnú kópiu objektu, tak najjednoduchšie je objekt znovu vytvoriť pomocou konštruktora a dať do neho rovnaké dáta. Ďalej môžeme použiť klonovanie, ale o tom zas až niekedy inokedy. Pripomeňme si situáciu v pamäti ešte raz a zamerajme sa na Jamesa Browna:

Referenčné hodnoty v Jave v pamäti počítača - Objektovo orientované programovanie v Jave

Čo sa ním stane? "Zožerie" ho tzv. Garbage collector.

Garbage collector - Objektovo orientované programovanie v Jave

Garbage collector a dynamická správa pamäte

Pamäť môžeme v programoch alokovať staticky, to znamená, že v zdrojovom kóde vopred určíme, koľko jej budeme používať. Doteraz sme to tak vlastne robili a nemali sme s tým problém, pekne sme do zdrojového kódu napísali potrebné premenné. Čoskoro sa ale budeme stretávať s aplikáciami (a už sme sa vlastne aj stretli), kedy nebudeme pred spustením presne vedieť, koľko pamäte budeme potrebovať. Spomeňte si na program, ktorý spriemeroval zadané hodnoty v poli. Na počet hodnôt sme sa používateľa opýtali až za behu programu. JVM teda musel za behu programu pole v pamäti založiť. V tomto prípade hovoríme o dynamickej správe pamäte.

V minulosti, hlavne v dobách jazykov C, Pascal a C++, sa na tento účel používali tzv. pointery, čiže priame ukazovatele do pamäte. Vo všeobecnosti to fungovalo tak, že sme operačný systém požiadali o kus pamäte o určitej veľkosti. On ju pre nás vyhradil a dal nám jej adresu. Na toto miesto v pamäti sme mali pointer, cez ktorý sme s pamäťou pracovali. Problém bol, že nikto nestrážil, čo do pamäte dávame (ukazovateľ smeroval na začiatok vyhradeného priestoru). Keď sme tam dali niečo väčšie, skrátka sa to aj tak uložilo a prepísali sa dáta za naším priestorom, ktoré patrili napríklad inému programu alebo operačnému systému (v tom prípade by našu aplikáciu OS asi zabil – zastavil). Často sme si však my v pamäti prepísali nejaké ďalšie dáta nášho programu a program sa začal správať chaoticky. Predstavte si, že si uložíte používateľa do poľa a v tej chvíli sa vám zrazu zmení farba používateľského prostredia, teda niečo, čo s tým vôbec nesúvisí. Hodiny strávite tým, že kontrolujete kód pre zmenu farby, potom zistíte, že je chyba v založení používateľa, kedy dôjde k pretečeniu pamäte a prepísaniu hodnôt farby.

Keď naopak nejaký objekt prestaneme používať, musíme po ňom miesto sami uvoľniť. Ak to neurobíme, pamäť zostane blokovaná. Pokiaľ toto robíme napr. v nejakej metóde a zabudneme pamäť uvoľňovať, naša aplikácia začne padať, prípadne zasekne celý operačný systém. Takáto chyba sa opäť zle hľadá, prečo program prestane po niekoľkých hodinách fungovať? Kde tú chybu v niekoľkých tisícoch riadkov kódu vôbec hľadať? Nemáme jedinú stopu, nemôžeme sa ničoho chytiť, musíme prejsť celý program riadok po riadku alebo začať preskúmavať pamäť počítača, ktorá je v binárke. Brrr. Podobný problém nastane, keď si niekde pamäť uvoľníme a následne pointer opäť použijeme (zabudneme, že je uvoľnený, to sa môže ľahko stať). Povedie niekam, kde je už uložené niečo iné a tieto dáta budú opäť prepísané. Povedie to k nekontrolovanému správaniu našej aplikácie a môže to dopadnúť aj takto:

Blue Screen Of Death – BSOD vo Windows - Objektovo orientované programovanie v Jave

Môj kolega raz hovoril: "Ľudský mozog sa nedokáže starať ani o správu vlastnej pamäte, nieto aby riešil memory management programu." Mal samozrejme pravdu, až na malú skupinu géniov ľudí prestalo baviť riešiť neustále a nezmyselné chyby. Za cenu mierneho zníženia výkonu vznikli riadené jazyky (managed) s tzv. garbage collectorom, jedným z nich je aj Java a C#. C++ sa samozrejme naďalej používa, ale iba na špecifické programy, napr. časti operačného systému alebo 3D enginy komerčných hier, kde je potrebné z počítača dostať maximálny výkon. Na 99 % všetkých ostatných aplikácií sa hodí Java, hlavne kvôli automatickej správe pamäte.

Garbage collector - Objektovo orientované programovanie v Jave

Garbage collector (ďalej iba GC) je vlastne program, ktorý beží paralelne s našou aplikáciou v samostatnom vlákne. Občas sa spustí a pozrie sa, na ktoré objekty už v pamäti nevedú žiadne referencie. Tie potom odstráni. Strata výkonu je minimálna a značne to zníži percento samovrážd programátorov ladiacich po večeroch rozbité pointery. Zapnutie GC môžeme dokonca ovplyvniť, aj keď to nie je v 99 % prípadov vôbec potrebné. Pretože je jazyk riadený a nepracujeme s priamymi pointermi, nie je vôbec možné pamäť nejako narušiť, nechať ju pretiecť a podobne, interpreter sa o pamäť automaticky stará.

Hodnota null

Posledná vec, ktorú si spomenieme, je tzv. hodnota null. Referenčné typy môžu, na rozdiel od primitívnych, nadobúdať špeciálne hodnoty a to null. Hodnota null je kľúčové slovo a označuje, že referencia neukazuje na žiadne dáta. Keď nastavíme premennú jack na null, zrušíme iba tú jednu referenciu. Ak na náš objekt existuje ešte nejaká referencia, bude aj naďalej existovať. Ak nie, bude uvoľnený GC. Zmeňme ešte posledné riadky nášho programu na:

        // change
        jack.name = "John Doe";
        jack = null;

Výstup:

Konzolová aplikácia
a: 56
b: 28
james: James Brown
jack: Jack White

a: 28
b: 28
james: Jack White
jack: Jack White

james: John Doe
jack: null

Vidíme, že objekt stále existuje a ukazuje naň premenná james, v premennej jack už nie je referencia. Hodnota null sa hojne využíva ako vo vnútri Javy, tak v databázach. K referenčným typom sa ešte raz vrátime.

V nasledujúcom kvíze, Kvíz - Úvod, konštruktory, metódy, dátové typy v Jave OOP, si vyskúšame nadobudnuté skúsenosti z predchádzajúcich lekcií.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 12x (2.68 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java

 

Predchádzajúci článok
Riešené úlohy k 3. lekcii OOP v Jave
Všetky články v sekcii
Objektovo orientované programovanie v Jave
Preskočiť článok
(neodporúčame)
Kvíz - Úvod, konštruktory, metódy, dátové typy v Jave OOP
Článok pre vás napísal David Hartinger
Avatar
Užívateľské hodnotenie:
41 hlasov
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David sa informačné technológie naučil na Unicorn University - prestížnej súkromnej vysokej škole IT a ekonómie.
Aktivity