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 č. 4

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

Oznamy

  • Počas najbližších cvičení, čiže v stredu 10. marca od 11:30 do 13:00 bude prebiehať prvý test. Bude pozostávať z troch úloh zameraných na látku z prvých troch týždňov, ktorých riešenia bude potrebné odovzdávať na testovač. Po dobu riešenia testu je potrebná účasť na stretnutí „Cvičenia” v MS Teams.
  • V stredu tiež budú zverejnené dve nebodované úlohy k štvrtým cvičeniam, zamerané na látku z dnešnej prednášky. Ďalšie podobne zamerané úlohy budú zverejnené na cvičeniach budúci týždeň.
  • Prvú domácu úlohu je potrebné odovzdať najneskôr do začiatku budúcej prednášky, čiže do pondelka 15. marca, 9:00.

Výnimky

Mechanizmus výnimiek (angl. exceptions) slúži v Jave na spracovanie chýb a iných výnimočných udalostí, ktoré môžu počas vykonávania programu nastať. Doposiaľ sme v našich programoch takéto situácie viac-menej ignorovali – napríklad sme obvykle predpokladali, že vstup vždy spĺňa požadované podmienky, že súbor, z ktorého sa pokúšame čítať, vždy existuje, atď. Dôvodom bola predovšetkým prílišná prácnosť ošetrovania chýb pomocou podmienok a neprehľadnosť programov, ktoré takéto podmienky obsahujú.

Uvažujme napríklad nasledujúci jednoduchý program, ktorý zo vstupu načíta prirodzené číslo n nasledované n celými číslami, ktoré postupne poukladá do poľa dĺžky n. Následne číta zo vstupu postupnosť indexov v rozmedzí 0n-1 a po každom načítaní indexu i zvýši hodnotu a[i] o jedna. Načítavanie vstupu sa ukončí po načítaní reťazca "KONIEC".

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int n = scanner.nextInt();
    int a[] = new int[n];
    for (int i = 0; i <= n - 1; i++) {
        a[i] += scanner.nextInt();
    }
    String next = scanner.next();
    while (!next.equals("KONIEC")) {
        a[Integer.parseInt(next)]++;
        next = scanner.next();
    }
}

Vykonávanie tohto programu môže skončiť chybou viacerými rôznymi spôsobmi: používateľ napríklad môže namiesto niektorého čísla zadať nečíselný reťazec; číslo n môže ďalej zadať ako záporné, čím vznikne chyba pri pokuse o alokovanie poľa; niektorý z ním zadaných indexov do poľa tiež nemusí byť v požadovanom rozmedzí. Po pokuse o ošetrenie týchto chýb pomocou podmienok dostávame nasledujúci horibilný program.

/** Nasledujuci kod je ukazkou toho, ako sa osetrovanie chyb v Jave NEMA robit. */

/** Metoda, ktora zisti, ci je retazec nebielych znakov reprezentaciou celeho cisla. */
private static boolean isInteger(String s) {
    Scanner stringScanner = new Scanner(s);
    return stringScanner.hasNextInt();
}

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int n = 0;
    if (scanner.hasNextInt()) {
        n = scanner.nextInt();
        if (n < 0) {
            System.err.println("Pocet prvkov pola nemoze byt zaporny.");
            System.exit(2);
        }
    } else {
        System.err.println("Vstup sa nezacina cislom.");
        System.exit(1);
    }
    int a[] = new int[n];
    for (int i = 0; i <= n - 1; i++) {
        if (scanner.hasNextInt()) {
            a[i] += scanner.nextInt();
        } else {
            System.err.println("Niektory z prvkov pola nebol zadany ako cele cislo.");
            System.exit(3);
        }
    }
    String next = scanner.next();
    while (!next.equals("KONIEC")) {
        if (isInteger(next)) {
            int i = Integer.parseInt(next);
            if (i >= 0 && i <= n - 1) {
                a[i]++;
            } else {
                System.err.println("Niektory z indexov do pola je mimo povoleneho rozmedzia.");
                System.exit(5);
            }
        } else {
            System.err.println("Niektory z indexov do pola nebol zadany ako cele cislo.");
            System.exit(4);
        }
        next = scanner.next();
    }
}

