Programovanie (2) v Jave
1-INF-166, letný semester 2023/24

Prednášky · Pravidlá · Softvér · Testovač
· Vyučujúcich predmetu možno kontaktovať mailom na adresách uvedených na hlavnej stránke. Hromadná mailová adresa zo zimného semestra v letnom semestri nefunguje.
· JavaFX: cesta k adresáru lib je v počítačových učebniach /usr/share/openjfx/lib.


Letný semester, prednáška č. 2: Rozdiel medzi revíziami

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
Riadok 213: Riadok 213:
  
 
=== Metódy <tt>get</tt> a <tt>set</tt> ===
 
=== Metódy <tt>get</tt> a <tt>set</tt> ===
 +
 +
Premenné inštancií tried sú teda zvyčajne súkromné. Existujú však prípady, keď je opodstatnené k niektorým z nich umožniť prístup aj iným triedam. Napríklad v našom príklade so zlomkami by sa v prípade nemožnosti pristúpiť k čitateľu alebo k menovateľu zlomku podstatne obmedzila funkcionalita triedy <tt>Fraction</tt>. Obvyklé riešenie takýchto situácií je ponechať samotnú premennú súkromnú, ale poskytnúť verejné metódy na čítanie a zmenu hodnoty tejto premennej. Takéto metódy pre premennu <tt>hodnota</tt> sa konvenčne nazývajú <tt>getHodnota</tt> a <tt>setHodnota</tt>. Podstatná časť našej triedy <tt>Fraction</tt> by tak mohla vyzerať napríklad nasledovne:
 +
 +
<syntaxhighlight lang="java">
 +
public class Fraction {
 +
    private int numerator;
 +
    private int denominator = 1;
 +
 +
    public Fraction() {
 +
 +
    }
 +
 +
    public Fraction(int numerator, int denominator) {
 +
        this.numerator = numerator;
 +
        this.denominator = denominator;
 +
    }
 +
 +
    public int getNumerator() {
 +
        return numerator;
 +
    }
 +
 +
    public void setNumerator(int numerator) {
 +
        this.numerator = numerator;
 +
    }
 +
 +
    public int getDenominator() {
 +
        return denominator;
 +
    }
 +
 +
    public void setDenominator(int denominator) {
 +
        this.denominator = denominator;
 +
    }
 +
 +
    public double evaluate() {
 +
        return (double) numerator / denominator;
 +
    }
 +
}
 +
</syntaxhighlight>
 +
* Určite nie je vhodné bezmyšlienkovite vytvárať metódy <tt>get</tt> a <tt>set</tt> pre všetky premenné. Opodstatnené to je iba vtedy, keď je daná premenná podstatnou charakteristikou triedy navonok, t.j. pokiaľ prístup k nej môže byť zaujímavý aj v prípade, že sa na samotnú triedu pozeráme ako na &bdquo;čiernu krabičku&rdquo;.
 +
 +
Výhody použitia metód <tt>get</tt> a <tt>set</tt> oproti použitiu verejných premenných sú napríklad nasledujúce:
 +
* Môžeme poskytnúť iba metódu <tt>get</tt>. Tým sa premenná stane &bdquo;určenou iba na čítanie&rdquo;. Ak by sme napríklad v našej triede <tt>Fraction</tt> zmazali metódy <tt>setNumerator</tt> a <tt>setDenominator</tt>, dostali by sme triedu reprezentujúcu nemodifikovateľné zlomky (podobne ako napríklad <tt>String</tt> reprezentuje nemodifikovateľné reťazce).
 +
* V rámci metódy <tt>set</tt> možno kontrolovať, či sa do premennej ukladá rozumná hodnota. Napríklad metóda <tt>setDenominator</tt> vyššie by mohla vyhodiť výnimku (ešte nevieme ako) v prípade, že by sme sa pokúsili nastaviť menovateľ na nulu.
 +
* Metódy <tt>get</tt> a <tt>set</tt> nemusia presne korešpondovať s premennými, a teda môžu ostať zachované aj po zmene vnútornej reprezentácie triedy. Uvažujme napríklad
  
 
== Preťažovanie metód ==
 
