Programovanie (2) v Jave
1-INF-166, LS 2016/17

Úvod · Pravidlá · Prednášky · Projekt · Netbeans · Odovzdávanie · 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).
· Predvádzanie projektov bude v pondelok 5.6. od 9:00 do 12:00 a v utorok 6.6 od 12:00 do 13:30 (po skúške), oboje v miestnosti M217. Na termín sa prihláste v AIS. Ak robíte vo dvojici, prihlási sa iba jeden člen dvojice.
· Body zo záverečného testu sú na testovači. Poradie príkladov: P1: do šírky, P2: topologické, P3: výnimky, P4: iterátor, P5: testy, P6: strom. Bolo potrebné získať aspoň 20 bodov zo 40.
· Opravný test bude 19.6.2017 od 9:00 v miestnosti M-I. Na termín sa prihláste v AISe.
· Zapisovanie známok a osobné stretnutia ku skúške budú v utorok 13.6. 13:30-14:30 v M163 a v stredu 14.6. 14:00-14:30 v M163.


Prednáška 28

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

Organizačné poznámky

  • DÚ13 do dnes
  • DÚ14 termín o dva týždne (OOP, dedenie). Začnite na nej pracovať skôr, nenechávajte si ju na poslednú chvíľu.
  • Informácie o projekte, ak ste pokročilí a chcete na ňom robiť skôr, ako preberieme tvorbu grafických prostredí v Java FX na prednáške.

Výnimky

  • Počas behu programu môže dôjsť k rôznym chybám a neobvyklým situáciám, napr.
    • neexistujúci súbor, zlý formát súboru
    • málo pamäte pri alokovaní polí, objektov
    • adresovanie mimo hraníc poľa, delenie nulou, ...
  • Doteraz sme v našich cvičných programoch ignorovali chyby
  • Programy určené pre užívateľov a kritické programy, ktorých zlyhanie by mohlo spôsobiť škody, by sa s takýmito situáciami mali vedieť rozumne vyrovnať
  • Ošetrovanie chýb bez požitia výnimiek
    • Do návratového kódu funkcie musíme okrem samotnej hodnoty zakomponovať aj ohlasovanie chýb
    • Po každom príkaze, ktorý mohol spôsobiť chybu, musíme existenciu chyby otestovať a vyrovnať sa s tým
    • Vedie to k neprehľadným programom

Malý príklad s načítaním poľa

Príklad: pseudokód funkcie, ktorá načíta zo súboru číslo n, naalokuje pole a načíta do poľa n čísel:

funkcia readArray {
  otvor subor vstup.txt
  if (nepodarilo sa otvorit) {
    return chybovy kod
  } 
  nacitaj cislo n
  if (nepodarilo sa nacitat n) {
    zatvor subor
    return chybovy kod
  }
  alokuj pole a velkosti n
  if (nepodarilo sa alokovat pole) {
    zatvor subor
    return chybovy kod
  }
  for (int i=0; i<n; i++) {
     nacitaj cislo a uloz do a[i]
     if (nepodarilo sa nacitat) {
         zatvor subor
         odalokuj pole 
         return chybovy kod 
     }
  }
  zatvor subor
  return naalokovane pole, beh bez chyby
  • Premiešané príkazy, ktoré niečo robia a ktoré ošetrujú chyby
  • Ľahko môžeme zabudnúť odalokovať pamäť alebo zavrieť súbor
  • Volajúca funkcia musí analyzovať chybový kód, môže potrebovať rozlišovať napr. problémy so súborom a s pamäťou
  • Chyba môže nastať aj pri zatváraní súboru

Jednoduché použite výnimiek v Jave

  • Prepíšme náš predchádzajúci príklad s výnimkami
    static int[] readArray(String filename) {
        Scanner s = null;
        int[] a = null;
        try {
            s = new Scanner(new File(filename));
            int n = s.nextInt();
            a = new int[n];
            for (int i = 0; i < n; i++) {
                a[i] = s.nextInt();
            }
            s.close();
            return a;
        } catch (Exception e) {
            if (s != null) {
                s.close();
            }
            e.printStackTrace();
            return null;
        }
    }
  • Využívame konštrukty try a catch.
  • Do try bloku dáme príkazy, z ktorých niektorý môže zlyhať.
  • Ak niektorý zlyhá a vyhodí výnimku, okamžite sa ukončí vykonávanie bloku try a pokračuje sa blokom catch. V bloku catch túto výnimku spracujeme, v našom prípade len debugovacím výpisom.
  • Ak sa podarilo čísla načítať do poľa, metóda vráti pole, inak vráti null

Ako všelijako môže zlyhať

Rôzne príklady, ako môže táto metóda zlyhať:

  • Príkazu na inicializáciu Scannera pošleme meno neexistujúceho súboru:
java.io.FileNotFoundException: vstup.txt (No such file or directory)
        at java.io.FileInputStream.open(Native Method)
        at java.io.FileInputStream.<init>(FileInputStream.java:137)
        at java.util.Scanner.<init>(Scanner.java:653)
        at prog.Prog.readArray(Prog.java:17)
        at prog.Prog.main(Prog.java:10)
  • V súbore sú nečíselné údaje:
java.util.InputMismatchException
        at java.util.Scanner.throwFor(Scanner.java:857)
        at java.util.Scanner.next(Scanner.java:1478)
        at java.util.Scanner.nextInt(Scanner.java:2108)
        at java.util.Scanner.nextInt(Scanner.java:2067)
        at prog.Prog.readArray(Prog.java:18)
        at prog.Prog.main(Prog.java:10)
  • Ak nie je dosť pamäte na pole a (toto ani nie je Exception, ale Error, takže náš catch to nezachytil, pozri ďalej)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at prog.Prog.readArray(Prog.java:19)
        at prog.Prog.main(Prog.java:10)
  • Ak je číslo n v súbore záporné
java.lang.NegativeArraySizeException
        at prog.Prog.readArray(Prog.java:19)
        at prog.Prog.main(Prog.java:10)
  • Súbor končí skôr ako sa načíta n čísel
java.util.NoSuchElementException
        at java.util.Scanner.throwFor(Scanner.java:855)
        at java.util.Scanner.next(Scanner.java:1478)
        at java.util.Scanner.nextInt(Scanner.java:2108)
        at java.util.Scanner.nextInt(Scanner.java:2067)
        at prog.Prog.readArray(Prog.java:21)
        at prog.Prog.main(Prog.java:10)
  • Dali by sa vyrobiť aj ďalšie prípady (napr. filename==null)
  • V dokumentácii sa o každej metóde dočítame, aké výnimky produkuje za akých okolností

Rozpoznávanie typov výnimiek

  • Možno by náš program mal rôzne reagovať na rôzne typy chýb, napr.:
    • Chýbajúci súbor: vypýtať si od užívateľa nové meno súboru
    • Zlý formát súboru: ukázať užívateľovi, kde nastala chyba, požiadať ho, aby ju opravil alebo zadal nové meno súboru
    • Nedostatok pamäte: program vypíše, že operáciu nie je možné uskutočniť vzhľadom na málo pamäte
  • Toto vieme spraviť, lebo výnimky patria do rôznych tried dedených z triedy Exception (prípadne z vyššej triedy Throwable)
  • K jednému príkazu try môžeme mať viacero príkazov catch pre rôzne triedy výnimiek, každý chytá tú triedu a jej podtriedy
    • Pri danej výnimke sa použije najvrchnejší catch, ktorý sa na ňu hodí
  • Po blokoch try a catch môže nasledovať blok finally, ktorý sa vykoná vždy, bez ohľadu na to, či nastala výnimka a či sa nám ju podarilo odchytiť nejakým príkazom catch
    • V tomto bloku môžeme napr. pozatvárať otvorené súbory a pod.

Jednoduchý príklad, ktorý vypíše rôzne hlášky pre rôzne typy chýb:

    static int[] readArray(String filename) {
        Scanner s = null;
        int[] a = null;
        try {
            s = new Scanner(new File(filename));
            int n = s.nextInt();
            a = new int[n];
            for (int i = 0; i < n; i++) {
                a[i] = s.nextInt();
            }
            return a;
        } catch (FileNotFoundException e) {
            System.err.println("Subor nebol najdeny");
            return null;
        } catch(java.util.NoSuchElementException e) {
            System.err.println("Zly format suboru");
            return null;
        } catch(OutOfMemoryError e) {
            System.err.println("Nedostatok pamate");
            return null;
        } catch(Throwable e) {
            System.err.println("Neocakavana chyba pocas behu programu");
            return null;
        } finally {
            if (s != null) {
                s.close();
            }
        }
    }
  • catch pre java.util.NoSuchElementException chytí aj InputMismatchException, ktorá je jej podtriedou, takže zahŕňa prípady keď súbor nečakane končí, aj keď v ňom nie sú číslené dáta
    • do tejto kategórie by sme chceli zaradiť aj prípad, kedy je n záporné, ale ten skončí na všeobecnej Throwable
    • to vyriešime tým, že hodíme vlastnú výnimku (viď nižšie)

Prehľad tried z tohto príkladu, plus niektorých ďalších, ktoré sa často vyskytujú:

Object
 |
 |-- Throwable
      |
      |-- Error  vážne systémové problémy
      |    |
      |    |-- VirtualMachineError
      |         |
      |         |-- OutOfMemoryError
      |
      |-- Exception
           |
           |-- IOException
	   |    |
	   |    |-- FileNotFoundException
           |
           |-- RuntimeException
                |
                |-- IndexOutofBoundsException
                |
		|-- NegativeArraySizeException
                |
                |-- NoSuchElementException
                |    |
                |    |-- InputMismatchException
                | 
                |-- NullPointerException

Hádzanie výnimiek, vlastné triedy výnimiek

  • Výnimku vyhodíme príkazom throw, pričom musíme vytvoriť objekt nejakej vhodnej triedy, ktorá podtriedou Throwable
  • V našom príklade pre záporné n môžeme vyhodiť objekt triedy java.util.NoSuchElementException, ktorý sa spracuje rovnako ako iné chyby s formátom súboru
            int n = s.nextInt();
            if(n<0) {
                throw new java.util.NoSuchElementException();
            }
  • Nie je to však elegantné riešenie, lebo táto trieda reprezentuje iný typ udalosti
  • Môžeme si vytvoriť aj vlastnú triedu, ktorá v premenných môže mať uložené podrobnejšie informácie o chybe, ktorá nastala.
    • Väčšinou to bude podtrieda triedy Exception
    static class WrongFormatException extends Exception {

        private String filename;

        public WrongFormatException(String filename) {
            this.filename = filename;
        }

        @Override
        public String getMessage() {
            return "Zly format suboru " + filename;
        }
    }

Propagácia a zreťazenie výnimiek

  • Ak vznikne výnimka v príkaze, ktorý nie je vo vnútri try-catch bloku, alebo ak jej typ nie je zachytený žiadnym catch príkazom, hľadá sa ďalší try-catch blok, napr. vo volajúcej metóde
  • Ak výnimku nikto nechytí, program skončí s chybovým výpisom zásobníka
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at prog.Prog.readArray(Prog.java:19)
        at prog.Prog.main(Prog.java:10)
  • Pri spracovaní výnimky v bloku catch je možné hodiť novú výnimku (trebárs vhodnejšieho typu)
  • Metóda musí deklarovať všetky výnimky, ktoré hádže, alebo ktoré v nej môžu vzniknúť a ich nechytá
    • Neplatí pre výnimky triedy RuntimeException a jej podtried a pre Throwable, ktoré nie sú výnimka (ale napr. Error)

Nasledujúci program si pýta meno súboru, až kým nenájde súbor, ktorý vie načítať

  • V metóde readArray spracuje chyby týkajúce sa formátu súboru a hodí novú výnimku typu WrongFormatException.
  • V metóde main spracuje WrongFormatException a FileNotFoundException tak, že sa znovu pýta meno súboru.
  • Iné nečakané udalosti, napr. málo pamäte, koniec vstupu od užívateľa a pod. spôsobia ukončenie programu s chybovou hláškou.
    public static void main(String[] args) {

        boolean fileRead = false;
        Scanner s = new Scanner(System.in);
        int[] a = null;
        while (!fileRead) {
            try {
                System.out.println("Zadaj meno suboru: ");
                String filename = s.next();
                a = readArray(filename);
                fileRead = true;
                System.out.println("Dlzka pola je " + a.length);
            } catch (WrongFormatException e) {
                System.out.println(e.getMessage());
            } catch (FileNotFoundException e) {
                System.out.println("Subor nebol najdeny.");
            } catch(Throwable e) {
                System.out.println("Neocakavana chyba.");
                System.exit(1);
            }
        }
    }

    static int[] readArray(String filename)
            throws WrongFormatException, FileNotFoundException {
        Scanner s = null;
        int[] a = null;
        try {
            s = new Scanner(new File(filename));
            int n = s.nextInt();
            if (n < 0) {
                throw new WrongFormatException(filename);
            }
            a = new int[n];
            for (int i = 0; i < n; i++) {
                a[i] = s.nextInt();
            }
            return a;
        } catch (java.util.NoSuchElementException e) {
            throw new WrongFormatException(filename);
        } finally {
            if (s != null) {
                s.close();
            }
        }
    }

Zhrnutie

  • Keď vznikne neočakávaná udalosť, môžeme ju signalizovať vyhodením výnimky pomocou príkazu throw.
  • Výnimka je objekt triedy, ktorá je podtriedou Throwable
  • Pri ošetrovaní sa nájde a vykoná najbližší vyhovujúci try ... catch blok obkolesujúci príkaz throw, v tej istej alebo niektorej volajúcej metóde, ďalej sa pokračuje za týmto blokom
  • Blok finally sa vykoná vždy, keď je aj keď nie je výnimka a aj ak sa výnimku nepodarilo chytiť. Slúži na zatváranie súborov a iné upratovacie práce.
  • Niektoré typy neodchytených výnimiek treba deklarovať v hlavičke funkcie.

Ďalšie informácie

Generické programovanie

  • V minulom semestri sme videli rôzne abstraktné dátové typy a dátové štruktúry, napr. zásobník, rad, slovník, spájaný zoznam,...
  • V každej sme museli zadefinovať, akého typu dáta bude obsahovať
  • Ak teda potrebujeme zásobník intov aj zásobník reťazcov, museli sme všetky funkcie písať dvakrát, čo prináša problémy

Zásobník dát typu Object

  • Dedenie nám prináša jedno riešenie tohto problému - zadefinovať typ dát ako Object a nakoľko všetky triedy sú podtriedami tejto triedy, môžeme ich v zásobníku skladovať
   class Node {
        private Object data;
        private Node next;
        public Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
        public Object getData() {
            return data;
        }
        public Node getNext() {
            return next;
        }
    }

    class Stack {
        private Node front;
        public void push(Object data) {
            Node p = new Node(data, front);
            front = p;
        }
        public Object pop() {
            Object res = front.getData();
            front = front.getNext();
            return res;
        }
    }

Teraz môžeme do zásobníka dávať rôzne veci:

        Stack s = new Stack();
        s.push(null);
        s.push("Hello world!");  // String je potomok Object
        s.push(new int[4]);  // pole sa tiez vie tvarit ako objekt
        int x = 4;
        s.push(x);           // kompilator vytvori objekt typu Integer

Ale pozor, keď vyberáme zo zásobníka, majú typ Object, musíme ich teda pretypovať:

       int y = (Integer)s.pop();  // ok 
       int z = (Integer)s.pop();  // java.lang.ClassCastException

Pre pretypovaní teda môže dôjsť k chybe počas behu programu, radšej sme, keď chybu objaví kompilátor.

Zásobník ako generická trieda

  • Zadefinujeme parametrický typ class Stack <T>, kde T je parameter reprezentujúci typ objektov, ktoré do zásobníka budeme dávať.
  • V definícii triedy namiesto konkrétneho typu (napr. Object), použijeme parameter T
  • Keď vytvárame nový zásobník, špecifikujeme typ T: Stack<Integer> s = new Stack<Integer>();
    • Potom do neho môžeme vkladať objekty triedy Integer a jej podtried
    class Node <T> {
        private T data;
        private Node <T> next;
        public Node(T data_, Node<T> next_) {
            data = data_;
            next = next_;
        }
        public T getData() {
            return data;
        }
        public Node <T> getNext() {
            return next;
        }
    }

    class Stack <T> {
        private Node<T> front;
        public void push(T data) {
            Node<T> p = new Node<T>(data, front);
            front = p;
        }

        public T pop() {
            T res = front.getData();
            front = front.getNext();
            return res;
        }
    }

Použitie zásobníka:

        Stack<Integer> s = new Stack<Integer>();
        s.push(new Integer(4));
        s.push(5);        
        Integer y = s.pop();
        int z = s.pop();

V tom istom programe môžeme vytvoriť zásobníky veľa rôznych typov.

Skratka:

  • namiesto Stack<Integer> s = new Stack<Integer>(); stačí písať Stack<Integer> s = new Stack<>();
    • kompilátor z kontextu určí, že v <> má byť Integer

Generické metódy

Aj jednotlivé metódy môžu mať typový parameter, ktorý sa píše pred návratový typ.

Statická metóda, ktorá dostane zásobník s prvkami typu T a vyprázdni ho.

    static <T> void emptyStack(Stack<T> s) {
        while(!s.isEmpty()) {
            s.pop();
        }
    }
        Stack<String> s = new Stack<String>();
        s.push("abc");
        Prog.<String>emptyStack(s);  // alebo len emptyStack(s);

Statická metóda, ktorá dostane pole s prvkami typu E a naplní ho referenciami na prvok e.

    static <E> void fillArray(E[] a, E e) {
        for(int i=0; i<a.length; i++) {
            a[i] = e;
        }
    }
        Integer[] a = new Integer[3];
        fillArray(a, 4);

        int[] b = new int[3];
        //fillArray(b, 4);  // E musí byť objekt, nie primitívny typ

Generické triedenie, rozhranie Comparable

Ukazovali sme si aj algoritmy na triedenie, aj tie pracovali s poľom konkrétneho typu.

  • Triediacu funkciu môžeme spraviť generickú, potrebujeme však prvky porovnávať
  • Operátory <, <= atď pracujú len s primitívnymi typmi
  • Použijeme teda špeciálnu metódu compareTo špecifikovanú v rozhraní Comparable
    • x.compareTo(y) vráti zápornú hodnotu, ak x<y, nulu ak x=y a kladnú hodnotu ak x>y
    • potrebujeme, aby prvky poľa boli z triedy, ktorá implementuje toto rozhranie, čo zapíšeme ako <E extends Comparable>

Jednoduché generické triedenie vkladaním:

    static <E extends Comparable> void sort(E[] a) {
        for (int i = 1; i < a.length; i++) {
            E prvok = a[i];
            int kam = i;
            while (kam > 0 && prvok.compareTo(a[kam - 1]) < 0) {
                a[kam] = a[kam - 1];
                kam--;
            }
            a[kam] = prvok;
        }
    }

    public static void main(String[] args) {
        Integer[] a = {3, 1, 2};
        sort(a);
        for (int i = 0; i < a.length; i++) {
            System.out.println(a[i]);
        }
    }

Java Collections

  • Java poskytuje štandardné triedy na mnohé často používané dátové štruktúry, používa generické programovanie
  • Tutoriál
  • Je dobré tieto triedy poznať a podľa potreby využívať
  • Pochopením ich štruktúry si tiež môžeme precvičiť objektovo-orientované programovanie
  • Na úvod si ukážeme malá ukážka, viac nabudúce

ArrayList

ArrayList sa podobá na vector z C++ (existuje aj trieda Vector)

  • ide o štruktúru reprezentujúcu pole, ktoré rastie podľa potreby
  • na koniec poľa pridávame metódou add(prvok), konkrétny prvok adresujeme metódou get(index), meníme cez set(index, hodnota), veľkosť poľa je size()
import java.util.ArrayList;

...
        ArrayList<Integer> a = new ArrayList<Integer>();
        a.add(2);
        a.add(7);
        for (int i = 0; i < a.size(); i++) {
            System.out.println(a.get(i));  // vypiseme vsetky prvky pola
            a.set(i, -1);                  // a potom ich prepiseme na -1
        }

LinkedList

LinkedList je obojsmerný spájaný zoznam, ktorý môžeme použiť napr. ako zásobník alebo rad

  • Vie teda efektívne pridávať a uberať prvky z oboch koncov a tiež prejsť cez všetky prvky zoznamu pomocou iterátora.
  • Hľadanie prvku na pozícii i niekde v strede zoznamu je pomalé.
        LinkedList<Integer> a = new LinkedList<Integer>();
        a.addFirst(2);   // to iste ako push
        a.addLast(7);    // to iste ako add
        for (ListIterator<Integer> it = a.listIterator(); it.hasNext(); ) {
            System.out.println(it.next());
        }
        a.removeFirst(); // to iste ako pop
        a.removeLast();

Prehľad Collections

  • Dátové štruktúry a algoritmy na základnú prácu so skupinami dát.
  • Generické typy - môžeme vytvárať dátové štruktúry pre dáta rôznych typov.
  • Definované pomocou rozhraní, jedno rozhranie (interface) môže mať viacero implementácií.

Vybrané triedy:

Rozhranie Význam Implementácie
Collection skupina objektov
- Set množina, skupina bez opakujúcich sa objektov HashSet
-- SortedSet množina s definovaným usporiadaním prvkov TreeSet
- List postupnosť objektov s určitým poradím ArrayList, LinkedList
Map slovník, asociatívne pole, mapuje kľúče na hodnoty HashMap
- SortedMap slovník s definovaným usporiadaním kľúčov TreeMap

V metódach je dobré argumenty definovať najvšeobecnejším vhodným rozhraním alebo triedou.

  • Napr. chceme spočítať súčet viacerých Integer-ov:
// tato metoda sa da pouzit iba na ArrayList
public static Integer sum(ArrayList<Integer> a) { ... }
// tato metoda sa da pouzit na hocijaku Collection (LinkedList, HashSet...)
public static Integer sum(Collection<Integer> a) { ... }

Základné operácie pre Collection:

public interface Collection<E> extends Iterable<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object element);

    boolean add(E element);  // optional
    boolean remove(Object element); // optional
    void clear(); // optional

    Iterator<E> iterator();

    Object[] toArray();
    <T> T[] toArray(T[] a);

   // a dalsie...
}
  • Metódy add a remove vracajú true, ak sa Collection zmenila a false, ak sa nezmenila.
  • Metódy, ktoré menia Collection, sú nepovinné v tom zmysle, že musia byť zadefinované, ale môžu hádzať UnsupportedOperationException