Vidíme, že už aj pri takto jednoduchej úlohe dostávame pomerne rozsiahly program, v ktorom väčšinu kódu zaberá práve spracovanie chýb. Ešte nepríjemnejšou je však skutočnosť, že ošetrovanie chýb je potrebné implementovať v mieste programu, kde táto chyba vznikla. To vedie k prelínaniu podstatných častí programu s časťami, ktoré slúžia iba na spracovanie chýb, v dôsledku čoho sa kód stáva veľmi neprehľadným. Z uvedených dôvodov sme až doposiaľ podobné chybové udalosti väčšinou ignorovali.

V praxi je ale nutné podobné chyby náležite poošetrovať – nie je totiž napríklad prípustné, aby textový editor spadol zakaždým, keď sa jeho používateľ pokúsi otvoriť súbor s neexistujúcim názvom. Výnimky poskytujú spôsob, ako spracovanie chybových udalostí realizovať podstatne efektívnejším spôsobom, než v ukážke vyššie. Medzi ich základné prednosti totiž patria:

  • Možnosť oddelenia kódu na spracovanie chýb od zvyšku kódu.
  • Možnosť jednoduchým spôsobom ponechať spracovanie chyby na volajúcu metódu v prípade, že sa to javí ako vhodnejšie riešenie.
  • Možnosť využívať pri spracúvaní chýb prostriedky objektovo orientovaného programovania.

Mechanizmus výnimiek v Jave

Pod výnimkou (angl. exception) sa v Jave rozumie inštancia špeciálnej triedy Exception, prípadne jej nadtriedy Throwable, reprezentujúca nejakú výnimočnú udalosť. Trieda Exception má pritom množstvo podtried reprezentujúcich rôzne typy chybových udalostí.

  • Výnimka môže počas vykonávania nejakej metódy f vzniknúť buď tak, že ju vyhodí JVM (napríklad pri delení nulou alebo pri prístupe k neexistujúcemu prvku poľa), alebo tak, že ju vyhodí sama táto metóda pomocou príkazu throw (detaily nižšie).
  • Vzniknutá výnimka môže byť priamo odchytená a ošetrená v rámci danej metódy f. V opačnom prípade je vykonávanie metódy ukončené a výnimka je posunutá metóde g, ktorá metódu f volala (posunieme sa teda v zásobníku volaní metód o úroveň nižšie).
  • V metóde g sa na celú situáciu dá pozerať tak, akoby výnimka vznikla pri vykonávaní príkazu, v rámci ktorého sa volala metóda f. Opäť teda môže byť výnimka buď odchytená a ošetrená, alebo rovnakým spôsobom predaná metóde, ktorá volala metódu g (aj v takom prípade hovoríme, že metóda g vyhodila výnimku, hoci reálne výnimka vznikla už v metóde f).
  • Takto sa v zásobníku volaní metód pokračuje nižšie a nižšie, až kým je nájdená metóda, ktorá danú výnimku odchytí a spracuje.
  • Ak sa takáto metóda na zásobníku volaní nenájde – čiže je výnimka vyhodená aj metódou main na jeho dne – typicky dôjde k ukončeniu vykonávania programu a k vypísaniu informácií o výnimke (vrátane zásobníka volaní metód) na štandardný chybový výstup; ide tu ale o zjednodušenie.
Vynimky.png

Chytanie a ošetrovanie výnimiek

Odchytenie a spracovanie výnimky možno v Jave realizovať nasledujúcim spôsobom:

  • Kód, v ktorom môže výnimka nastať, obalíme do bloku try.
  • Kód spracúvajúci výnimku umiestnime do bezprostredne nasledujúceho bloku catch. Za samotným kľúčovým slovom catch nasleduje jeden argument reprezentujúci výnimku, ktorá sa má odchytiť. Napríklad blok catch (Exception e) odchytí ľubovoľnú výnimku, ktorá je inštanciou triedy Exception (alebo nejakej jej podtriedy).
  • Kedykoľvek nastane v bloku try výnimka, okamžite sa vykonávanie tohto bloku ukončí. Ak je vzniknutá výnimka kompatibilná s argumentom bloku catch, pokračuje sa blokom catch a výnimka sa považuje za odchytenú. (Ak výnimka s týmto argumentom nie je kompatibilná, bude správanie rovnaké ako pri absencii bloku try.)

