Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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

Zopakujme si pre istotu ešte raz, č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ácie 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 - 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 uží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 u = new User("James Brown", 28);

Premenná u 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 - 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 na dvakrát, raz v zásobníku a raz v halde. V zásobníku je uložená iba tzv. referencie, teda odkaz do haldy, kde sa potom nachádza skutočný objekt.

Napr. v C++ je veľký rozdiel medzi pojmom ukazovateľ a referencie. 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 referencie 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 vzápätí ukážeme.
  3. Pomocou referencií môžeme jednoducho vytvárať štruktúry s dynamickou veľkosťou, napr. štruktúry podobné poli, 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 u = new User("James Brown", 28);
User v = 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 - Objektovo orientované programovanie v Jave

Teraz skúsme priradiť do premennej a premennú b. Rovnako tak priradíme aj premennú v do premennej u. Primitívny typ sa v zásobníku len skopíruje, pri objekte sa skopíruje iba referencie (č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 u = new User("James Brown", 28);
User v = new User("Jack White", 32);
a = b;
u = v;

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 - 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 na nasledujúce:

        // variable declaration
        int a = 56;
        int b = 28;
        User u = new User("James Brown", 28);
        User v = new User("Jack White", 32);
        System.out.printf("a: %s%nb: %s%nu: %s%nv: %s%n%n", a, b, u, v);
        // assignment
        a = b;
        u = v;
        System.out.printf("a: %s%nb: %s%nu: %s%nv: %s%n%n", a, b, u, v);

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

Konzolová aplikácia
a: 56
b: 28
u: James Brown
v: Jack White

a: 28
b: 28
u: Jack White
v: Jack White

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

        // change
        v.name = "John Doe";
        System.out.printf("u: %s%nv: %s%n", u, v);

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

Konzolová aplikácia
a: 56
b: 28
u: James Brown
v: Jack White

a: 28
b: 28
u: Jack White
v: Jack White

u: John Doe
v: John Doe

Spolu so zmenou premennej v sa zmení aj premenná u, 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 James Browna:

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

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

Garbage collector - Objektovo orientované programovanie v Jave - 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 ju 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 poľa 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. Napospol to fungovalo tak, že sme si povedali operačnému systému 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 av 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í uží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ť, pokiaľ 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ého niečo iné a tieto dáta budú opäť prepísané. Povedie to k nekontorolované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 - 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 - 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ánia. Strata výkonu je minimálna a značne to zníži percento samovrážd programátorov, ladiacich po večeroch rozbitých pointerov. Zapnutie GC môžeme dokonca z kódu 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, interpret sa o pamäť automaticky stará.

Hodnota null

Posledná vec, o ktorej sa zmienime, 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 referencie neukazuje na žiadne dáta. Keď nastavíme premennú v na null, zrušíme iba tú jednu referenciu. Ak na náš objekt existuje ešte nejaká referencie, bude aj naďalej existovať. Pokiaľ nie, bude uvoľnený GC. Zmeňme ešte posledné riadky nášho programu na:

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

Výstup:

Konzolová aplikácia
a: 56
b: 28
u: James Brown
v: Jack White

a: 28
b: 28
u: Jack White
v: Jack White

u: John Doe
v: null

Vidíme, že objekt stále existuje a ukazuje naň premenná u, v premennej v už nie je referencie. Hodnota null sa bohato 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é 0x (11.07 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:
3 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