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 č. 3: Rozdiel medzi revíziami

Z Programovanie
Skočit na navigaci Skočit na vyhledávání
Riadok 212: Riadok 212:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
* Inštanciu <tt>c</tt> triedy <tt>Circle</tt> teraz môžeme nielen vypísať na konzolu ako <tt>System.out.println(c)</tt> (použije sa metóda <tt>toString</tt>), ale môžeme pre ňu zavolať aj ľubovoľnú metódu triedy <tt>Shape</tt>, napríklad <tt>c.move(1, 1)</tt> alebo <tt>c.setX(2)</tt>.
+
* Inštanciu <tt>c</tt> triedy <tt>Circle</tt> teraz môžeme nielen vypísať na konzolu cez <tt>System.out.println(c)</tt> (použije sa metóda <tt>toString</tt>), ale môžeme pre ňu zavolať aj ľubovoľnú metódu triedy <tt>Shape</tt>, napríklad <tt>c.move(1, 1)</tt> alebo <tt>c.setX(2)</tt>.
  
 
===Dedenie a typy===
 
===Dedenie a typy===

Verzia zo dňa a času 14:50, 26. február 2021

Oznamy

  • Dnes po prednáške bude zverejnené zadanie prvej domácej úlohy, ktorej riešenia bude potrebné odovzdať najneskôr do pondelka 15. marca, 9:00 (čiže do začiatku piatej prednášky).

Opakovanie: triedy a objekty

  • Objekt je predovšetkým súborom rôznych dát a metód na manipuláciu s nimi. Na objekty sa odkazuje pomocou ich identifikátorov, ktoré sú referenciami na ich „pamäťové adresy”.
  • Každý objekt je inštanciou nejakej triedy (angl. class). Triedu možno chápať ako „vzor”, podľa ktorého sa vytvárajú objekty. Trieda tiež reprezentuje typ jej objektov.
  • Trieda sa teda podobá na struct z jazykov C a C++ v tom, že môže obsahovať niekoľko hodnôt rôznych typov. Ide však o omnoho bohatší koncept – môže obsahovať metódy (funkcie) na prácu s dátami uloženými v inštancii danej triedy, umožňuje nastaviť viditeľnosť jednotlivých premenných a metód pomocou modifikátorov, atď.
  • Konštruktory sú špeciálne metódy triedy slúžiace na vytvorenie objektu (inštancie triedy).
  • Základným princípom objektovo orientovaného programovania je zapuzdrenie (angl. encapsulation): spojenie dát a súvisiaceho kódu do koherentného logického celku.
    • Trieda väčšinou navonok ukazuje iba vhodne zvolenú časť metód.
    • Premenné a pomocné metódy sú skryté – je ich tak možné meniť bezo zmeny kódu využívajúceho triedu.

Dedenie

Trieda môže byť podtriedou inej triedy. Napríklad trieda Pes môže byť podtriedou všeobecnejšej triedy Zviera: každý objekt, ktorý je inštanciou triedy Pes je potom súčasne aj inštanciou triedy Zviera. Tento vzťah medzi triedami vyjadrujeme kľúčovým slovom extends v definícii triedy: píšeme teda napríklad

class Pes extends Zviera { 
    ...
}

Hovoríme tiež, že trieda Pes dedí od triedy Zviera, alebo že trieda Pes triedu Zviera rozširuje.

Dedenie umožňuje vyhnúť sa nutnosti písať podobný kód viackrát. Namiesto niekoľkých podobných tried s podobnými metódami možno vytvoriť ich nadtriedu a spoločné časti kódu presunúť tam.

Príklad

Uvažujme triedy reprezentujúce rôzne geometrické útvary, ktoré môžeme posúvať v rovine. Takto by mohli vyzerať časti tried bez dedenia:

public class Rectangle {
    private double x, y;           // Suradnice laveho horneho rohu
    private double width, height;  // Vyska a sirka obdlznika

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    public void move(double deltaX, double deltaY) {
        x += deltaX;
        y += deltaY;
    }
}
public class Circle {
    private double x, y;    // Suradnice stredu
    private double radius;  // Polomer kruznice

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    public void move(double deltaX, double deltaY) {
        x += deltaX;
        y += deltaY;
    }
}

Vidíme, že obidve triedy obsahujú pomerne veľa spoločného kódu. Teraz to isté s dedením – spoločné premenné a metódy presunieme do spoločnej nadtriedy Shape:

public class Shape {
    private double x, y;  // Suradnice nejakeho vyznacneho bodu geometrickeho utvaru

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }

    public void move(double deltaX, double deltaY) {
        x += deltaX;
        y += deltaY;
    }
}
public class Rectangle extends Shape {
    private double width, height;

    public double getWidth() {
        return width;
    }

    public void setWidth(double width) {
        this.width = width;
    }

    public double getHeight() {
        return height;
    }

    public void setHeight(double height) {
        this.height = height;
    }

    // Pripadne dalsie metody pre obdlznik
}
public class Circle extends Shape {
    private double radius;

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }

    // Pripadne dalsie metody pre kruznicu
}

V rámci triedy možno používať aj verejné metódy a premenné nadtriedy, ako keby boli jej vlastné – a často aj metódy a premenné s inými modifikátormi rôznymi od private (o tom neskôr). V metódach a konštruktoroch triedy Circle tak napríklad môžeme používať metódy getX, setX, getY, setY a move.

class Rectangle extends Shape {
    // ...
    
    public Rectangle(double x, double y, double width, double height) {
        setX(x);
        setY(y);
        setWidth(width);
        setHeight(height);
    }

    // ...
}
class Circle extends Shape {
    // ...
    
    public Circle(double x, double y, double radius) {
        setX(x);
        setY(y);
        setRadius(radius);
    }
    
    @Override                    // Vyznam tejto znacky si vysvetlime o chvilu
    public String toString() {
        return "Stred: [" + getX() + "," + getY() + "]; Polomer: " + getRadius() + ".";
    }

    // ...
}
  • Inštanciu c triedy Circle teraz môžeme nielen vypísať na konzolu cez System.out.println(c) (použije sa metóda toString), ale môžeme pre ňu zavolať aj ľubovoľnú metódu triedy Shape, napríklad c.move(1, 1) alebo c.setX(2).

Dedenie a typy

  • Premenná typu Shape môže obsahovať referenciu na objekt triedy Shape alebo jej ľubovoľnej podtriedy:
Circle c = new Circle(0,0,5);
Shape s = c;     // toto je korektne priradenie
// c = s;        // toto neskompiluje, kedze s nemusi byt kruh
c = (Circle) s;  // po pretypovani to uz skompilovat pojde; program ale moze padnut, ak s nie je instanciou Circle alebo null

// Istejsi pristup je teda najprv overit, ci je s skutocne instanciou triedy Circle:
if (s instanceof Circle) {  
    c = (Circle) s;
}
  • Vďaka tejto črte možno rôzne typy útvarov spracúvať tým istým kódom. Napríklad nasledujúca funkcia dostane pole útvarov (môžu v ňom byť útvary rôznych typov) a posunie každý z nich o daný vektor (deltaX, deltaY):
static void moveAll(Shape[] shapes, int deltaX, int deltaY) {
    for (Shape x : shapes) {
        x.move(deltaX, deltaY);
    }
}
  • Cvičenie: čo vypíše nasledujúci kód?
Shape[] shapes = new Shape[2];
shapes[0] = new Rectangle(0,0,1,2);
shapes[1] = new Circle(0,0,1);

moveAll(shapes, 2, 2);

for (Shape x : shapes) {
    if (x instanceof Circle) {
        System.out.println("Je to kruh.");
        Circle c = (Circle) x;
        c.print();
    }
    if (x instanceof Shape) {
        System.out.println("Je to utvar.");
    }
}

Dedenie a konštruktory

  • Typickou úlohou konštruktora je správne nainicializovať objekt.
  • Pri dedení si väčšinou každá trieda inicializuje „svoje” premenné.
  • Napríklad krajší spôsob realizácie konštruktorov pre geometrické útvary je nasledovný: Shape inicializuje x a y, pričom napríklad Circle nechá inicializáciu x a y na Shape a inicializuje už len radius.
  • Prvý príkaz konštruktora môže pozostávať z volania konštruktora predka pomocou kľúčového slova super (z angl. superclass, t.j. nadtrieda).
class Shape {
    int x, y;             
   
    public Shape(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    // zvysok triedy Shape
}

class Rectangle extends Shape {
    int width, height;    
    
    public Rectangle(int x, int y, int height, int width) {
        super(x,y);
        this.height = height;
        this.width = width;
    }
    
    // zvysok triedy Rectangle
}

class Circle extends Shape {
    int radius;         
    
    public Circle(int x, int y, int radius) {
        super(x,y);
        this.radius = radius;
    }
    
    // zvysok triedy Circle
}
  • Ak nezavoláme konštruktor predka ručne, automaticky sa zavolá konštruktor bez parametrov, t.j. super(). To môže pri kompilovaní vyústiť v chybu v prípade, keď nadtrieda nemá definovaný konštruktor bez parametrov (či už explicitne jeho implementáciou, alebo implicitne tým, že sa neuvedie implementácia žiadneho konštruktora nadtriedy). Napríklad v horeuvedenom príklade je teda volanie konštruktora nadtriedy nutnou podmienkou úspešnej kompilácie.
  • Výnimkou je prípad, keď sa na prvom riadku volá iný konštruktor tej istej triedy pomocou this(...) – vtedy sa volanie konštruktora nadtriedy nechá na práve zavolaný konštruktor.

Prekrývanie metód a polymorfizmus

Podtrieda môže prekryť (angl. override) niektoré zdedené metódy, aby sa chovali inak ako v predkovi.

Napríklad môžeme mať útvar Segment (úsečka), ktorý je zadaný dvoma koncovými bodmi a v metóde move treba posunúť oba. Metódu z predka môžeme zavolať pomocou super.move, ale nemusí to byť na prvom riadku a nemusí byť použitá vôbec:

class Segment extends Shape {
    int x2, y2;
    
    public Segment(int x, int y, int x2, int y2) {
        super(x,y);
        this.x2 = x2;
        this.y2 = y2;
    }

    @Override
    public void move(int deltaX, int deltaY) {
        super.move(deltaX, deltaY);  // volanie metody v predkovi
        x2 += deltaX;
        y2 += deltaY;
    }
}

Anotácia @Override je nepovinná, ale odporúčaná. Ide o informáciu pre kompilátor, ktorou sa vyjadruje snaha o prekrytie zdedenej metódy. Ak sa v predkovi nenachádza metóda s rovnakou hlavičkou, kompilátor vyhlási chybu. Tým sa dá predísť obzvlášť nepríjemným chybám.

S prekrývaním metód súvisí polymorfizmus, pod ktorým sa v programovaní (hlavne pri OOP) rozumie schopnosť metód chovať sa rôzne:

  • S určitou formou polymorfizmu sme sa už stretli, keď sme mali viacero metód s rovnakým menom, avšak s rôznymi typmi parametrov (tzv. preťažovanie metód, angl. overloading).
  • Pri dedení sa navyše môže metóda chovať rôzne v závislosti od triedy, ku ktorej táto metóda patrí.
  • To, ktorá verzia metódy sa zavolá, záleží od toho, akého typu je objekt, nie akého typu je referencia naň.
  Shape s = new Segment(0,0,1,-5);
  s.move(1,1);  // zavola prekrytu metodu z triedy Segment
  s = new Circle(0,0,1);
  s.move(1,1);  // zavola metodu z triedy Shape, lebo v Circle nie je prekryta

  Shape[] shapes = new Shape[3];
  // vypln pole shapes
  //...
  for(Shape x : shapes) {
      x.move(deltaX, deltaY);  // kazdy prvok sa posuva svojou metodou move, ak ju ma
  }

Vo všeobecnosti sa pri volaní o.f(par1,...,parn) pre objekt o typu T aplikuje nasledujúci princíp:

  • Ak má trieda T svoju implementáciu metódy f s vhodnými parametrami, vykoná sa táto verzia metódy.
  • V opačnom prípade sa vhodná verzia metódy f hľadá v nadtriede triedy T, v prípade neúspechu v nadtriede nadtriedy T, atď.

Polymorfizus môže byť schovaný aj hlbšie – neprekrytá metóda z predka môže vo svojom tele volať prekryté metódy, čím sa jej správanie mení v závislosti od typu objektu:

class SuperClass {
    void doX() { 
        System.out.println("doX in Super");
    }
   
    void doXTwice() { 
        doX();
        doX();
    }    
}

class SubClass extends SuperClass {
    void doX() { 
        System.out.println("doX in Sub");
    }
}

// v metode main:
SuperClass a = new SubClass();
a.doXTwice();  // vypise 2x doX in Sub

Zmysluplnejší príklad bude poskytovať metóda printArea v príklade nižšie.

Abstraktné triedy a metódy

Aby sa metóda chovala v určitej skupine tried polymorfne, musí byť definovaná v ich spoločnej nadtriede. V tejto nadtriede však nemusí existovať jej zmysluplná implementácia.

  • Uvažujme napríklad metódu area(), ktorá zráta plochu geometrického útvaru.
  • Pre triedy Rectangle, Circle, resp. Segment je implementácia takejto metódy zrejmá. Zmysluplná implementácia v ich spoločnej nadtriede Shape by však bola prinajmenšom problematická.

Vzniknutú situáciu možno riešiť nasledovne:

  • Metódu area() v triede Shape, ako aj triedu Shape samotnú, označíme za abstraktnú modifikátorom abstract.
  • Abstraktná metóda pozostáva iba z hlavičky bez samotnej implementácie.
  • Abstraktná trieda je trieda, ktorá môže obsahovať abstraktné metódy. Zo zrejmých dôvodov z nej nemožno tvoriť inštancie (napríklad v našom príklade by tieto inštancie „nevedeli, čo robiť” pri volaní metódy area()), ale za účelom volania z podtried môže obsahovať definície konštruktorov. Abstraktná trieda slúži iba na dedenie, stále však môže byť typom referencie na objekt.
  • Podtriedy abstraktnej triedy, ktoré nie sú abstraktné, musia implementovať všetky abstraktné metódy svojho predka.

Príklad:

abstract class Shape {
    // ...
    
    public abstract double area();
    
    public void printArea() {
        System.out.println("Plocha je " + area() + ".");
    }
}

class Rectangle extends Shape {
    // ...
    
    @Override
    public double area() {
        return width * height;
    }
}

class Circle extends Shape {
    // ...
    
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

class Segment extends Shape {
    // ...
    
    @Override
    public double area() {
        return 0;
    }
}

Napríklad program

public static void main(String[] args) {
    Shape[] shapes = new Shape[3];
    shapes[0] = new Rectangle(0,0,1,2);
    shapes[1] = new Circle(0,0,1);
    shapes[2] = new Segment(1,1,2,2);
    
    for (Shape x : shapes) {
        x.printArea();
    }
}

potom vypíše nasledujúci výstup:

Plocha je 2.0.
Plocha je 3.141592653589793.
Plocha je 0.0.

Hierarchia tried a trieda Object

  • V Jave môže každá trieda dediť iba od jednej triedy (na rozdiel napríklad od C++, kde je možné dedenie od viacerých tried).
  • Dedenie je však možné „viacúrovňovo”:
class Pes extends Zviera {
}

class Civava extends Pes { // hierarchia tried nemusi verne zodpovedat realite
}
  • Všetky triedy sú automaticky potomkami triedy Object; tá sa tiež považuje za priamu nadtriedu tried, ktoré explicitne nerozširujú žiadnu triedu.
  • Trieda Object obsahuje metódy (napr. toString()), ktoré je často užitočné prekrývať.

Príklad:

  • Nasledujúci kus kódu je o niečo elegantnejším spôsobom vypisovania geometrických útvarov, než pomocou metódy Circle.print:
class Circle extends Shape {
    // ...
    
    @Override
    public String toString() {
        return "Stred: (" + x + "," + y + "). Polomer: " + radius + ".";   
    }
    
    // ...
}

// ...

// V metode main: 

Circle c = new Circle(0, 0, 1);
System.out.println(c.toString());
System.out.println(c);  // ekvivalentne predchadzajucemu volaniu

// ...
  • Veľmi špeciálnym druhom objektov v Jave sú polia, pričom polia typu T (kde T je trieda alebo primitívny typ) sa považujú za inštancie triedy T[], ktorá je podtriedou triedy Object. Aj na polia teda v princípe možno aplikovať metódu toString triedy Object, avšak od jej použitia nemožno očakávať žiadne rozumné správanie, keďže nebola zmysluplným spôsobom prekrytá.

Rozhrania

Rozhranie (interface) je podobným konceptom ako abstraktná trieda. Existuje však medzi nimi niekoľko rozdielov, z ktorých najpodstatnejšie sú tieto:

  • Rozhranie nemôže obsahovať konštruktory, ani iné ako finálne premenné.
  • Rozhranie slúži predovšetkým ako zoznam abstraktných metód – kľúčové slovo abstract tu netreba uvádzať. Naopak implementované nestatické metódy musia byť označené kľúčovým slovom default.
  • Kým od tried sa dedí pomocou kľúčového slova extends, rozhrania sa implementujú pomocou kľúčového slova implements. Rozdiel je predovšetkým v tom, že implementovať možno aj viacero rozhraní. Jedno rozhranie môže navyše rozširovať iné (dopĺňať ho o ďalšie požadované funkcie): v takom prípade používame kľúčové slovo extends.
  • Všetky položky v rozhraní sa chápu ako verejné (public).

Príklad použitia:

interface Stack {
    void push(int item);
    int pop();
}

interface Printable {
    void print();
}

class LinkedStack implements Stack, Printable {
    static class Node {
        private int data;
        private Node next;
        
        public Node(int data, Node next) {
            this.data = data;
            this.next = next;
        }
        
        public int getData() {
            return data;
        }
        
        public Node getNext() {
            return next;
        }
    }
    
    private Node top;
    
    @Override
    public void push(int item) {
        Node p = new Node(item, top);
        top = p;
    }
    
    @Override
    public int pop() {
        if (top == null) {
            return -1;
        }
        int result = top.getData();
        top = top.getNext();
        return result;
    }
    
    @Override
    public void print() {
        Node p = top;
        while (p != null) {
            System.out.print(p.getData() + " ");
            p = p.getNext();
        }
        System.out.println();
    }
}

class ArrayStack implements Stack, Printable {
    private int[] a;
    private int n;
    
    public ArrayStack() {
	a = new int[100]; 
	n = 0;
    }

    @Override
    public void push(int item) {
        a[n] = item;
        n++;
    }
    
    @Override 
    public int pop() {
        if (n <= 0) {
            return -1;
        }
	n--;
        return a[n];
    }

    @Override 
    public void print() {
        for (int i = 0; i <= n-1; i++) {
            System.out.print(a[i] + " ");
        } 
        System.out.println();
    }
}

class Blabol implements Printable {
    @Override 
    public void print() { 
        System.out.println("Blabla"); 
    }
}

public class InterfaceExample {
    static void fillStack(Stack stack) {
        stack.push(10);
        stack.push(20);
    }

    static void printTwice(Printable what) {
        what.print();
        what.print();
    }

    public static void main(String[] args) {
        LinkedStack s1 = new LinkedStack();
        Stack s2 = new ArrayStack();
        Blabol b = new Blabol();
        fillStack(s1);
        fillStack(s2);
        printTwice(s1);
        //printTwice(s2); // s2 je Stack a nevie, ze sa vie vypisat
        printTwice((ArrayStack) s2);
        printTwice(b);
    }
}

Prehľad niektorých modifikátorov tried, premenných a metód

Modifikátory prístupu:

  • public: triedy, rozhrania a ich súčasti prístupné odvšadiaľ.
  • (žiaden modifikátor): viditeľnosť len v rámci balíčka (package).
  • protected: viditeľnosť v triede, jej podtriedach a v rámci balíčka.
  • private: viditeľnosť len v danej triede.

Iné modifikátory:

  • abstract: neimplementovaná metóda alebo trieda s neimplementovanými metódami.
  • final:
    • Ak je trieda final, nedá sa z nej ďalej dediť.
    • Ak je metóda final, nedá sa v podtriede prekryť.
    • Ak je premenná alebo parameter final, ide o konštantu, ktorú nemožno meniť.
  • static:
    • Statické premenné a metódy sa týkajú celej triedy, nie konkrétnej inštancie.
    • Statické triedy vo vnútri inej triedy nie sú viazané na jej konkrétnu inštanciu (viac neskôr).

Aritmetický strom s využitím dedenia

V minulom semestri sme upozorňovali na návrhový nedostatok pri realizácii aritmetického stromu: niektoré položky uložené v struct-och sa využívali len v niektorých uzloch stromu (hodnoty iba v listoch a operátory iba vo vnútorných uzloch). Tomuto sa vieme vyhnúť pomocou dedenia.

  • Jednotlivé typy vrcholov budú podtriedy abstraktnej triedy Node
  • Namiesto použitia príkazu switch na typ vrchola tu prekryjeme potrebné funkcie, napríklad evaluate.
abstract class Node {
    public abstract int evaluate();
}

abstract class NularyNode extends Node {
}

abstract class UnaryNode extends Node {
    Node child;
    
    public UnaryNode(Node child) {
        this.child = child; 
    }
}

abstract class BinaryNode extends Node {
    Node left;
    Node right;

    public BinaryNode(Node left, Node right) { 
        this.left = left;
        this.right = right; 
    }
}

class Constant extends NularyNode {
    int value;
    
    public Constant(int value) { 
        this.value = value;
    }

    @Override 
    public int evaluate() { 
        return value;
    }

    @Override
    public String toString() { 
        return Integer.toString(value);
    }
}

class UnaryMinus extends UnaryNode {
    public UnaryMinus(Node child){
        super(child); 
    }

    @Override 
    public int evaluate() { 
        return -child.evaluate();
    }

    @Override 
    public String toString() { 
        return "(-" + child.toString() + ")";
    }
}

class Plus extends BinaryNode { 
    public Plus(Node left, Node right) { 
        super(left,right);
    }

    @Override
    public int evaluate() { 
        return left.evaluate() + right.evaluate();
    }

    @Override 
    public String toString() { 
        return "(" + left.toString() + "+" + right.toString() + ")";
    }
}

public class Expressions {

    public static void main(String[] args) {
        Node expr = new Plus(new UnaryMinus(new Constant(2)),
			     new Constant(3));
        System.out.println(expr);
        System.out.println(expr.evaluate());
    }
}

Odkazy