== Preťažovanie metód ==

Verzia zo dňa a času 20:12, 19. február 2021

Oznamy

  • Na test pre pokročilých sa v prípade záujmu treba prihlásiť do utorka 23. februára, 11:30.
  • Prvú bonusovú úlohu treba odovzdať do stredy 24. februára, 11:30.

Základné koncepty objektovo orientovaného programovania

Objekty a triedy

Dvoma najzákladnejšími konceptmi objektovo orientovaného programovania (OOP) sú triedy a objekty.

  • Trieda (angl. class) je typ, ktorý podobne ako struct v C/C++ môže združovať údaje rôznych typov. Okrem toho ale obvykle obsahuje aj definície metód na manipuláciu s týmito údajmi.
  • Objekt (angl. object) je inštancia triedy – obsahuje teda už nejakú konkrétnu sadu údajov vyhovujúcu definícii triedy, na ktorú možno aplikovať metódy triedy.
  • Triedu teda možno chápať ako „vzor”, podľa ktorého sa vytvárajú objekty.

Príklad: nasledujúca trieda Fraction reprezentuje zlomky. Obsahuje dve premenné numerator a denominator zodpovedajúce čitateľu a menovateľu zlomku a metódu na vyhodnotenie zlomku.

public class Fraction {
    int numerator;
    int denominator;

    double evaluate() {
        return (double) numerator / denominator;
    }
}

Inštanciami tejto triedy, t.j. objektmi typu Fraction, sú konkrétne realizácie triedy Fraction (napr. zlomok s čitateľom 2 a menovateľom 3). O spôsobe ich vytvorenia si povieme o chvíľu. Avšak v prípade, že už máme nejakú inštanciu fraction triedy Fraction vytvorenú, môžeme hodnotu zlomku vypísať napríklad nasledovne:

Fraction fraction;

// Sem pride vytvorenie instancie triedy Fraction a jej priradenie do premennej fraction.

System.out.println(fraction.evaluate());

Príklad: časť triedy reprezentujúcej zásobník implementovaný pomocou poľa (čo je v Jave značne suboptimálne riešenie) by mohla vyzerať napríklad nasledovne.

public class MyStack {
    int data[];
    int pocet;

    int pop() {
       pocet--;
       return data[pocet];
    }

    // Dalsie metody (napr. push) ...
}

Ak si opäť odmyslíme vytvorenie samotného zásobníka, môžeme so zásobníkom typu MyStack pracovať napríklad takto:

MyStack stack;

// Sem pride vytvorenie zasobnika a napriklad niekolko prikazov push.

int x = s.pop();

Neskôr uvidíme, že medzi štandardnými triedami jazyka Java možno nájsť aj množstvo dátových štruktúr a medzi nimi aj triedu Stack pre zásobníky. Príklad vyššie je teda iba ilustračný a tvorbe tried podobného druhu je vo všeobecnosti lepšie sa vyvarovať.

Príklad: v Jave sú všetky typy okrem primitívnych triedami a ich inštancie sú teda objektmi. S výnimkou veľmi špecifického prípadu polí (o ktorom si viac povieme neskôr) pôjde o triedy a objekty v podobe, v akej si ich predstavíme na tejto prednáške.

Referencie na objekty

Premenná, ktorej typom je trieda, obsahuje referenciu na objekt, ktorý je inštanciou tejto triedy.

  • Podľa toho sa teda správajú operátory = a ==.
  • K premenným a metódam objektu, na ktorý príslušná referencia ukazuje, pristupujeme pomocou operátora . a píšeme napríklad fraction.numerator alebo fraction.evaluate().
Fraction fraction1, fraction2;

// ...

fraction1 = fraction2;    // Obidve premenne ukazuju na to iste miesto v pamati.
fraction1.numerator = 3;  // Zmeni sa aj hodnota fraction2.numerator.
  • Do premennej, ktorej typom je trieda, možno priradiť hodnotu null – v takom prípade ide o referenciu, ktorá neukazuje na žiaden objekt.

Konštruktory a inicializácia objektov

Často je potrebné súčasne s vytvorením objektu vykonať rôzne inicializačné úkony – napríklad pri zásobníku typu MyStack alokovať pole, pri zlomkoch typu Fraction inicializovať premenné na vhodné hodnoty, a pod. Na takúto inicializáciu objektov v Jave slúžia špeciálne metódy – takzvané konštruktory. Volanie konštruktorov je neodmysliteľne späté s vytváraním objektov.

  • Názov konštruktora je vždy rovnaký ako názov triedy, ku ktorej patrí (špeciálne teda ide o jediné metódy, ktorých názov podľa konvencie začína veľkým písmenom).
  • Do hlavičky konštruktora sa nepíše návratová hodnota (konštruktor žiadnu nemá). V opačnom prípade pôjde o bežnú metódu (nie o konštruktor), čo môže viesť k pomerne nepríjemným chybám.
  • Prípadné argumenty sa zapisujú rovnako ako pri ktorejkoľvek inej metóde.
  • Pre jednu triedu možno definovať aj viacero konštruktorov, ktoré sa však musia líšiť postupnosťou typov argumentov (aby bolo pri volaní jasné, o ktorý z konštruktorov ide).

Príklad: pre triedu Fraction môžeme napísať napríklad nasledujúce dva konštruktory.

public Fraction() {
    numerator = 0;
    denominator = 1;
}

public Fraction(int num, int denom) { // Neskor uvidime, ze nie je nutne pre argumenty konstruktora a premenne instancie volit ine nazvy
    numerator = num;
    denominator = denom;
}
  • Ak pre triedu nedefinujeme žiaden konštruktor, automaticky sa vytvorí konštruktor bez parametrov, ktorý v princípe sám o sebe neurobí nič, ale je možné ho zavolať (bez čoho objekt nevytvoríme).

Samotné vytvorenie inštancie triedy sa realizuje pomocou operátora new, za ktorým nasleduje volanie niektorého konštruktora.

Fraction f1 = new Fraction();
Fraction f2;
f2 = new Fraction(2, 3);

System.out.println(f1.evaluate());
System.out.println(f2.evaluate());
  • Operátor new dynamicky alokuje pamäť pre objekt, zavolá príslušný konštruktor a vráti referenciu na vytvorený objekt.
  • Nie je potrebné starať sa o neskoršie odalokovanie pamäte – túto úlohu v JVM vykonáva tzv. garbage collector.

Na rozdiel od lokálnych premenných sú premenné inštancií alokované automaticky, a to na hodnoty 0, false, alebo null v závislosti od typu premennej. Prípadne je možné niektoré premenné inštancií inicializovať aj explicitne na odlišné hodnoty:

public class Fraction {
    int numerator;        // Inicializuje sa na nulu.
    int denominator = 1;

    // ...
}

Alternatívne možno premenné inicializovať v rámci konštruktora, rovnako ako v jednom z vyššie uvedených príkladov. Pri vytváraní inštancie triedy pomocou operátora new sa jednotlivé procesy vykonajú v nasledujúcom poradí:

  • Najprv sa vykoná automatická alebo explicitná inicializácia premenných (a to aj v prípade, že nebol definovaný žiaden konštruktor triedy a je tak volaný jej východzí konštruktor bez parametrov).
  • Až následne sa spustí volaný konštruktor.

Kľúčové slovo this

V rámci (nestatických) metód a konštruktorov tried možno používať kľúčové slovo this, ktoré sa pre každú inštanciu tejto triedy interpretuje ako referencia na seba, t.j. na objekt, na ktorom bola metóda obsahujúca toto kľúčové slovo volaná. Kľúčové slovo this sa používa predovšetkým nasledujúcimi troma spôsobmi:

  • Pre ľubovoľnú premennú premenna alebo metódu metoda inštancie triedy možno na prístup k tejto premennej resp. metóde použiť zápis this.premenna resp. this.metoda. Často sú teda tieto zápisy ekvivalentné kratším zápisom premenna resp. metoda. Niekedy sa však môže stať, že sa niektorá premenná inštancie prekryje napríklad argumentom alebo lokálnou premennou s rovnakým názvom. V takom prípade možno k premennej inštancie pristúpiť iba prostredníctvom this.premenna. Naša trieda Fraction by teda napríklad mohla vyzerať aj takto:
public class Fraction {
    int numerator;
    int denominator = 1;

    public Fraction() {

    }

    public Fraction(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public double evaluate() {
        return (double) numerator / denominator;
    }
}
  • Kľúčové slovo this možno využiť aj na predanie danej inštancie ako argument nejakej metódy.
public class Fraction {
    // ... 

    @Override                      // Tuto znacku si zatial nevsimajme, kod by fungoval aj bez nej.
    public String toString() {
        return numerator + " / " + denominator;
    }

    public void print() {
        System.out.println(this);  // Rovnake spravanie ako s argumentom toString() resp. this.toString().
    }
}
  • Na prvom riadku konštruktora možno pomocou this( ... ) zavolať iný konštruktor tej istej triedy. Často napríklad potrebujeme spraviť niektoré úkony pri inicializácii objektu vždy, t.j. v ľubovoľnom konštruktore, zatiaľ čo iné sú žiadúce iba v niektorých konštruktoroch. V takom prípade by z návrhového hľadiska nebolo rozumné opakovať spoločné časti kódu v každom z konštruktorov. (Čo sa stane v prípade, keď v tomto kóde bude treba niečo zmeniť?) Namiesto toho je vhodnejšie v „pokročilejších” konštruktoroch zavolať nejaký „menej pokročilý” konštruktor (ale iba na prvom riadku).
public class Circle {
    double centerX = 0; // Netreba explicitnu inicializaciu, ale moze to byt prehladnejsie
    double centerY = 0; // Netreba explicitnu inicializaciu, ale moze to byt prehladnejsie
    double radius = 1;
    
    public Circle() {
        System.out.println("Vytvaram kruh.");
    }   
    
    public Circle(double centerX, double centerY) {
        this();                  
        this.centerX = centerX;
        this.centerY = centerY;
    }
    
    public Circle(double centerX, double centerY, double radius) {
        this(centerX, centerY);
        this.radius = radius;
    }
}

Modifikátory prístupu

Premenným a metódam, ako aj triedam samotným, možno v Jave nastavovať tzv. modifikátory prístupu určujúce viditeľnosť týchto súčastí z iných tried. Modfikátory prístupu sú v Jave štyri:

  • private: k premennej alebo metóde možno pristúpiť iba v rámci jej triedy; na triedy sa tento modifikátor použiť nedá.
  • (žiadny modifikátor): premenná, metóda, alebo trieda je viditeľná len v rámci jej balíka.
  • protected: podobné ako v predchádzajúcom prípade; rozdiel uvidíme na budúcej prednáške.
  • public: premennú, metódu, alebo triedu možno použiť z ľubovoľnej triedy.

Každá trieda Trieda s modifikátorom public musí byť uložená v zdrojovom súbore, ktorého názov musí byť Trieda.java. Názvy tried s inými modifikátormi prístupu sa nemusia zhodovať s názvom súboru a jeden zdrojový súbor môže obsahovať aj viacero tried (najviac jedna z nich však môže byť public, pričom v takom prípade sa názov tejto triedy musí zhodovať s názvom súboru). Za dobrú prax sa ale považuje pre každú triedu vytvoriť samostatný súbor, ktorého názov sa zhoduje s názvom triedy.

Ako sme už spomenuli na minulej prednáške, modifikátor prístupu statickej metódy main musí byť vždy public.

Zapuzdrenie

Jedným z hlavných metodických princípov objektovo orientovaného programovania je zapuzdrenie (angl. encapsulation). Ide o „zabalenie” dát a metód na manipuláciu s nimi do spoločného „puzdra” – inštancie nejakej triedy.

