Pouze tento týden sleva až 80 % na e-learning týkající se C# .NET. Zároveň využij akce až 50 % zdarma při nákupu e-learningu. 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í.
Slevovy týden 3/50

Lekce 4 - Referenční a primitivní datové typy

V předešlém cvičení, Řešené úlohy k 3. lekci OOP v Javě, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

Začínáme pracovat s objekty a objekty jsou referenčními datovými typy, které se v některých ohledech chovají jinak, než typy primitivní (např. int). Je důležité, abychom přesně věděli, co se uvnitř programu děje, jinak by nás v budoucnu mohlo leccos překvapit.

Zopakujme si pro jistotu ještě jednou, co jsou to primitivní typy. Obecně jsou to jednoduché struktury, např. jedno číslo, jeden znak. Většinou se chce, abychom s nimi pracovali co nejrychleji, v programu se jich vyskytuje velmi mnoho a zabírají málo místa. V anglické literatuře jsou často popisovány slovy light-weight. Mají pevnou velikost. Příkladem jsou např. int, float, double, char, boolean a další.

Aplikace (resp. její vlákno) má operačním systémem přidělenou paměť v podobě tzv. zásobníku (stack). Jedná se o velmi rychlou paměť s přímým přístupem, její velikost aplikace nemůže ovlivnit, prostředky jsou přidělovány operačním systémem. Tato malá a rychlá paměť je využívána k ukládání lokálních proměnných primitivního typu (až na výjimky při iteracích, kterými se nebudeme zabývat). Proměnnou si v ní můžeme představit asi takto:

Zásobník vb paměti počítače

Na obrázku je znázorněna paměť, kterou může naše aplikace využívat. V aplikaci jsme si vytvořili proměnnou a typu int. Její hodnota je 56 a uložila se nám přímo do zásobníku. Kód by mohl vypadat takto:

int a = 56;

Můžeme to chápat tak, že proměnná a má přidělenu část paměti v zásobníku (velikosti datového typu int, tedy 32 bitů), ve které je uložena hodnota 56.

Vytvořme si novou konzolovou aplikaci a přidejme si k ní jednoduchou třídu, která bude reprezentovat uživatele nějakého systému. Pro názornost vypustím komentáře a nebudu řešit viditelnosti:

public class Uzivatel {
    public int vek;
    public String jmeno;

    public Uzivatel(String jmeno, int vek) {
        this.jmeno = jmeno;
        this.vek = vek;
    }

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

Třída má 2 jednoduché veřejné atributy, konstruktor a přetížený toString(), abychom uživatele mohli jednoduše vypisovat. Do našeho původního programu přidejme vytvoření instance této třídy:

int a = 56;
Uzivatel u = new Uzivatel("Jan Novák", 28);

Proměnná u je nyní referenčního typu. Podívejme se na novou situaci v paměti:

Zásobník a halda v paměti počítače

Vidíme, že objekt (proměnná referenčního datového typu) se již neukládá do zásobníku, ale do paměti zvané halda. Je to z toho důvodu, že objekt je zpravidla složitější než primitivní datový typ (většinou obsahuje hned několik dalších atributů) a také zabírá více místa v paměti.

Zásobník i halda se nacházejí v paměti RAM. Rozdíl je v přístupu a velikosti. Halda je prakticky neomezená paměť, ke které je však přístup složitější a tím pádem pomalejší. Naopak zásobník je paměť rychlá, ale velikostně omezená.

Proměnné referenčního typu jsou v paměti uloženy vlastně nadvakrát, jednou v zásobníku a jednou v haldě. V zásobníku je uložena pouze tzv. reference, tedy odkaz do haldy, kde se poté nalézá opravdový objekt.

Pozn.: Např. v C++ je velký rozdíl mezi pojmem ukazatel a reference. Java žádné ukazatele naštěstí nemá a používá termín reference, ty se paradoxně principem podobají spíše ukazatelům v C++. Pojmy ukazatel a reference zde zmíněné tedy znamenají referenci ve smyslu Javy a nemají s C++ nic společného.

Můžete se ptát, proč je to takto udělané. Důvodů je hned několik, pojďme si některé vyjmenovat:

  1. Místo ve stacku je omezené.
  2. Když budeme chtít použít objekt vícekrát (např. ho předat jako parametr do několika metod), nemusíme ho v programu předávat jako kopii. Předáme pouze malý primitivní typ s referencí na objekt místo toho, abychom obecně paměťově náročný objekt kopírovali. Toto si vzápětí ukážeme.
  3. Pomocí referencí můžeme jednoduše vytvářet struktury s dynamickou velikostí, např. struktury podobné poli, do kterých můžeme za běhu vkládat nové prvky. Ty jsou na sebe navzájem odkazovány referencemi, jako řetěz objektů.

Založme si 2 proměnné typu int a 2 proměnné typu Uzivatel:

int a = 56;
int b = 28;
Uzivatel u = new Uzivatel("Jan Novák", 28);
Uzivatel v = new Uzivatel("Josef Nový", 32);

Situace v paměti bude následující:

Referenční hodnoty v Javě v paměti počítače

Nyní zkusme přiřadit do proměnné a proměnnou b. Stejně tak přiřadíme i proměnnou v do proměnné u. Primitivní typ se v zásobníku jen zkopíruje, u objektu se zkopíruje pouze reference (což je vlastně také primitivní typ), ale objekt máme stále jen jeden. V kódu vykonáme tedy toto:

int a = 56;
int b = 28;
Uzivatel u = new Uzivatel("Jan Novák", 28);
Uzivatel v = new Uzivatel("Josef Nový", 32);
a = b;
u = v;

V paměti bude celá situace vypadat následovně:

Referenční hodnoty v Javě v paměti počítače

Přesvědčme se o tom, abyste viděli, že to opravdu tak je :) Nejprve si necháme všechny čtyři proměnné vypsat před a po změně. Protože budeme výpis volat vícekrát, napíši ho poněkud úsporněji. Mohli bychom dát výpis do metody, ale ještě nevíme, jak deklarovat metody přímo v souboru s metodou main (v tomto případě soubor Program.java) a zpravidla se to ani moc nedělá, pro vážnější práci bychom si měli udělat třídu. Upravme tedy kód na následující:

    // založení proměnných
    int a = 56;
    int b = 28;
    Uzivatel u = new Uzivatel("Jan Novák", 28);
    Uzivatel v = new Uzivatel("Josef Nový", 32);
    System.out.printf("a: %s\nb: %s\nu: %s\nv: %s\n\n", a, b, u, v);
    // přiřazování
    a = b;
    u = v;
    System.out.printf("a: %s\nb: %s\nu: %s\nv: %s\n\n", a, b, u, v);

Na výstupu programu zatím rozdíl mezi primitivním a referenčním typem nepoznáme:

Konzolová aplikace
a: 56
b: 28
u: Jan Novák
v: Josef Nový

a: 28
b: 28
u: Josef Nový
v: Josef Nový

Nicméně víme, že zatímco v a a b jsou opravdu 2 různá čísla se stejnou hodnotou, v u a v je ten samý objekt. Pojďme změnit jméno uživatele v a dle našich předpokladů by se měla změna projevit i v proměnné u. K programu připíšeme:

    // změna
    v.jmeno = "John Doe";
    System.out.printf("u: %s\nv: %s\n", u, v);

Změnili jsme objekt v proměnné v a znovu vypíšeme u a v:

Konzolová aplikace
a: 56
b: 28
u: Jan Novák
v: Josef Nový

a: 28
b: 28
u: Josef Nový
v: Josef Nový

u: John Doe
v: John Doe

Spolu se změnou v se změní i u, protože proměnné ukazují na ten samý objekt. Jestli se ptáte, jak vytvořit opravdovou kopii objektu, tak nejjednodušší je objekt znovu vytvořit pomocí konstruktoru a dát do něj stejná data. Dále můžeme použít klonování, ale o tom zas až někdy jindy. Připomeňme si situaci v paměti ještě jednou a zaměřme se na Jana Nováka.

Referenční hodnoty v Javě v paměti počítače

Co se sním stane? "Sežere" ho tzv. Garbage collector.

Garbage collector

Garbage collector a dynamická správa paměti

Paměť můžeme v programech alokovat staticky, to znamená, že ve zdrojovém kódu předem určíme, kolik jí budeme používat. Doposud jsme to tak vlastně dělali a neměli jsme s tím problém, hezky jsme do zdrojového kódu napsali potřebné proměnné. Brzy se ale budeme setkávat s aplikacemi (a už jsme se vlastně i setkali), kdy nebudeme před spuštěním přesně vědět, kolik paměti budeme potřebovat. Vzpomeňte si na program, který zprůměroval zadané hodnoty v poli. Na počet hodnot jsme se uživatele zeptali až za běhu programu. JVM (pojem viz 1. lekce) tedy musel za běhu programu pole v paměti založit. V tomto případě hovoříme o dynamické správě paměti.

