Programovanie (2) v Jave
1-INF-166, LS 2017/18

Úvod · Pravidlá · Prednášky · Netbeans · Testovač · Test a skúška
· Vyučujúcich môžete kontaktovať 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).
· DÚ10 je zverejnená, odovzdávajte do stredy 16.5. 22:00.
· Bonusový projekt odovzdávajte do pondelka 21.5. 22:00 na testovači. Predvádzanie projektov bude po prvom termíne skúšky v stredu 23.5. (t.j. začneme medzi 11:45 a 12:00 v H6). Ak vtedy nemôžete prísť, kontaktujte vyučujúce, dohodneme iný termín.
· Opravný/náhradný test bude v pondelok 28.5. o 10:00 v F1-108. V prípade záujmu sa prihláste v AISe.
· Na termíny skúšok sa zapisujte v AIS. Termíny: streda 23.5. (riadny), štvrtok 7.6. (riadny alebo 1. opravný), streda 20.6. (1. alebo 2. opravný) plus v prípade potreby ešte jeden termín v poslednom týždni skúškového. Prípadné konflikty nám dajte vedieť čím skôr. Ďalšie informácie na stránke Test a skúška.


Prednáška 27

Z Programovanie
Prejsť na: navigácia, hľadanie

Organizačné poznámky

  • DÚ6 (objekty) odovzdávajte do stredy 14.3. 22:00.
  • DÚ7 (dedičnosť) bude zverejnená tento týždeň
  • Najbližšia rozcvička budúcu stredu, bude pokrývať objekty a dedenie (cvičenia z tohto a minulého týždňa)
  • Problém s diakritikou na testovači opravený, ak narazíte na technické problémy, dajte nám vedieť
  • Rozcvičku je potrebné riešiť priamo na cvičení (v učebni), vo výnimočných prípadoch si dohodnúť vzdialený prístup vopred.

Opakovanie: triedy a objekty

  • Trieda (class) sa podobá na struct, ktorý sme videli v minulom semestri, môže teda obsahovať niekoľko premenných rôznych typov.
    • Inštancia triedy alebo aj objekt triedy (instance, object) je konkrétna hodnota daného typu, t.j. kus pamäti s uloženými premennými triedy
  • Navyše triedy zvyknú obsahovať aj metódy, ktoré s premennými v objekte niečo robia.
  • Konštruktory sú špeciálne metódy na inicializáciu premenných v triede.
  • Premenným a metódam triedy môžeme modifikátormi nastaviť, či ich má byť vidno mimo triedy.
  • Zvyklosti v jazyku Java: mená tried väčšinou začínajú veľkým písmenom (napr. String), mená metód a premenných malým (napr. print), viacslovné názvy majú veľké písmená na začiatkoch slov, napr toString
  • Zapuzdrenie (encapsulation): spojenie dát a súvisiaceho kódu.
    • Trieda väčšinou navonok ukazuje iba vhodne zvolenú časť metód.
    • Premenné a pomocné metódy sú skryté.
    • Preto ich je možné meniť bez zmeny kódu využívajúceho triedu.
public class Stack {
    public static final int MAXN = 100;
    private int data[];
    private int pocet;

    public Stack() {
        data = new int[MAXN];
        pocet = 0;
    }

    public int pop() {
        pocet--;
        return data[pocet];
    }
    public void push(int x) {
	data[pocet] = x;
        pocet++;
    }
    public boolean isEmpty() {
        return pocet==0;
    }
    public static void main(String[] args) {
        Stack s = new Stack();
        Stack t = s;
        s.push(7);
        t.push(5);
        System.out.println(s.pop());
        System.out.println(s.pop());
        System.out.println(s.pop()); // co spravi tento prikaz?
    }
}

Dnes pokračujeme ďalšími pojmami: dedenie (inheritance) a rozhranie (interface). K obom týmto oblastiam má Java pekné tutoriály:

Dedenie

  • Trieda môže byť podtriedou inej triedy, napr. trieda Pes môže byť podtriedou všeobecnejšej triedy Zviera
  • Toto vyjadrujeme kľúčovým slovom extends v definícii triedy class Pes extends Zviera { ... }
  • Ak máme niekoľko podobných tried s podobnými metódami, vytvoríme nadtriedu a spoločné časti kódu tam presunieme
    • vyhneme sa teda kopírovaniu podobného kódu

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:

class Rectangle {
    int x, y;            // suradnice laveho horneho rohu
    int width, height;   // vyska a sirka
    void move(int deltaX, int deltaY) {
	x+=deltaX; y+=deltaY;
    }
    // plus dalsie metody pre obdlznik
}

class Circle {
    int x, y;           // suradnice stredu
    int radius;         // polomer
    void move(int deltaX, int deltaY) {
	x+=deltaX; y+=deltaY;
    }
    // plus dalsie metody pre kruh
}

To isté s dedením: spoločné premenné a metódy presunieme do spoločnej triedy Shape a vynecháme z Circle a Rectangle, tie ich dedia:

class Shape {
    int x, y; // suradnice vyznacneho bodu utvaru (roh, stred a pod.)
    void move(int deltaX, int deltaY) {
	x+=deltaX; y+=deltaY;
    }
}

class Rectangle extends Shape {
    int width, height;
    // plus dalsie metody pre obdlznik
}

class Circle extends Shape {
    int radius;
    // plus dalsie metody pre kruh
}

Trieda môže používať premenné a metódy nadtriedy, ako keby boli jej vlastné s výnimkou tých, ktoré sú private (o tom neskôr).

  • Napr. v metódach triedy Circle môžeme používať premenné x, y, metódu move.
class Circle extends Shape {
    int radius;
    void print() {
	System.out.println("stred (" + x + "," + y + "), polomer " + radius);
    }
}

Ak máme premennú Circle c, môžeme zavolať metódy c.move(1,1) alebo c.print() alebo použiť premenné c.x, c.y, c.radius

  • (v praxi väčšinou chceme premenné v triede skryť)

Dedenie a typy

  • Ak máme premennú typu Shape, môže obsahovať referenciu na objekt triedy Shape alebo jej ľubovoľnej podtriedy.
Circle c = new Circle(...);
Shape s = c;    // toto je dobre
// c = s;       //! toto neskompiluje - s nemusi byt kruh
c = (Circle)s;  // pretypujeme, skompiluje ale moze padnut, ak s nie je Circle alebo null
if(s instanceof Circle) {  // pre istotu kontrola, ci je s Circle alebo podtrieda
  c = (Circle)s;
}
  • Toto je výhodné, lebo môžeme rôzne typy útvarov spracovávať tým istým kódom. Napr. 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);
  }
}

Čo vypíšu tieto riadky?

Shape[] shapes = new Shape[2];
shapes[0] = new Rectangle(0,0,1,2);
shapes[1] = new Circle(0,0,1);

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

Konštruktory a dedenie

  • Úlohou konštruktora je správne nainicializovať objekt
  • Pri dedení si väčšinou každá trieda inicializuje "svoje" premenné
    • Napr. Shape inicializuje x a y, Circle nechá inicializáciu x a y na Shape a inicializuje radius
  • Prvý príkaz v konštruktore môže byť volanie konštruktoru predka, ktorý voláme kľúčovým slovom super (zo superclass, nadtrieda)
class Shape {
    int x, y; // suradnice vyznacneho bodu utvaru (roh, stred a pod.)
    Shape(int x, int y) {
	this.x = x; this.y = y;
    }
    // zvysok triedy Shape
}
class Circle extends Shape {
    int radius;
    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()
  • Výnimka je, ak na prvom riadku voláme iný konštruktor tej istej triedy pomocou this(), vtedy sa volanie super nechá na zavolaný konštruktor

Polymorfizmus, prekrytie metód

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

  • Napr. 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;
    Segment(int x, int y, int x2, int y2) {
	super(x,y);
	this.x2 = x2; this.y2 = y2;
    }
    @Override
    void move(int deltaX, int deltaY) {
	super.move(deltaX, deltaY);  // volanie metody v predkovi
	x2 += deltaX; y2 += deltaY;
    }
}

Značka (annotation) @Override je nepovinná, ale odporúčaná. Tým kompilátoru naznačíte, že sa snažíte prekryť zdedenú metódu a ak taká v predkovi nie je, vyhlási chybu.

Polymorfizmus v programovaní (hlavne pri OOP) je schopnosť funkcií chovať sa rôzne

  • S určitou formou polymorfizmu sme sa už stretli, keď sme mali viacero metód s rovnakým menom ale s rôznymi typmi parametrov, ktoré sa mohli rôzne správať (tzv. overloading)
  • Pri dedení navyše sa môže funkcia chovať rôzne v rôznych triedach aj keď sa volá rovnako a má rovnaké typy parametrov
  • To, ktorá verzia sa zavolá, záleží od toho, akého typu je objekt, nie akého typu je definovaná referencia
  Shape s = new Segment(0,0,1,-5);
  s.move(1,1);  // zavola pretazenu metodu z triedy Segment
  s = new Circle(0,0,1);
  s.move(1,1);  // zavola metodu z triedy Shape, lebo Circle ju nepretazil

  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
  }


Keď zavoláme metódu f() pre objekt o typu T (t.j. zavolám o.f() )

  • Ak trieda T má f(), vykoná sa
  • Ak T nemá f(), hľadáme f() v nadtriede a prípadne v jej nadtriede atď

Polymorfizus môže byť schovaný aj hlbšie:

  • Neprekrytá metóda z predka môže vo svojom tele volať prekryté metódy a tý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 viď metóda printArea v príklade nižšie.