  • Kód z iných tried by mal s dátami „zabalenými” v objekte manipulovať iba pomocou jeho metód na to určených.
  • To sa obvykle zabezpečí tak, že sa modifikátor public priradí iba tým metódam, ktoré sú určené na použitie „zvonka”. Premenným a pomocným metódam sa priradí iný modifikátor, napríklad private.
  • Verejné metódy tak tvoria akúsi „sadu nástrojov”, ktorú trieda poskytuje iným triedam na prácu s jej inštanciami. Napríklad trieda pre zásobník by mohla mať (okrem konštruktora) verejné metódy push, pop, isEmpty a peek, pričom jej premenné a prípadné pomocné metódy by boli súkromné.
  • Výhodou tohto prístupu je, že možno zmeniť vnútornú implementáciu triedy bez toho, aby to nejako ovplyvnilo ostatné triedy. Jediné, čo musí zostať zachované, je správanie verejných funkcií (čo zvyčajne ide zariadiť aj pri zmenenej implementácii zvyšku triedy). Napríklad v triede pre zásobník by sme mohli namiesto poľa použiť spájaný zoznam a zvyšné triedy by to nijak neovplyvnilo.
  • Zapuzdrenie tak umožňuje rozdeliť projekt na relatívne nezávislé logické celky s dobre definovaným rozhraním.

Metódy get a set

Premenné inštancií tried sú teda zvyčajne súkromné. Existujú však prípady, keď je opodstatnené k niektorým z nich umožniť prístup aj iným triedam. Napríklad v našom príklade so zlomkami by sa v prípade nemožnosti pristúpiť k čitateľu alebo k menovateľu zlomku podstatne obmedzila funkcionalita triedy Fraction. Obvyklé riešenie takýchto situácií je ponechať samotnú premennú súkromnú, ale poskytnúť verejné metódy na čítanie a zmenu hodnoty tejto premennej. Takéto metódy pre premennu hodnota sa konvenčne nazývajú getHodnota a setHodnota. Podstatná časť našej triedy Fraction by tak mohla vyzerať napríklad nasledovne:

public class Fraction {
    private int numerator;
    private int denominator = 1;

    public Fraction() {

    }

    public Fraction(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public int getNumerator() {
        return numerator;
    }

    public void setNumerator(int numerator) {
        this.numerator = numerator;
    }

    public int getDenominator() {
        return denominator;
    }

    public void setDenominator(int denominator) {
        this.denominator = denominator;
    }

    public double evaluate() {
        return (double) numerator / denominator;
    }
}
  • Určite nie je vhodné bezmyšlienkovite vytvárať metódy get a set pre všetky premenné. Opodstatnené to je iba vtedy, keď je daná premenná podstatnou charakteristikou triedy navonok, t.j. pokiaľ prístup k nej môže byť zaujímavý aj v prípade, že sa na samotnú triedu pozeráme ako na „čiernu krabičku”.

Výhody použitia metód get a set oproti použitiu verejných premenných sú napríklad nasledujúce:

  • Môžeme poskytnúť iba metódu get. Tým sa premenná stane „určenou iba na čítanie”. Ak by sme napríklad v našej triede Fraction zmazali metódy setNumerator a setDenominator, dostali by sme triedu reprezentujúcu nemodifikovateľné zlomky (podobne ako napríklad String reprezentuje nemodifikovateľné reťazce).
  • V rámci metódy set možno kontrolovať, či sa do premennej ukladá rozumná hodnota. Napríklad metóda setDenominator vyššie by mohla vyhodiť výnimku (ešte nevieme ako) v prípade, že by sme sa pokúsili nastaviť menovateľ na nulu.
  • Metódy get a set nemusia presne korešpondovať s premennými, a teda môžu ostať zachované aj po zmene vnútornej reprezentácie triedy. Uvažujme napríklad

Preťažovanie metód

Statické vs. nestatické metódy a premenné

Ďalšie príklady

Odkazy