IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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í.

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 paměti počítače - Objektově orientované programování v Javě

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 s názvem například ReferencniTypy 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á dva jednoduché veřejné atributy, konstruktor a přepsanou metodu toString(), abychom uživatele mohli jednoduše vypisovat. Do našeho původního programu (metoda main()) přidejme vytvoření instance této třídy:

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

Proměnná jan 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 - Objektově orientované programování v Javě

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.

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 dvě proměnné typu int a dvě proměnné typu Uzivatel:

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

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

Referenční hodnoty v Javě v paměti počítače - Objektově orientované programování v Javě

Nyní zkusme přiřadit do proměnné a proměnnou b. Stejně tak přiřadíme i proměnnou josef do proměnné jan. 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 jan = new Uzivatel("Jan Novák", 28);
Uzivatel josef = new Uzivatel("Josef Nový", 32);
a = b;
jan = josef;

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

Referenční hodnoty v Javě v paměti počítače - Objektově orientované programování v Javě

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 ReferencniTypy.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 jan = new Uzivatel("Jan Novák", 28);
        Uzivatel josef = new Uzivatel("Josef Nový", 32);
        System.out.printf("a: %s%nb: %s%njan: %s%njosef: %s%n%n", a, b, jan, josef);
        // přiřazování
        a = b;
        jan = josef;
        System.out.printf("a: %s%nb: %s%njan: %s%njosef: %s%n%n", a, b, jan, josef);

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

Konzolová aplikace
a: 56
b: 28
jan: Jan Novák
josef: Josef Nový

a: 28
b: 28
jan: Josef Nový
josef: Josef Nový

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

        // změna
        josef.jmeno = "John Doe";
        System.out.printf("jan: %s%njosef: %s%n", jan, josef);

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

Konzolová aplikace
a: 56
b: 28
jan: Jan Novák
josef: Josef Nový

a: 28
b: 28
jan: Josef Nový
josef: Josef Nový

jan: John Doe
josef: John Doe

Spolu se změnou proměnné josef se změní i proměnná jan, 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 - Objektově orientované programování v Javě

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

Garbage collector - Objektově orientované programování v Javě

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 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 - Objektově orientované programování v Javě

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 - Objektově orientované programování v Javě

Garbage collector (dále pouze GC) 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. Hodnota null je klíčové slovo a označuje, že reference neukazuje na žádná data. Když nastavíme proměnnou josef 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
        josef.jmeno = "John Doe";
        josef = null;

Výstup:

Konzolová aplikace
a: 56
b: 28
jan: Jan Novák
josef: Josef Nový

a: 28
b: 28
jan: Josef Nový
josef: Josef Nový

jan: John Doe
josef: null

Vidíme, že objekt stále existuje a ukazuje na něj proměnná jan, v proměnné josef již není reference. Hodnota 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.

V následujícím kvízu, Kvíz - Úvod, konstruktory, metody, datové typy v Javě OOP, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.


 

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 705x (11.88 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 Hartinger
Avatar
Uživatelské hodnocení:
509 hlasů
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 se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity