FAV-ZCU/KIV PPA1/15. Výjimky.md

14 KiB
Raw Blame History

Výjimky

  • Mechanizmus výjimek je silným bezpečnostním mechanizmem nejen v Javě, ale i v mnoha jiných jazycích
  • Umožňuje (a v některých případech vynucuje) řešení chybových stavů, které za běhu programu mohou z různých důvodů nastat

Základní pojmy

  • Výjimka (exception)
    • Též výjimečný stav (exceptional event) či (nepřesně) chyba (error)
    • Událost, u které si nepřejeme, aby v programu vznikla
    • Např. očekáváme od uživatele jako vstup celé číslo, ale on zadá reálné
    • Bez výjimky by program fungoval lépe podle očekávání
  • Vyhození výjimky (throw exception)
    • Okamžik, kdy výjimka vznikne

Popis výjimky

  • Výjimka v Javě je instance konkrétní speciální třídy
  • Samotná třída výjimky udává, k jaké chybě došlo
  • Obsahuje upřesňující informace
    • Přesnější popis chyby
    • Obsah zásobníku (stack trace)

Druhy výjimek

  • Existují dva základní druhy výjimek
    • Exception
      • Je NUTNÉ je ošetřit ve zdrojovém kódu
    • RuntimeException
      • Je MOŽNÉ je ošetřit ve zdrojovém kódu, ale není to nutné
    • Způsob práce s oběma typy je zcela stejný
  • Od základních typů odvozeno (odděděno) mnoho dalších výjimek, které jsou více specifické
    • Výjimky mají hierarchickou strukturu, na vrcholu je Exception
    • Protože všechny jsou původně odvozené (odděděné) od základních, je možné při ošetřování výjimek používat názvy nadřazených nebo základních druhů
      • Např. FileNotFoundException (soubor nenalezen) je možné nahradit IOException (obecná chyba vstupu a výstupu) nebo rovnou Exception

Výjimky (Exception)

  • Též kontrolované výjimky (checked exceptions)
  • Všechny tyto výjimky jsou odvozené (přímo nebo přes delší hierarchii) od Exception a nikde v hierarchii odvození (dědění) se nevyskytuje RuntimeException
  • Je nutné se o ně v programu postarat, tj. ošetřit je přidáním speciální části zdrojového kódu
  • Vyskytují se v souvislosti s voláním metod, u kterých je zvýšená pravděpodobnost, že se při jejich použití může vyskytnout problém
    • Typicky metody, které pracují se vstupy a výstupy
    • Tyto metody jsou napsány tak, že při jejich použití je nutné na potenciální nebezpečí reagovat
      • Pokud není přidána část kódu pro ošetření případné výjimky, program nejde ani přeložit

Výjimky za běhu (RuntimeException)

  • Všechny tyto výjimky jsou odvozeny (přímo nebo přes delší hierarchii) od RuntimeException
  • Není nutné se o ně v programu postarat, protože mohou nastat kdekoliv
    • Příkladem je přístup do neexistujícího prvku pole (ArrayIndexOutOfBoundException), volání metody nad referenční proměnnou, která je null (NullPointerException), dělení nulou (ArithmeticException) a mnoho dalších
    • Program lze přeložit překladač nás nenutí, abychom je ošetřili pomocí speciální části zdrojového kódu
    • Považujeme-li to za vhodné, můžeme je ošetřit (stejným způsobem jako kontrolované výjimky)
      • Typicky je to v místech, kde je zvýšená pravděpodobnost problému (např. vstupy od uživatele)
    • Nemá větší smysl ošetřovat tyto výjimky v místě, kde jsme si stoprocentně jisti, že výjimka nemůže nastat (nemůže být vyhozena)
      • Přesto se může stát, že výjimka v tomto místě nastane, protože jsme nezvážili všechny možnosti
    • Pokud je výjimka za běhu programu vyhozena a není ošetřena, dojde k předčasnému ukončení programu a výpisu výjimky na standardní chybový výstup (na obrazovku)
      • Z chybového výpisu lze poznat, kde k výjimce došlo
      • Dalším krokem je oprava programu odstranění důvodu výjimky nebo její ošetření

Reakce na výjimku

  • Existují tři základní přístupy, jak reagovat na výjimku
    • Předání výjimky výše
      • Programátor výjimku nechce (nebo neumí) ošetřovat v místě vyhození, a proto informaci o jejím výskytu předá do nadřazené úrovně (tzn. do volající metody)
    • Kompletní ošetření výjimky
      • Výjimka je zachycena a ošetřena v místě vyhození (vzniku)
    • Kombinace obou přístupů
      • Výjimka je zachycena a (částečně či kompletně) ošetřena v místě vyhození, ale informace o ní se navíc předá do nadřazené úrovně (tzn. do volající metody)
  • Všechny tyto postupy se běžně používají
    • Často se využívá nejprve předání výjimky výše a následně její ošetření ve vyšší úrovni