Abstraktné metódy a triedy

  • Aby sa metóda chovala v určitej skupine tried polymorfne, musí byť definovaná v ich spoločnej nadtriede
  • Pre túto nadtriedu však nemusí existovať zmysluplná implementácia
  • Napr. môžeme mať metódu area(), ktorá zráta plochu geometrického útvaru
    • vieme zrátať plochu kruhu alebo obdĺžnika, ale čo je plocha všeobecného útvaru?
  • Vtedy môžeme označiť metódu v nadtriede ako abstraktnú (neimplementovanú)
    • potom ale celú nadtriedu treba označiť ako abstraktnú, čo znamená, že nemôžeme tvoriť inštancie tejto triedy (také objekty by totiž nevedeli, čo robiť pri volaní area)
    • podtriedy, ak nie sú absktraktné, musia abstraktné metódy z predka implementovať

Tu je príklad:

abstract class Shape {   
    abstract double area();  // nepiseme telo metody

    void printArea() {       // neabstraktne metody mozu volat abstraktne
	System.out.println("Plocha je " + area());
    }

    // plus zvysok triedy
}
class Rectangle extends Shape {
    double area() {
 	return width*height;
    }
    // plus zvysok triedy
}
class Circle extends Shape {
    double area() {
	return Math.PI*radius*radius;
    }
    // plus zvysok triedy
}

// v main:
// Shape s = new Shape(10,20);  //! neskompiluje
Shape[] shapes = new Shape[2];
shapes[0] = new Rectangle(0,0,1,2);
shapes[1] = new Circle(0,0,1);
for(Shape x : shapes) {
    x.printArea();
}

Program vypíše

Plocha je 2.0
Plocha je 3.141592653589793

Hierarchia tried a trieda Object

  • V Jave sa dedí iba od jednej triedy (na rozdiel od napr. C++)
  • Dedenie je však možné viacúrovňovo
class Pes extends Zviera {
}
class Civava extends Pes {
}
  • Všetky triedy sú automaticky potomkami triedy Object
  • Trieda Object obsahuje metódy (napr. toString()), ktoré môžeme prekryť.
class Blabol /* extends Object */ { 
    @Override public String toString() { return "blabla"; }
}
// v metode main:
Blabol b = new Blabol();
System.out.println(b.toString());  
System.out.println(b);  // spravi toString automaticky
// PrintStream ma metody napr. println(Object x) aj println(String x)

Aritmetický strom s využitím dedenia

V minulom semestri sme mali problém s vrcholmi aritmetického stromu, že niektoré premenné sa používali len v niektorých vrcholoch stromu (napr. hodnota len v listoch, operátor len vo vnútorných vrcholoch). Tomuto sa vieme vyhnúť pomocou dedenia.

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

abstract class NularyNode extends Node {
}

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

abstract class BinaryNode extends Node {
    Node left;
    Node right;
    BinaryNode(Node left, Node right) { 
	this.left=left; this.right=right; 
    }
}

class Constant extends NularyNode {
    int value;
    Constant(int value) { this.value=value;}
    @Override public int evaluate() { return value;}
    @Override public String toString() { 
	return new Integer(value).toString();
    }
}

class UnaryMinus extends UnaryNode {
    UnaryMinus(Node child){ super(child); }
    @Override public int evaluate() { 
	return -child.evaluate();
    }
    @Override public String toString() { 
	return "(-"+child.toString()+")";
    }
}

class Plus extends BinaryNode { 
    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 Expression {

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

Rozhranie (interface)

Interface sa podobá na abstraktnú triedu, ale

  • neobsahuje premenné ani konštruktory, väčšinou ani implementácie metód
  • ide teda predošetkým o zoznam abstraktných metód, ktoré treba implementovať
    • slovo abstract netreba uvádzať, je tam implicitne
  • trieda môže implementovať viacero rozhraní - pripomína "viacnásobnú dedičnosť"

Jeden interface môže rozširovať iný - dopĺňať ho o ďalšie požadované funkcie

  • používame rovnako kľúčove slovo extends
interface Stack {
    void push(int item);
    int pop();
}
interface Printable {
    void print();
}
class LinkedStack implements Stack, Printable {
    static class Node {
        public int data;
        public Node next;
        public Node(int item, Node next_) {data=item; next=next_;}
    }

    private Node front;
    
    @Override public void push(int item) {
        Node p=new Node(item, front);
        front=p;
    }
    @Override public int pop() {
        if (front==null) return -1;
        int res=front.data;
        front=front.next;
        return res;
    }
    @Override public void print() {
        Node p=this.front;
        while (p!=null) {
            System.out.print(p.data+" ");
            p=p.next;
        }
        System.out.println();
    }    
}

class ArrayStack implements Stack, Printable {
    private int[] a;
    private int n;
    
    ArrayStack(){
	a=new int[100]; 
	n=0;
    }
    @Override public void push(int item){
        a[n]=item;
        n++;
    }
    
    @Override public int pop(){
        if (n<1) return -1;
	n--;
        return a[n];
    }
    @Override public void print(){
        for (int i=0; i<n; 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, že sa vie vypísať
        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ľ
  • prázdny: 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