Programovanie (1) v C/C++
1-INF-127, ZS 2024/25

Úvod · Pravidlá · Prednášky · Softvér · Testovač
· Kontaktujte nás pomocou e-mailovej adresy E-prg.png (bude odpovedať ten z nás, kto má príslušnú otázku na starosti alebo kto má práve čas).
· Prosíme študentov, aby si pravidelne čítali e-maily na @uniba.sk adrese alebo aby si tieto emaily preposielali na adresu, ktorú pravidelne čítajú.


Letný semester, prednáška č. 2

Z Programovanie
Skočit na navigaci Skočit na vyhledávání

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;
    }

    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

Preťažovanie metód

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

Ďalšie príklady

Odkazy