Předání výjimky výše (deklarace výjimky)

  • Metoda, ve které se výjimka vyskytla (byla vyhozena), se zříká odpovědnosti za její zpracování
    • Předání odpovědnosti výše se deklaruje v hlavičce metody
    • Tento postup je velice jednoduchý, ale pouze odsouvá řešení problému, které bude muset být nakonec provedeno (ale toto řešení může být např. provedeno na vhodnějším místě, než je metoda, kde výjimka vznikla)
    • Na první pohled nejjednodušší řešení
      • Není třeba žádných zásahů do těla metody
      • Do hlavičky metody se přidá throws NázevVýjimky
        • Např. throws IOException

Kompletní ošetření výjimky

  • Případná vyhozená výjimka nepronikne ven z metody
    • Nadřazená úroveň (volající metoda) se nemusí o nic starat
  • Ošetření výjimky se provede pomocí konstrukce try-catch-finally
    • Vlastní výkonný kód se nemění, jen se uzavře do bloku začínajícího klíčovým slovem try
      • Prakticky je často nutné přesunout některé deklarace vně bloku try
      • V tomto bloku předpokládáme vznik výjimky
      • Výjimka vždy vznikne na konkrétní řádce => touto řádkou skončí vykonávání bloku try, pokud k výjimce dojde
    • Blok catch, který za blokem try bezprostředně následuje, určuje, na jakou výjimku se bude reagovat a jak
      • Kód v bloku catch se provede pouze v případě, že k uvedené výjimce (někde v bloku try) dojde
      • Bloků catch může být více, každý může reagovat na jinou výjimku
      • Blok catch reaguje pouze na výjimku, která je deklarovaná v závorce a všechny výjimky od ní odvozené (potomky)
        • Pokud nastane jiná výjimka, blok catch se neprovede
        • Pokud není uveden žádný vhodný blok catch, výjimka není odchycena
    • Blok finally
      • Blok finally následuje za blokem catch
      • Obsahuje část kódu, která se provede vždy ať už k výjimce dojde, nebo ne (i když dojde k jiné výjimce, která není odchycena)
      • Nepoužívá se příliš často
  • Možné ošetření výjimky pouze vypsání chyby a ukončení programu
    • Primitivní, ale často rozumný způsob
    • Hodí se v případech, kdy vznik výjimky znamená, že program dál stejně nemůže smysluplně pokračovat
    • Umožní program přeložit (pokud ošetřujeme kontrolovanou výjimku)
    • V případě, že je výjimka vyhozena, se o ní z chybového výpisu dozvíme
    • Pokud ošetřujeme výjimku přímo v místě vzniku v nějaké hluboce zanořené metodě, je to často jediný smysluplný způsob kompletního ošetření výjimky
      • Vhodnější alternativa je předání výjimky výše a její ošetření na vyšší úrovni, kde se dá smysluplně reagovat
  • Ošetření více různých výjimek
    • Za blokem try může následovat více bloků catch, odchytávajících různé výjimky
      • Je tak možné reagovat různě podle toho, k jaké výjimce došlo
    • Je možné reagovat i na více různých výjimek stejně
      • Je možné uvést výjimku, která je společným předkem všech výjimek, na které chceme reagovat stejně často to může být přímo Exception
        • V tom případě jsou ale odchyceny všechny výjimky (i ty, o kterých jsme neuvažovali) a to není žádoucí
      • Je možné uvést více výjimek do jednoho bloku catch
        • Výjimky jsou odděleny svislítkem „|“
        • Lepší postup, protože jsou zachycené pouze uvedené výjimky (a výjimky od nich odvozené), ale žádné další

Předání výjimky výše a její ošetření výše

  • Běžný postup
  • Výjimka je z metod na nižší úrovni (např. zajišťující čtení ze souboru) propagována výše, až do úrovně, kde je vhodné ji ošetřit, a tam je výjimka vhodně ošetřena (např. v GUI zobrazením dialogu s varováním)

Nejhorší reakce na výjimku

  • Důvod je v naprosté většině případů lenost programátora
    • Nechce výjimku propagovat, protože by to následně musel udělat i ve všech volajících metodách
    • Použije se konstrukce try-catch, ale do bloku catch se nic nenapíše
  • Zmíněným postupem jsme se připravili o všechny výhody mechanizmu výjimek a vlastně jsme situaci ještě zhoršili
    • Výjimka proběhne, ale nikdo se nedozví kdy a kde
  • Někdy je však skutečně potřeba na zachycenou výjimku nijak nereagovat a také ji nepropagovat
    • Typickým příkladem je odchycení InterruptedException při synchronizaci vláken (viz předmět KIV/PGS)
    • V tom případě je možné nechat blok catch bez výkonného kódu, ale je nutné doplnit dostatečně podrobný vysvětlující komentář, proč tomu tak je
    • Je také možné převést kontrolovanou výjimku na výjimku za běhu

