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

Lekce 4 - Referenční datové typy v Kotlin

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

Všechny datové typy v Kotlin jsou tzv. referenční. Až doposud nás to netrápilo, protože jsme pracovali jen s čísly nebo texty, což byly sice také objekty, ale měly vždy pouze jednu hodnotu. Co se nyní změnilo je, že přes jednu proměnnou přistupujeme hned k několika atributům a dokonce můžeme přes různé proměnné přistupovat k tomu samému objektu. 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.

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. Do této paměti Kotlin ukládá pouze reference na naše objekty. Druhá část paměti, kde jsou tyto objekty skutečně uložené, se nazývá halda. Práci s referencemi si dnes vyzkoušíme.

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:

class Uzivatel(var jmeno: String, var vek: Int) {
    override fun toString(): String {
        return jmeno
    }
}

Třída má 2 veřejné atributy, konstruktor a přepsanou metodu toString(), abychom uživatele mohli jednoduše vypisovat. Do funkce main() v hlavním souboru přidejme vytvoření instance této třídy:

val u: Uzivatel? = Uzivatel("Jan Novák", 28)

Proměnnou u jsme rovnou také deklarovali jako nullovatelnou, abychom si referenci mohli zkusit i vyprázdnit. Podívejme se na aktuální situaci v paměti:

Zásobník a halda v paměti počítače - Objektově orientované programování v Kotlin

Všechny proměnné Kotlin ukládá do paměti 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.

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

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ý 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ů.

Např. v C++ je velký rozdíl mezi pojmem ukazatel a reference. Kotlin žá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 Kotlin a nemají s C++ nic společného.

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

var u: Uzivatel? = Uzivatel("Jan Novák", 28)
var v: Uzivatel? = Uzivatel("Josef Nový", 32)

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

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

Nyní zkusme přiřadit do proměnné u proměnnou v. Zkopíruje se pouze odkaz na objekt, ale objekt máme stále jen jeden. V kódu vykonáme tedy toto:

var u = Uzivatel("Jan Novák", 28)
var v = Uzivatel("Josef Nový", 32)
u = v

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

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

Přesvědčme se o tom, abyste viděli, že to opravdu tak je :) Nejprve si necháme obě dvě proměnné vypsat před a po změně. Navíc si vypíšeme i tzv. hash kód přes metodu hashCode(), abychom viděli, že se jedná o různé objekty. Protože budeme výpis volat vícekrát, napíši ho poněkud úsporněji. Upravme tedy kód na následující:

// založení proměnných
var u = Uzivatel("Jan Novák", 28)
var v = Uzivatel("Josef Nový", 32)
println("u: $u ${u.hashCode()} ")
println("v: $v ${v.hashCode()}\n")
// přiřazení
u = v
println("u: $u ${u.hashCode()} ")
println("v: $v ${v.hashCode()}\n")
class Uzivatel(var jmeno: String, var vek: Int) {
    override fun toString(): String {
        return jmeno
    }
}

Výstup programu:

u: Jan Novák 460141958
v: Josef Nový 1163157884

u: Josef Nový 1163157884
v: Josef Nový 1163157884

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:

v.jmeno = "John Doe"
println("u: $u ${u.hashCode()} ")
println("v: $v ${v.hashCode()}\n")
class Uzivatel(var jmeno: String, var vek: Int) {
    override fun toString(): String {
        return jmeno
    }
}

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

u: Jan Novák 460141958
v: Josef Nový 1163157884

u: Josef Nový 1163157884
v: Josef Nový 1163157884

u: John Doe 1163157884
v: John Doe 1163157884

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

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

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

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. Ale také nemusíme určit kolik paměti budeme potřebovat. 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ž jsme naopak nějaký objekt přestali používat, museli jsme po něm místo sami uvolnit. Pokud jsme to neudělali, paměť zůstala 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 nekontrolovanému chování naší aplikace a může to dopadnout i takto:

Blue Screen Of Death – BSOD ve Windows - Objektově orientované programování v Kotlin

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 Kotlin a Swift. 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í Kotlin, hlavně kvůli automatické správě paměti.

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

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

Z lekce Typový systém: Null safety v Kotlin byste již měli alespoň pasivně znát klíčové slovo null. Proměnné v Kotlin, které jsou nullovatelné, mohou nabývat navíc speciální hodnoty null. null označuje, že reference neukazuje na žádná data. Když nastavíme proměnnou v na null, zrušíme pouze 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:

v = null
println("u: $u ${u?.hashCode()} ")
println("v: $v ${v?.hashCode()}\n")

Výstup programu:

u: Jan Novák 666988784
v: Josef Nový 1414644648

u: Josef Nový 1414644648
v: Josef Nový 1414644648

u: John Doe 1414644648
v: John Doe 1414644648

u: John Doe 1414644648
v: null 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 občas využívá jak uvnitř Kotlinu (i když může způsobit chyby), 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 Kotlin 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 26x (9.6 kB)
Aplikace je včetně zdrojových kódů v jazyce Kotlin

 

Předchozí článek
Řešené úlohy k 3. lekci OOP v Kotlin
Všechny články v sekci
Objektově orientované programování v Kotlin
Přeskočit článek
(nedoporučujeme)
Kvíz - Úvod, konstruktory, metody, datové typy v Kotlin OOP
Článek pro vás napsal Samuel Kodytek
Avatar
Uživatelské hodnocení:
23 hlasů
Autor se věnuje všem jazykům okolo JVM. Rád pomáhá lidem, kteří se zajímají o programování. Věří, že všichni mají šanci se naučit programovat, jen je potřeba prorazit tu bariéru, který se říká lenost.
Aktivity