Poglavje 7
Izjeme

7.1 Kaj so izjeme?

Med izvajanjem programa lahko pride do mnogo t.i. posebnih okoliščin (napak, težav). Nekatere med njimi so tako resne, da mora program zaradi njih končati (na primer: pride do napake v navideznem stroju, zmanjka pomnilnika, ...), druge pa so take, da se jih da s pravilnimi postopki obvladati (na primer: če datoteka, iz katere hočemo brati, ne obstaja, lahko izberemo drugo datoteko). Model izjem programerjem olajša delo, saj omogoča avtomatizirano upravljanje s posebnimi okoliščinami.

7.2 Izvor izjemnih okoliščin

Izjemne okoliščine so posledica:

Izjemne okoliščine so neizbežne, saj

7.3 Primer izjemne okoliščine

Kaj se zgodi, ce poskušamo izvesti naslednjo kodo?

 

  int a=3/0;

V programskem jeziku C bi se program, v katerem bi prišlo do izvajanja te vrstice, končal, na zaslon pa bi se izpisalo:


Floating point exception.


Podobno bi se zgodilo v Javi - izpisalo bi se


java.lang.ArithmeticException: / by zero


in program bi se končal. Vendar obstaja BISTVENA razlika med obema jezikoma: v programskem jeziku C napake deljenja z nič ne moremo “prestreči”, v Javi pa jo lahko in sicer s pomočjo mehanizma za prestrezanje izjem (exception handling). Ta omogoča:

Osnovna ideja prestrezanja izjem je sledeča:

7.4 Paradigma try-and-catch

Če pri izvajanju kode pride do izjemne okoliščine, se bo sprožila izjema. Če je bila koda, ki je sprožila izjemo zaprta v poskusni blok, lahko to izjemo v prestreznem bloku ujamemo in ustrezno ukrepamo. Splošna oblika try-and-catch bloka je prikazana spodaj.

 

try { 
  //stavki, ki lahko sprozijo neko izjemo 
} catch(tipIzjeme izj) { 
  // stavki za odziv na izjemo tipa tipIzjeme 
} 
// stavki, ki se izvedejo tudi, ce izjeme ni bilo

Programsko kodo, v kateri lahko pride do izjemne okoliščine (v zgornjem primeru do deljenja z nic) zapremo v poskusni ali try blok. Če se je ob izvajanju kode, zaprte v poskusni blok, sprožila izjema, lahko to izjemo ujamemo v prestreznem ali catch bloku.

 

  try { 
    int a=3/0; 
  } catch (Exception e) { 
    System.out.println("OOPS-napaka!"); 
  }

7.5 Izjema je objekt

Ko se pojavi izjemna okoliščina, se zgradi nov objekt razreda java.lang.Throwable ali naslednik. Izjema, ki se sproži, je torej objekt.Ker vse izjeme izvirajo iz razreda java.lang.Throwable, imajo (med drugim) tudi naslednje metode:

Metoda Primer




public String toString()

java.lang.ArithmeticException: / by zero


public String getMessage()

/ by zero


public void printStackTrace()

/ by zero at izjeme.Deli.main(Deli.java:7)

 

Naloga 7-I. V programu v try bloku deli z nič in nato v catch bloku kliči metode getMessage(), toString() in printStackTrace().  

Rešitev naloge 7-I.: Metode objekta izjema izjeme/Deli.java

 

Ko poženemo zgornji program, se na zaslon izpiše:

e.getMessage(): / by zero  
e.toString()  : java.lang.ArithmeticException: / by zero  
Stack trace:  
java.lang.ArithmeticException: / by zero  
        at izjeme.Deli.main(Deli.java:7)

7.6 Hierarhija izjem

java.lang.Object  
 |  
 +--java.lang.Throwable  
       |  
       +--java.lang.Exception  
       |   |  
       |   +--java.lang.ClassNotFoundException  
       |   |  
       |   +--java.io.IOException  
       |   |   |  
       |   |   +--java.io.FileNotFoundException  
       |   |  
       |   +--java.lang.RuntimeException  
       |       |  
       |       +--java.lang.NullPointerException  
       |       |  
       |       +--java.lang.IndexOutOfBoundsException  
       |           |  
       |           +--java.lang.ArrayIndexOutOfBoundsException  
       |  
       +--java.lang.Error  
           |  
           +--java.lang.VirtualMachineError  
               |  
               +--java.lang.OutOfMemoryError


images/exceptions.eps

Slika 7.1: Hierarhija izjem


Izjeme so v osnovi dveh tipov: naslednice razreda java.lang.Error in java.lang.Exception. Prve so resne napake, zaradi katerih program praviloma konča (napaka višje sile, kot na primer napaka v strojni opremi, napaka JVM in podobno), druge so take, da lahko ukrepamo.

7.6.1 java.lang.Error

7.6.2 java.lang.Exception

7.7 Primer programa z izjemnimi okoliščinami

Recimo, da imamo tabelo števil (tabela a). Program uporabnika vpraša po dveh indeksih (i in j) ter izpiše kvocient a[i]∕a[j].

Računanje kvocienta dveh elementov tabele izjeme/Kvocient.java

 

Program, ki smo ga napisali, ni dobro zavarovan pred težavami. Pojavita se namreč lahko dve izjemni okoliščini: deljenje z nič in pa uporabnik vpiše indeks elementa v tabeli, ki ne obstaja (npr. 10).

Primer izvajanja programa Kvocient.class
[user@localhost]# java Kvocient 
 
Vnesi prvi  indeks: 0 
Vnesi drugi indeks: 1 
4 / 2 = 2 
 
Vnesi prvi  indeks: 3 
Vnesi drugi indeks: 2 
java.lang.ArithmeticException: / by zero 
    at TabelaDeli.main(TabelaDeli.java:20)

Program se je končal, ker smo poskušali deliti s številom nič. Zaradi napake uporabnik programa ne more več uporabljati (saj se je ta končal). Če pa bi napako ulovili, bi program delal naprej. To bi storili tako, da bi “problematični del” zaprli v try blok. Namesto

 

  k = a[i] / a[j]; 
  System.out.printf("%d/%d=%d", a[i],a[j],k);

bi napisali

 

  try { 
    k = a[i] / a[j]; 
    System.out.printf("%d/%d=%d\n", i,j,k); 
  } catch (Exception e) { 
    System.out.println("Prislojedonapake!"); 
  }

V tem primeru bi izvajanje potekalo takole:

Primer izvajanja programa Kvocient.class s popravkom
[user@localhost]# java Kvocient 
 
Vnesi prvi  indeks: 0 
Vnesi drugi indeks: 1 
4 / 2 = 2 
 
Vnesi prvi  indeks: 3 
Vnesi drugi indeks: 2 
Prislo je do napake! 
 
Vnesi prvi  indeks: 5 
Vnesi drugi indeks: 9 
Prislo je do napake!

Do napake je prišlo v dveh primerih - ko smo delili z nič (DivisionByZero) in ko smo vpisali indeks elementa, ki ga ni (ArrayIndexOutOfBounds). V obeh primerih je program izpisal isto sporočilo (to je slabo), v nobenem pa se ni končal (to je dobro). Slabost lahko odpravimo tako, da bolj natančno povemo, kakšno napako lovimo v catch.

 

  try { 
    k = a[i] / a[j]; 
    System.out.printf("%d/%d=%d\n", a[i],a[j],k); 
  } catch (ArithmeticException e) { 
    System.out.println("Napaka:deljenjeznic!"); 
  } catch (ArrayIndexOutOfBoundsException e) { 
    System.out.println("Napaka:napacenindeks"); 
  }

Popravljen Kvocient.class - ločimo dve različni izjemi
[user@localhost]# java Kvocient 
 
Vnesi prvi  indeks: 0 
Vnesi drugi indeks: 2 
Napaka: deljenje z nic! 
 
Vnesi prvi  indeks: 1 
Vnesi drugi indeks: 9 
Napaka: napacen indeks

7.8 Prednosti uporabe izjem

Oglejmo si primer kode za odpiranje in branje datoteke. Če ne uporabljamo mehanizma izjem, je treba vnaprej predvideti vse možne tezave z zaporedjem if-else stavkov.

 

if datoteka obstaja 
  odpri datoteko 
  if uspesno odprl 
    preberi njeno dolzino 
    if uspesno prebal dolzino 
      if datoteka zadosti kratka 
        if zadosti pomnilnika 
          zasezi pomnilnik 
          if uspelo zaseci pomnilnik 
            vcitaj datoteko v pomnilnik 
            if uspelo vcitati 
              uspeh!!! 
            else 
              napaka pri vcitavanju v pomnilnik 
          else 
            napaka pri zaseganju pomnilnika 
        else 
          napaka zaradi primanjkjaja pomnilnika 
      else 
        napaka zaradi predolge datoteke 
    else 
      napaka pri branju dolzine datoteke 
   else 
     napaka pri branju datoteke 
  else napaka pri odpiranju datoteke 
  zapri datoteko 
else 
  napaka zaradi neobstajanja datoteke

Podobno nalogo lahko z uporabo mehanizma izjem rešimo mnogo bolj elegantno.

 

try{ 
    odpri datoteko 
    preberi njeno dolzino 
    zasezi pomnilnik 
    vcitaj datoteko v pomnilnik 
    uspeh!!! 
    zapri datoteko 
    } 
 catch (izjema pri odpiranju datoteke) 
    { 
      obdelaj to izjemo 
    } 
 catch (izjema pri branju dozine datoteke) 
    { 
      obdelaj to izjemo 
    } 
 catch (izjema pri zaseganju pomnilnika) 
    { 
      obdelaj to izjemo 
    } 
 catch (izjema pri vcitavanju v pomnilnik) 
    { 
      obdelaj to izjemo 
    }

Poleg tega, da je zgornja koda bolj čitljiva, je tudi bolj varna. Ker so metode za odpiranje datoteke, branje dolžine datoteke in ostale zapisane v try bloku v Javi deklarirane kot metode, ki lahko vržejo izjemo, jih ne moremo uporabiti, ne da bi morebitne izjeme tudi ujeli. To nam prepreči prevajalnik, ki programa, ki ne lovi vseh izjem, ki jih mečejo uporabljene metode, sploh ne prevede. Na ta način se prepreči, da bi programer pozabil preveriti in napisati kodo za katero do možnih izjemnih okoliščin.

7.9 Kako napisati kodo, ki vrže izjemo?