Konstrukce try-with-resources

  • Po skončení práce se soubory (čtení/zápis) je obecně dobrým zvykem soubor zavřít metodou close()
    • V předchozích příkladech nebylo použito
      • Protože to nebylo z hlediska výkladu podstatné a bude to podrobněji zmíněno později
      • Protože neuzavřením souboru data.txt se ve skutečnosti nic špatného nestane
        • Byl otevřen pouze pro čtení
        • JVM při ukončení programu (ať už s výjimkou nebo bez) automaticky uzavře všechny soubory, které program otevřel
    • Neuzavření souboru však může vadit např. při zápisu do něj, a proto je rozumné soubory uzavírat vždy
  • Čtení nebo zápis do souboru může často skončit výjimkou
  • Pokud se vyskytne výjimka, program neprovede část kódu uvedenou za řádkou, kde výjimka nastala
    • Buď okamžitě ukončí metodu (pokud je výjimka předána výše) nebo skočí do odpovídajícího bloku catch
    • V této části kódu (která se při vyhození výjimky neprovede) se typicky vyskytuje volání metody close() pro uzavření souborů, které se tím pádem při výskytu výjimky neprovede
      • Řešení
        • Přesunout volání close() do bloku finally
          • Běžný postup, ale metoda close() může také vyhodit výjimku a tu je třeba ošetřit
        • Využít konstrukci try-with-resources
  • Konstrukce try-with-resources
    • Umožňuje automatické uzavření souboru, bez ohledu na to, zda k výjimce došlo nebo ne
    • Instance použitá pro práci se souborem (např. instance třídy Scanner) se uvede do kulatých závorek za klíčové slovo try
    • Třída musí implementovat rozhraní AutoCloseable
    • Splňují knihovní třídy pro práci se soubory
    • V závorce za klíčovým slovem try může být uvedeno i více instancí různých tříd, jednotlivé deklarace jsou odděleny středníkem

Ruční vyhození výjimky

  • Výjimka může v programu nejen vzniknout, může být i vyhozena ručně
    • Příkaz throw výjimka;
      • POZOR! Nikoliv throws
    • Např. throw new IOException("Soubor je prazdny.");
      • Vytvořena a vyhozena nová instance výjimky IOException
      • Za příkazem throw je normální vytvoření instance výjimky jedním z jejích konstruktorů
    • Např. throw ex;
      • Vyhozena existující instance výjimky, na kterou ukazuje referenční proměnná ex
      • Vyhozena může být kontrolovaná výjimka (Exception) i výjimka za běhu (RuntimeException)
      • Ruční vyhození výjimky se používá poměrně často
        • Při zadání nesprávných parametrů metody
        • Při kombinaci ošetření výjimky v místě výskytu a předání výjimky výše

Vyhození výjimky při zadání nesprávných parametrů metody

  • Způsob, jak zareagovat např. v setru při zadání neplatné hodnoty atributu
    • Dosud jsme řešili nevhodně tím, že se v případě zadání neplatné hodnoty nic nestalo
    • Výjimka je vyhozena příkazem throw
    • Pokud se jedná o kontrolovanou výjimku (Exception), musí být v hlavičce metody uvedena za klíčovým slovem throws
    • Pokud se jedná o výjimku za běhu (RuntimeException), nemusí být v hlavičce metody uvedena
      • Přesto je dobré ji tam uvést
        • Dá se tak jasně najevo, jakou výjimku či výjimky může metoda vyhazovat
        • Pokud se jedná o výjimku za běhu, není nutné výjimku ošetřovat při volání metody, i když je uvedena v hlavičce
    • Příklad vyhození vlastní výjimky v setru ve třídě Ctverec
      • Použití třídy Ctverec je ve třídě VlastnostiCtverce

Ošetření výjimky v místě výskytu a její předání výše

  • Výjimka je v místě výskytu (částečně nebo úplně) ošetřena, ale informace o ní je propagována výše
  • Propagování výše se provede v bloku catch příkazem throw
    • Výjimky jsou zachyceny blokem catch, ve kterém se provede ošetření (zde není žádné potřeba) a výjimka se následně předá výše
    • Uzavření souboru (pokud je otevřen) se provede v bloku finally (chceme ho zavřít v každém případě, ať už výjimka vznikla nebo ne)
  • Pokud potřebujeme v ošetření výjimky provést pouze uzavření souboru (a ne jinou akci), je možné (a lepší) použít konstrukci try-with-resources
    • Bloky catch ani finally se vůbec neuvedou, ale k uzavření souboru dojde
    • Kvůli chybějícímu bloku catch se výjimka ani nezachytí a není tedy ani nutné znovu ji vyhazovat příkazem throw