V minulosti, hlavně v dobách jazyků C, Pascal a C++, se k tomuto účelu používaly tzv. pointery, neboli přímé ukazatele do paměti. Vesměs to fungovalo tak, že jsme si řekli operačnímu systému o kus paměti o určité velikosti. On ji pro nás vyhradil a dal nám její adresu. Na toto místo v paměti jsme měli pointer, přes který jsme s pamětí pracovali. Problém byl, že nikdo nehlídal, co do paměti dáváme (ukazatel směřoval na začátek vyhrazeného prostoru). Když jsme tam dali něco většího, zkrátka se to stejně uložilo a přepsala se data za naším prostorem, která patřila třeba jinému programu nebo operačnímu systému (v tom případě by naši aplikaci OS asi zabil - zastavil). Často jsme si však my v paměti přepsali nějaká další data našeho programu a program se začal chovat chaoticky. Představte si, že si uložíte uživatele do pole a v tu chvíli se vám najednou změní barva uživatelského prostředí, tedy něco, co s tím vůbec nesouvisí. Hodiny strávíte tím, že kontrolujete kód pro změnu barvy, poté zjistíte, že je chyba v založení uživatele, kdy dojde k přetečení paměti a přepsání hodnot barvy. Když naopak nějaký objekt přestaneme používat, musíme po něm místo sami uvolnit, pokud to neuděláme, paměť zůstane blokovaná. Pokud toto děláme např. v nějaké metodě a zapomeneme paměť uvolňovat, naše aplikace začne padat, případně zasekne celý operační systém. Taková chyba se opět špatně hledá, proč program přestane po několika hodinách fungovat? Kde tu chybu v několika tisících řádků kódu vůbec hledat? Nemáme jedinou stopu, nemůžeme se ničeho chytit, musíme projet celý program řádek po řádku nebo začít prozkoumávat paměť počítače, která je v binárce. Brrr. Podobný problém nastane, když si někde paměť uvolníme a následně pointer opět použijeme (zapomeneme, že je uvolněný, to se může lehce stát), povede někam, kde je již uloženého něco jiného a tato data budou opět přepsána. Povede to k nekontorolovanému chování naší aplikace a může to dopadnout i takto:

Blue Screen Of Death – BSOD ve Windows

Můj kolega jednou pravil: "Lidský mozek se nedokáže starat ani o správu paměti vlastní, natož aby řešil memory management programu." Měl samozřejmě pravdu, až na malou skupinu géniů lidi přestalo bavit řešit neustálé a nesmyslné chyby. Za cenu mírného snížení výkonu vznikly řízené jazyky (managed) s tzv. garbage collectorem, jedním z nich je i Java a C#. C++ se samozřejmě nadále používá, ale pouze na specifické programy, např. části operačního systému nebo 3D enginy komerčních her, kde je potřeba z počítače dostat maximální výkon. Na 99% všech ostatních aplikací se hodí Java, hlavně kvůli automatické správě paměti.

Garbage collector

Garbage collector je vlastně program, který běží paralelně s naší aplikací, v samostatném vlákně. Občas se spustí a podívá se, na které objekty již v paměti nevedou žádné reference. Ty potom odstraní. Ztráta výkonu je minimální a značně to sníží procento sebevražd programátorů, ladících po večerech rozbité pointery. Zapnutí GC můžeme dokonce z kódu ovlivnit, i když to není v 99% případů vůbec potřeba. Protože je jazyk řízený a nepracujeme s přímými pointery, není vůbec možné paměť nějak narušit, nechat ji přetéct a podobně, interpret se o paměť automaticky stará.

Hodnota null

Poslední věc, o které se zmíníme, je tzv. hodnota null. Referenční typy mohou, na rozdíl od primitivních, nabývat speciální hodnoty a to null. Null je klíčové slovo a označuje, že reference neukazuje na žádná data. Když nastavíme proměnnou v na null, zrušíme pouze tu jednu referenci. Pokud na náš objekt existuje ještě nějaká reference, bude i nadále existovat. Pokud ne, bude uvolněn GC. Změňme ještě poslední řádky našeho programu na:

    // změna
    v.jmeno = "John Doe";
    v = null;

Výstup:

Konzolová aplikace
a: 56
b: 28
u: Jan Novák
v: Josef Nový

a: 28
b: 28
u: Josef Nový
v: Josef Nový

u: John Doe
v: null

Vidíme, že objekt stále existuje a ukazuje na něj proměnná u, v proměnné v již není reference. Null se bohatě využívá jak uvnitř Javy, tak v databázích. K referenčním typům se ještě jednou vrátíme.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 576x (2.89 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

Předchozí článek
Řešené úlohy k 3. lekci OOP v Javě
Všechny články v sekci
Objektově orientované programování v Javě
Přeskočit článek
(nedoporučujeme)
Kvíz - Úvod, konstruktory, metody, datové typy v Javě OOP
Článek pro vás napsal David Čápka
Avatar
Uživatelské hodnocení:
160 hlasů
David je zakladatelem ITnetwork a programování se profesionálně věnuje 13 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity