Lekce 3 - Hrací kostka v Kotlin - Zapouzdření, konstruktor a Random
V předešlém cvičení, Řešené úlohy k 1.-2. lekci OOP v Kotlin, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
V dnešním tutoriálu začneme pracovat na slíbené aréně, ve které budou proti sobě bojovat dva bojovníci. Boj bude tahový (na přeskáčku) a bojovník vždy druhému ubere život na základě síly jeho útoku a obrany druhého bojovníka. Simulujeme v podstatě stolní hru, budeme tedy simulovat i hrací kostku, která dodá hře prvek náhodnosti. Začněme zvolna a vytvořme si dnes právě tuto hrací kostku. Zároveň se naučíme jak definovat vlastní konstruktor.
Vytvoření projektu
Vytvořme si nový projekt, pojmenujme ho TahovyBoj
a vytvořme
si všechny potřebné součásti jako package
a soubor
Main.kt
, který bude obsahovat naši main()
metodu. K
projektu si přidejme novou class
s názvem Kostka
.
Zamysleme se nad atributy, které kostce dáme. Jistě by se hodilo, kdybychom
si mohli zvolit počet stěn kostky (klasicky 6 nebo 10 stěn, jak je zvykem u
tohoto typu her). Dále bude kostka potřebovat generovat náhodná čísla.
Toho docílíme pomocí rozsahu a metody shuffled()
. Naše třída
bude mít nyní jen 1 atribut:
- pocetSten typu
int
Zapouzdření
Minule jsme kvůli jednoduchosti nastavovali všechny atributy naší třídy
jako public
, tedy jako veřejně přístupné. Většinou se však
spíše nechce, aby se daly zvenčí modifikovat a používá se modifikátor
private
. Atribut je poté viditelný jen uvnitř třídy a zvenčí
se Kotlin tváří, že vůbec neexistuje. Při návrhu třídy tedy nastavíme
vše na private
a v případě, že něco bude opravdu potřeba
vystavit, použijeme public
. V Kotlin se v případě
public
modifikátor spíše vynechává, jelikož je výchozí.
Naše třída nyní vypadá asi takto:
class Kostka { /** Počet stěn kostky */ private val pocetSten: Int }
Kód se nám nyní podtrhne červeně a nebude se chtít
zkompilovat. Kotlinu se totiž nelíbí, že jsme si založili nový
atribut bez nastavené hodnoty. To se vyřeší po tom, co si napíšeme
konstruktor a atribut pocetSten
inicializujeme.
Konstruktory
Až doposud jsme neuměli zvenčí nastavit jiné atributy než
public
, protože např. private
nejsou zvenčí
viditelné. Již jsme si říkali něco málo o konstruktoru objektu. Je to
metoda, která se zavolá ve chvíli vytvoření instance
objektu. Slouží samozřejmě k nastavení vnitřního stavu objektu a
k provedení případné inicializace. Kostku bychom nyní v
Main.kt
vytvořili takto:
fun main(args: Array<String>) { val kostka = Kostka() // v tuto chvíli se zavolá konstruktor }
Právě Kostka()
je konstruktor. Protože v naší třídě
žádný není, Kotlin si dogeneruje prázdnou metodu. My si však nyní
konstruktor do třídy Kostka
přidáme. Tělo konstruktoru se
deklaruje klíčovým slovíčkem init
. V konstruktoru nastavíme
počet stěn na pevnou hodnotu. Konstruktor bude vypadat následovně:
init { pocetSten = 6 }
Nyní naše aplikace již půjde přeložit.
Pokud kostku vytvoříme, bude mít v atributu pocetSten
hodnotu
6
. Vypišme si počet stěn do konzole, ať vidíme, že tam
hodnota opravdu je. Abychom hodnotu mohli zvenčí přečíst, nastavíme
modifikátor atributu pocetSten
na public
, resp.
modifikátor přístupu odstraníme. Že jsme tím porušili zapouzdření? Ale
ne. Máme atribut, který se nedá měnit (je imutabilní), díky slovíčku
val
. Jeho zveřejněním jsme v podstatě pouze docílili toho, že
je atribut read-only (atribut je viditelný a lze ho číst, nelze jej ovšem
již měnit). Tento princip samozřejmě platí pouze v případě, když
atribut poprvé sami inicializujeme v konstruktoru. Kotlin má ještě další
konstrukce, které se k tomuto účelu dají použít, ale tím se zatím
nebudeme zabývat. Deklarace atributu bude tedy nyní vypadat takto:
val pocetSten: Int
Přesuňme se do Main.kt
a zkusme si vytvořit kostku a vypsat
počet stěn:
{KOTLIN_OOP} fun main(args: Array<String>) { val kostka = Kostka() // v tuto chvíli se zavolá konstruktor println(kostka.pocetSten) } {/KOTLIN_OOP}
{KOTLIN_OOP} class Kostka { val pocetSten: Int init { pocetSten = 6 } } {/KOTLIN_OOP}
Výstup:
6
Vidíme, že se konstruktor opravdu zavolal. My bychom ale chtěli, abychom mohli u každé kostky při vytvoření specifikovat, kolik stěn budeme potřebovat. Dáme tedy konstruktoru parametr. Parametry konstruktoru se v Kotlinu píší za název třídy do závorek, stejně jako kdyby to byly parametry funkce nebo metody:
class Kostka(aPocetSten: Int) { val pocetSten: Int init { pocetSten = aPocetSten } }
Všimněte si, že jsme před název parametru konstruktoru přidali znak
a
, protože jinak by měl stejný název jako atribut
pocetSten
a Kotlin by to zmátlo. Vraťme se k Main.kt
a zadejme tento parametr do konstruktoru:
{KOTLIN_OOP} fun main(args: Array<String>) { val kostka = Kostka(10) // v tuto chvíli se zavolá konstruktor println(kostka.pocetSten) } {/KOTLIN_OOP}
{KOTLIN_OOP} class Kostka(aPocetSten: Int) { val pocetSten: Int init { pocetSten = aPocetSten } } {/KOTLIN_OOP}
Výstup:
10
Vše funguje, jak jsme očekávali. Kotlin nám již v tuto chvíli
nevygeneruje prázdný (tzv. bezparametrický konstruktor), takže kostku bez
parametru vytvořit nelze. My to však můžeme umožnit, vytvořme si další
konstruktor a tentokrát bez parametru. V něm zavoláme náš parametrický
konstruktor a předáme mu hodnotu 6
, protože takovou hodnotu asi
uživatel naší třídy u kostky očekává jako výchozí:
constructor() : this(6)
Slovíčkem this
voláme náš parametrický
konstruktor a ještě si jej níže podrobněji vysvětlíme.
Zkusme si nyní vytvořit 2 instance kostky, každou jiným konstruktorem
(opět v Main.kt
):
{KOTLIN_OOP} fun main(args: Array<String>) { val sestistenna = Kostka() val desetistenna = Kostka(10) println(sestistenna.pocetSten) println(desetistenna.pocetSten) } {/KOTLIN_OOP}
{KOTLIN_OOP} class Kostka(aPocetSten: Int) { val pocetSten: Int constructor() : this(6) init { pocetSten = aPocetSten } } {/KOTLIN_OOP}
Výstup:
6 10
Kotlinu nevadí, že máme 2 konstruktory, protože jejich parametry jsou různé. Hovoříme o tom, že konstruktor má přetížení (overload). Toho můžeme využívat i u metod, nejen u konstruktorů. IntelliJ nám přehledně nabízí všechny přetížení konstruktoru nebo metody ve chvíli, kdy zadáme její název. V nabídce vidíme naše 2 konstruktory:
Stejného chování by se dalo docílit pomocí
výchozích argumentů
, ale o tom později.

Mnoho metod v Kotlinu má hned několik přetížení, zkuste se podívat
např. na metodu trim()
na třídě String
. Je dobré
si u metod projít jejich přetížení, abyste neprogramovali něco, co již
někdo udělal před vámi.
Ukážeme si ještě, jak jde obejít nepraktický název atributu u
parametrického konstruktoru (v našem případě aPocetSten
) a
potom konstruktory pomalu opustíme. Problém je samozřejmě v tom, že když
napíšeme:
class Kostka(pocetSten: Int) { init { pocetSten = pocetSten } }
Kotlin neví, kterou z proměnných myslíme, jestli parametr nebo atribut. V
tomto případě přiřazujeme do parametru znovu ten samý parametr. Uvnitř
třídy se máme možnost odkazovat na její instanci, je uložena v proměnné
this
. Využití si můžeme představit např. kdyby kostka měla
metodu dejHraci(hrac: Hrac)
a tam by volala
hrac.seberKostku(this)
. Zde bychom hráči pomocí
this
předali sebe sama, tedy tu konkrétní kostku, se kterou
pracujeme. My se tím zde nebudeme zatěžovat, ale využijeme odkazu na
instanci při nastavování atributu:
class Kostka(pocetSten: Int) { init { this.pocetSten = pocetSten } }
Pomocí this
jsme specifikovali, že levá proměnná
pocetSten
náleží instanci, pravou Kotlin chápe jako z
parametru. Máme tedy 2 konstruktory, které nám umožňují tvořit různé
hrací kostky.
Protože může být poněkud zdlouhavé psát třídu s více atributy a
tyto parametry poté nastavovat pomocí parametrů konstruktoru, umožňuje
Kotlin zkrácený zápis konstruktoru. Atribut definujeme již v parametru
konstruktoru tím, že přidáme klíčové slovo val
nebo
var
před jeho název. Kotlin tím pozná, že chceme vytvořit
atribut třídy se stejným názvem jako má parametr konstruktoru. V našem
případě by náš kód vypadal takto:
class Kostka(val pocetSten: Int) { // Náš atribut jsme definovali již v parametru konstruktoru constructor() : this(6) }
Náhodná čísla
Definujme na kostce metodu hod()
, která nám vrátí náhodné
číslo od 1
do počtu stěn. Je to velmi jednoduché, metoda bude
public
(půjde volat zvenčí) a nebude mít žádný parametr.
Návratová hodnota bude typu Int
. Náhodné číslo získáme tak,
že si vygenerujeme rozsah s prvky od 1
do hodnoty proměnné
pocetSten
, zamícháme jej a vybereme z něj první prvek.
Vrácení zamíchaného rozsahu (stejně jako pole) provedeme metodou
shuffled()
. Rozsah si vytvoříme stejným způsobem jak jsme to
byli zvyklí dělat ve for
cyklech:
fun hod(): Int { return (1..pocetSten).shuffled().first() }
Překrývání metody toString()
Kostka je téměř hotová, ukažme si ještě jednu užitečnou metodu,
kterou ji přidáme a kterou budeme hojně používat i ve většině našich
dalších objektů. Řeč je o metodě toString()
, o které jsme se
již zmínili a kterou obsahuje každý objekt, tedy i nyní
naše kostka. Metoda je určena k tomu, aby vrátila tzv. textovou
reprezentaci instance. Hodí se ve všech případech, kdy si instanci
potřebujeme vypsat nebo s ní pracovat jako s textem. Tuto metodu mají např.
i čísla.
Již víme, že v Kotlinu funguje implicitní konverze, jakmile tedy budeme
chtít do konzole vypsat objekt, Kotlin na něm zavolá metodu
toString()
a vypíše její výstup. Pokud si vytváříme vlastní
třídu, měli bychom zvážit, zda se nám takováto metoda nehodí. Nikdy
bychom si neměli vymýšlet vlastní metodu, např. něco jako
vypis()
, když máme v Kotlinu připravenou cestu, jak toto
řešit. U kostky nemá toString()
vyšší smysl, ale u bojovníka
bude jistě vracet jeho jméno. My si ji ke kostce stejně přidáme, bude
vypisovat, že se jedná o kostku a vrátí i počet stěn. Nejprve si zkusme
vypsat do konzole naši instanci kostky:
{KOTLIN_OOP} fun main(args: Array<String>) { val sestistenna = Kostka() val desetistenna = Kostka(10) println(sestistenna) println(desetistenna) } {/KOTLIN_OOP}
{KOTLIN_OOP} class Kostka(pocetSten: Int) { val pocetSten: Int constructor() : this(6) init { this.pocetSten = pocetSten } fun hod(): Int { return (1..pocetSten).shuffled().first() } } {/KOTLIN_OOP}
Do konzole se vypíše pouze cesta k naší třídě, tedy
cz.itnetwork.Kostka
a tzv. hash kód objektu. V mém případě byl
vypsán tento řetězec:
cz.itnetwork.Kostka@1b6d3586 cz.itnetwork.Kostka@4554617c
Metodu již jednoduše nedefinujeme, ale protože již existuje, musíme ji
přepsat, resp. překrýt. Tím se opět nebudeme nyní
podrobně zabývat, nicméně chci, abychom již teď uměli
toString()
používat. Pro překrytí označíme metodu slovíčkem
override
:
V Kotlinu existuje tzv. datová třída, která nám později vygeneruje tuto a další 2 metody. Budeme se o nich bavit v dalších lekcích.
{KOTLIN_OOP} class Kostka(pocetSten: Int) { val pocetSten: Int constructor() : this(6) init { this.pocetSten = pocetSten } fun hod(): Int { return (1..pocetSten).shuffled().first() } override fun toString(): String { return "Kostka s $pocetSten stěnami" } } {/KOTLIN_OOP}
{KOTLIN_OOP} fun main(args: Array<String>) { val sestistenna = Kostka() val desetistenna = Kostka(10) println(sestistenna) println(desetistenna) } {/KOTLIN_OOP}
Nyní opět zkusíme do konzole vypsat přímo instanci kostky.
Výstup:
Kostka s 6 stěnami
Ještě si naše kostky vyzkoušíme. Zkusíme si v programu s našima dvěma kostkama v cyklech házet a podíváme se, jestli fungují tak, jak se očekává:
{KOTLIN_OOP} fun main(args: Array<String>) { // vytvoření val sestistenna = Kostka() val desetistenna = Kostka(10) // hod šestistěnnou println(sestistenna) for (i in 0..9) { print(sestistenna.hod().toString() + " ") } // hod desetistěnnou println("\n\n" + desetistenna) for (i in 0..9) { print(desetistenna.hod().toString() + " ") } } {/KOTLIN_OOP}
{KOTLIN_OOP} class Kostka(pocetSten: Int) { val pocetSten: Int constructor() : this(6) init { this.pocetSten = pocetSten } fun hod(): Int { return (1..pocetSten).shuffled().first() } override fun toString(): String { return "Kostka s $pocetSten stěnami" } } {/KOTLIN_OOP}
Výstup může vypadat nějak takto:
Kostka s 6 stěnami 3 6 6 1 6 3 6 2 6 3 Kostka s 10 stěnami 5 9 9 2 10 4 9 3 10 5
Máme hotovou docela hezkou a nastavitelnou třídu, která reprezentuje hrací kostku. Bude se nám hodit v naší aréně, ale můžete ji použít i kdekoli jinde. Vidíme, jak OOP umožňuje znovupoužívat komponenty.
V následujícím cvičení, Řešené úlohy k 3. lekci OOP v Kotlin, si procvičí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 66x (8.81 kB)
Aplikace je včetně zdrojových kódů v jazyce Kotlin