Metoda, v kateri lahko pride do izjeme, mora izjemo obravnavati, ali pa jo mora posredovati klicoči metodi. Metoda, ki posreduje izjemo, mora imeti v deklaraciji to posredovanje navedeno, kot je razvidno iz spodnjega primera:

 

  public void branje() throws IOException { ...

Če metoda posreduje več izjem, so te v deklaraciji ločene z vejico.

Oglejmo si primer kode, v kateri najprej deklariramo razred izjeme NapacenArgument, nato še razred Cot, v katerem, če pride do napake, to izjemo tudi “vržemo”.

Primer metode, ki vrže izjemo izjeme/Cot.java

 

V zgornjem primeru metoda Cot (kotangens) preveri, ali je podani argument pravilen (različen od 0). Če je argument pravilen, vrne rezultat, sicer vrže izjemo tipa NapacenArgument. Zakaj lahko metodo kličemo na dva načina (v try bloku in brez njega), bomo videli kasneje (glej razdelek 7.10.1 o nepreverljivih izjemah).

7.10 Ali moramo izjemo vedno uloviti?

Natančneje: če ima neka metoda v deklariciji zapisano, da lahko vrže izjemo, ali moramo tako metodo obvezno klicati v try bloku?

Odgovor se glasi: načeloma da.

Povejmo še malo natančneje. Paradigma try-catch je bila razvita prav z namenom, da se prepreči malomarno programiranje, pri katerem bi programer lahko pozabil na katero od izjemnih okoliščin. Toda tudi pri izjemah obstaja nekaj izjem. Namreč: obstaja nekaj izjemnih okoliščin, ki se lahko pojavijo na skoraj vsakem mestu programa (primer: vedno lahko pride do napake v navideznem stroju, za katero ni kriv program pač pa neka zunanja okoliščina). Poleg tega obstajajo izjemne okoliščine, do katerih lahko pride pri zelo pogostih operacijah, vendar se jim s pazljivim programiranjem z lahkoto ognemo (primer: pri delu s tabelo moramo paziti, da uporabljamo samo tiste elemente, katerih indeks je v mejah med 0 in tabela.length-1; pri uporabi kateregakoli drugega indeksa pride do izjemne okoliščine - sproži se izjema ArrayIndexOutOfBounds). Če bi želeli vse dele programa, ki vsebujejo tako kodo, zapreti v try bloke, bi program postal zelo nepregleden. Zato Java pozna dva tipa izjem in sicer:

7.10.1 Nepreverljive izjeme

Nepreverljive izjeme so vse izjeme, ki so naslednice razredov java.lang.Error in java.lang.RuntimeException ter njunih podrazredov. Med slednje spadata tudi izjemi ArrayIndexOutOfBoundsException ter ArithmeticException.

Nepreverljivih izjem ni potrebno loviti - to nalogo lahko prepustimo navideznemu stroju. Seveda lovljenje ni prepovedano - s tem bomo povečali trdoživost programa. Če namreč lovljenje v celoti prepustimo navideznemu stroju, se bo program končal tudi v primerih, ko to ni nujno potrebno.

Primer: vsako deljenje dveh števil, pri katerem se lahko zgodi, da bo kvocient enak 0, je dobro zapreti v try blok (čeprav Java tega eksplicitno ne zahteva). S tem omogočimo, da se bo program, če je to le smiselno, nemoteno izvajal naprej. Če pa lovljenje morebitnih izjem pri deljenju z nič prepustimo navideznemu stroju, se bo program v primeru, da do takega deljenja res pride, zagotovo končal.

V programu Cot.java smo metodo Cot klicali na dva načina - v in izven try bloka. To je možno, saj je izjema NapacenArgument naslednik razreda ArithmeticException, torej je nepreverljiva. Vidimo, da v prvem primeru program teče naprej, v drugem primeru pa se izvajanje konča (do izpisa Konec ne pride).

7.10.2 Preverljive izjeme

Vse izjeme, ki niso nepreverljive, so preverljive. Preverljive izjeme moramo obvezno uloviti ali pa jih moramo posredovati klicoči metodi. Če želimo izjemo ujeti, moramo napisati odgovarjajoč try-catch blok, če pa želimo izjemo posredovati naprej, moramo v deklaraciji metode napisati pravilno throws deklaracijo.

Spodnja primera prikazujeta obe možnosti. V prvem metoda preberiZnak() sama poskrbi za morabitno izjemo IOException, ki se lahko pojavi pri branju iz standardnega vhoda. V drugem primeru pa metoda preberiZnak() skrb za morebitno izjemo prepusti klicoči metodi. V obeh primerih pa je nekdo moral poskrbeti za izjemo, saj je IOException preverljiva izjema.

Primer, v katerem metoda sama obravnava izjemo izjeme/Ulovi.java

 

Primer, v katerem metoda izjemo posreduje naprej izjeme/Prepusti.java

 

Za utrjevanje pridobljenega znanja rešimo še naslednjo nalogo.

Naloga 7-II. Napiši metodo, ki sproži izjemo. Metodo nato kliči iz dveh različnih metod: v prvi obravnavaj izjemo, v drugi pa izjemo vrži naprej.  

Rešitev naloge 7-II.: Obravnavanje izjem na dva načina izjeme/VrziAliNeVrzi.java

 

7.11 Blok finally

Recimo, da imamo

 

try { 
  // stavki poskusnega bloka 
} catch (tipIzjeme izj) { 
  // stavki prestreznika 
} 
// koncni stavki

Videti je, kot da se končni stavki izvršijo v vsakem primeru, saj:

Vendar to ni res. Končni stavki se ne izvršijo v naslednjih primerih:

Kaj naredimo s kodo, ki se mora izvršiti v vsakem primeru (na primer, koda, s katero sprostimo zasedene sistemske vire)? Odgovor: zapremo jo v poseben t.i. inally blok. Stavki znotraj finally bloka se bodo izvršili v vsakem primeru.

 

try { 
  // stavki poskusnega bloka 
} catch(tipIzjeme izj) { 
  // stavki prestreznika 
} finally { 
  // koncni stavki 
}

Oglejmo si uporabo bloka finally na primeru branja datoteke v tabelo.

 

1try { 
2  odpriDatoteko(); 
3  beriIzDatoteke(); 
4  prebranePodatkeShraniVPolje(); 
5  sestejKomponentePolja(); 
6  izpisiVsotoNaZaslon(); 
7} catch(IOException e) { 
8  izpisiSporociloONapaki(); 
9} finally { 
10  zapriDatoteko(); 
11}

Da se blok finally res izvrši vedno, lahko preverimo z naslednjim programom. Kaj izpiše če ga poženemo brez in kaj, če ga poženemo z enim argumentom?

Primer programa s finally blokom izjeme/Final.java

 

7.12 Sledenje

images/veriga.eps

Če metoda b() sproži izjemo, izvajalni sistem najprej išče ustrezni prestreznik v b(); če ga tam ni, išče prestreznik v a() in če ga tudi tam ni, v main(). Če prestreznika ni niti v main(), se na izjemo odzove navidezni stroj.

Veriga izjem izjeme/Veriga.java

 

Če je klicev veliko, je včasih težko določiti, kje izjema izvira. Iskanje izvora olajša metoda printStackTrace() iz razreda Throwable, ki izpiše tip in izvor izjeme, ter zaporedje klicev.

7.13 Nasledniki razreda Throwable

V primeru, ko potrebujemo svoj razred za sporočanje izjem, ustvarimo podrazred razreda Throwable. Prva odločitev, ki jo moramo pri tem sprejeti je, ali bo naša izjema predvidljiva ali ne – od tega je odvisno, kateri razred bomo izbrali kot neposredni predhodnik našega razreda. Če se odločimo, da bo našo izjemo treba vedno preverjati, bomo za predhodnika izbrali razred Exception,

 

class MojaPreverljivaIzjema extends Exception { ... }

sicer pa razred RuntimException ali katerega od njegovih naslednikov.

 

class MojaNepreverljivaIzjema extends Exception { ... }

V novem razredu moramo redefinirati metodo getMessage(), na primer takole

 

class DeljenjeZNic extends ArithmeticException { 
  public String getMessage() { 
     return "Deljenjeznicnidovoljeno"; 
  } 
}

7.14 Sprožanje izjem

Izjemo lahko v naši kodi sprožimo s pomočjo rezervirane besede throw, kot je prikazano v spodnjem primeru.

 

public static double deli(int x, int y)  { 
      if (y == 0) throw new DeljenjeZNic(); 
      return x / y; 
}

Ker razred DeljenjeZNic predstavlja nepreverljivo izjemo (glej prejšnji razdelek), nam v metodi deli() izjeme ni bilo treba ”najaviti”. V nasprotnem primeru, če metoda vrže preverljivo izjemo, pa moramo to najaviti v glavi metode, kot prikazuje spodnji primer v 9. vrstic.

Sprožanje preverljive izjeme izjeme/PIzjema.java