Príklad: Uvažujme program, ktorý prečíta zo vstupného súboru vstup.txt prirodzené číslo n a za ním n celých čísel. Napokon vypíše na konzolu súčet načítaných čísel. V takomto programe môže nastať výnimka predovšetkým z dvoch dôvodov: súbor vstup.txt nemusí existovať alebo môže nastať iná chyba pri pokuse o vytvorenie inštancie triedy Scanner; nemusí byť dodržaný požadovaný formát vstupného súboru a môže tak vzniknúť výnimka pri volaní metódy nextInt triedy Scanner. Kód teda môžeme obaliť do bloku try a prípadnú výnimku môžeme spracovať napríklad jednoduchým výpisom textu "Nieco sa pokazilo." do štandardného chybového výstupu. Všetok kód nasledujúci za blokom catch sa vykoná bez ohľadu na to, či výnimka nastala alebo nenastala.

import java.io.*;
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        try {
            Scanner scanner = new Scanner(new File("vstup.txt"));
            int n = scanner.nextInt();
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += scanner.nextInt();
            }
            System.out.println("Sucet je: " + sum + ".");
            scanner.close();
        } catch (Exception e) {
            System.err.println("Nieco sa pokazilo.");
        }
        System.out.println("Aj po odchytenej vynimke pokracujem dalej a vypisem tento text...");
    }
}

Namiesto výpisu textu "Nieco sa pokazilo." by sme napríklad mohli vypísať aj informácie o vzniknutej výnimke použitím metódy

e.printStackTrace();

Uvedené riešenie má ale dva menšie nedostatky:

  • V prípade vyhodenia výnimky nikdy nedôjde uzatvoreniu vstupného súboru, pretože vykonávanie bloku try sa preruší ešte predtým, než príde na rad príslušný príkaz. O niečo vhodnejšou alternatívou by bolo presunutie príkazu na zatvorenie scanner-a až za koniec bloku try; ale aj vtedy by sa mohlo stať, že sa tento príkaz nevykoná kvôli nejakej výnimke, ktorá nebola odchytená (napríklad inštancia typu Throwable, alebo výnimka vyhodená v bloku catch). Riešením je pridať za bloky catch blok finally, ktorý sa vykoná bez ohľadu na to, či nastala výnimka a či sa nám ju podarilo odchytiť (dokonca sa vykoná aj v prípade, že sa v try bloku úspešne vykonal príkaz return).
import java.io.*;
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(new File("vstup.txt"));
            int n = scanner.nextInt();
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += scanner.nextInt();
            }
            System.out.println("Sucet je: " + sum + ".");
        } catch (Exception e) {
            System.err.println("Nieco sa pokazilo.");
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
        System.out.println("Aj po odchytenej vynimke pokracujem dalej a vypisem tento text...");
    }
}
  • Nerozlišujeme medzi dvoma najpravdepodobnejšími príčinami vyhodenia výnimky: medzi chybou nejakým spôsobom súvisiacou s manipuláciou so súborom vstup.txt a medzi chybou spôsobenou zlým formátom vstupu. V prvom prípade sa vyhodí výnimka, ktorá je inštanciou triedy IOException z balíka java.io alebo nejakej jej podtriedy (napríklad FileNotFoundException). V druhom prípade pôjde o výnimku typu NoSuchElementException z balíka java.util, vyhodenú metódou nextInt triedy Scanner. Za blok try môžeme pridať aj viacero blokov catch pre viacero typov výnimiek. V prípade, že niektorá výnimka je „kompatibilná” s viacerými takýmito blokmi, bude odchytená prvým z nich.
import java.io.*;
import java.util.*;

public class Trieda {

    public static void main(String[] args) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(new File("vstup.txt"));
            int n = scanner.nextInt();
            int sum = 0;
            for (int i = 1; i <= n; i++) {
                sum += scanner.nextInt();
            }
            System.out.println("Sucet je: " + sum + ".");
        } catch (IOException e) {
            System.err.println("Chyba suvisiaca s pristupom k suboru vstup.txt.");
        } catch (NoSuchElementException e) {
            System.err.println("Zly format vstupneho suboru.");
        } catch (Exception e) {
            // Takto spracovat vynimku, o ktorej nic netusime, nie je uplne najlepsi napad (takze len na ukazku).
            System.err.println("Nejaka ina vynimka.");   
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
        System.out.println("Aj po odchytenej vynimke pokracujem dalej a vypisem tento text...");
    }
}

Hierarchia výnimiek

Hádzanie nových výnimiek

Výnimky vlastného typu

Generické programovanie

Úvod do Java Collections

Odkazy