2.3. Izņēmumi

"The most likely way for the world to be destroyed, most experts agree, is by accident. That's where we come in. We're computer professionals. We cause accidents." Nathaniel Borenstein, inventor of MIME, in: Programming as if People Mattered: Friendly Programs, Software Engineering and Other Noble Delusions, Princeton University Press, Princeton, NJ, 1991.

Ievads

Mērķi

  • Izņēmuma jēdziens
  • try, catch, un finally komandas
  • Izņēmumu kategorijas
  • Svarīgākās klases izņēmumu klašu hierarhijā. Daži bieži sastopami izņēmumi
  • Saprast izņēmumu virzību pa metožu izsaukumu steku.
  • Programmēšana savu izņēmumu apstrādei.
  • Kā dažādās programmēšanas valodās apstrādā izpildes laika kļūdas?

Valodā C kļūdu apstrādi bieži veica, deklarējot metodes, kuras atgriež veselus skaitļus. Ja veselais skaitlis bija negatīvs, tad varēja secināt, ka notikusi tā vai cita kļūda (varēja vienoties par to, kuram skaitlim - "kļūdas kodam" atbilst kura situācija).

int f(int arg1, int arg2, int& return_value) {
    if (kautkasslikts) {
            // atgriež "kļūdas kodu"
                return -1;
        }
        else {
            // izrēķina "īsto" atgriežamo vērtību
                return_value = g(arg1, arg2); 
    }
        // atgriež "sekmīgas izpildes kodu"
        return 0; 
}

Metodes f() izsaucējs pārbaudot atgriešanās kodu varēja uzzināt, vai return_value ir sekmīgi izrēķināts, vai nē. Tā kā valodā Java nav argumentu nodošanas "pēc references", tad atgriežamās vērtības nevar rakstīt argumentu sarakstā un šāda pieeja nav lietojama, toties tajā ir īpaši mehānismi izņēmumu apstrādei.

Izņēmumi

java.lang.Exception apraksta programmas izpildes laikā radušās kļūdas, kuras nav kritiskas, t.i. tās pēc vēlēšanās var apstrādāt un programmu turpināt. Situāciju piemēri, kad rodas izņēmumi:

  • Mēģina atvērt neeksistējošu failu
  • Tīkla savienojums tiek pārtraukts
  • Izteiksmju operandi ir ārpus pieļaujamajām robežām
  • Programma mēģina dinamiski ielādēt .class failu, kurš neeksistē

Turpretī java.lang.Error klase definē nopietnas kļūdu situācijas, kuras programmētājam nav jācenšas apstrādāt pašam.

Izņēmuma piemērs

1   public class HelloWorld {
2       public static void main (String[] args) {
3           int i = 0;
4  
5           String lines[] = {"AAA", "BBB", "CCC"};
6  
7           while (i < 4) {
9               System.out.println(lines[i]);
10              i++;
11          }
12      }
13  }

Šī programma demonstrē izpildes laika kļūdu - mēģinājumu lasīt masīvā aiz pēdējā elementa. Šajā konkrētajā gadījumā kļūdu varētu novērst, ja 11. rindiņas vietā rakstītu pārbaudi, kura noskaidro īstās masīva beigas.

        while (i < lines.length) { ... }

try un catch komandas

1  try {
2      // koda fragments, kurš varētu izraisīt noteikta veida izņēmumus
3  } catch (MyExceptionType myExcept) {
4      // šo izpilda, ja rodas MyExceptionType izņēmums
5  } catch (Exception otherExcept) {
6      // šo izpilda, ja rodas cits Exception tips
7      otherExcept.printStackTrace(System.out); 
8  }

7. rindiņa šajā programmā drukā izsaukumu steku; līdzīgu izsaukumu steku drukā arī JVM, ja izņēmumu programmētājs neapstrādā un main() metode beidzas ar kļūdu.

Izsaukumu steka (Call Stack) mehānisms

  • Ja izņēmumu kādā metodē neapstrādā try-catch bloks, šis izņēmums nonāk pie metodes izsaucēja.
  • Ja izņēmums nonāk līdz pat main() metodei un tur netiek apstrādāts, programma beidzas anormāli (abnormal termination).

finally komanda

1  try {
2      iegutResursu();
3      izmantotResursu();
4  } catch (MyException e) {
5      registretProblemu(e);
6  } finally {
7      atbrivotResursu();
8  }

finally blokā vadība nonāk ikreiz, kad no try bloka izpildās kaut viena rindiņa. Arī return vai break - ja tie mēģina izlēkt no try bloka - vispirms nodod vadību finally. Šādās situācijās programmas lasīšana ir apgrūtināta.

Ja izejot no catch vai finally izpildes vēl ir neapstrādāti izņēmumi (t.i. šie bloki paši rada izņēmumus), tad šie izņēmumi nonāk ārpus try-catch-finally konstrukcijas (pirms tam noteikti izpildās finally kods).

Piemērs vēlreiz

1   public class HelloWorld {
2       public static void main (String[] args) {
3           int i = 0;
4  
5           String lines[] = {"AAA", "BBB", "CCC"};
6  
7           while (i < 4) {
8               try {
9                   System.out.println(lines[i]);
10                  i++;
11              } 
12              catch (ArrayIndexOutOfBoundsException e) {
13                  System.out.println("Reset index 'i'");
14                  i = 0;
15              } finally {
16                  System.out.println("This line is always printed");
17              }
18          }
19      }
20  }

Izņēmumu kategorijas

Kļūdu un izņēmumu hierarhija

Labs stils nopietnās programmās prasa apstrādāt visus izņēmumus, kaut vai main() metodē tos noķerot, izdrukājot atbilstošu paziņojumu un pamēģinot atbrīvot programmas piesaistītos resursus.

Kļūdas arī ir Throwable, t.i. tās var noķert un apstrādāt. Bet, piemēram, steka pārpildīšanās vai "out-of-memory" nozīmē, ka programmas izpildei ir beigušies resursi. Šādā situācijā nav jēgas censties situāciju vēl tālāk pasliktināt, veicot papildu apstrādi un šādi pieprasot papildu resursus.

Bieži sastopamu izņēmumu klases

  • java.lang.ArithmeticException
  • java.lang.NullPointerException
  • java.lang.ArrayIndexOutOfBoundsException
  • java.lang.SecurityException

Likums "Noķer vai deklarē!"

  • Izņēmumus var noķert un apstrādāt ar try-catch-finally bloku.
  • ... vai arī var deklarēt, ka metode izraisa izņēmumu (kuru savukārt var noķert izsaucošā metode) ar throws deklarāciju.
  • Metode var deklarēt, ka tā izraisa vairāk nekā vienu izņēmumu
1  public void readDatabaseFile(String file)
2          throws FileNotFoundException, UTFDataFormatException {
3      // open file stream; may cause FileNotFoundException
4      FileInputStream fis = new FileInputStream(file);
5      // read a string from fis may cause UTFDataFormatException...
6  }
  • Uz "izpildes laika izņēmumiem" un kļūdām, t.i. java.lang.RuntimeException un java.lang.Error instancēm "noķer vai deklarē" likums neattiecas.

Metožu pārdefinēšana un izņēmumi

Pārdefinējošā metode (apakšklasē) var izraisīt izņēmumus, kuri ir apakšklases tiem izņēmumiem, kurus izraisa pārdefinējamā metode (virsklasē). T.i. apakšklases metodē nevar izraisīt izņēmumus, kuri ir "sliktāki" nekā deklarēts virsklasē.

Metožu pārdefinēšanas piemēri

1  public class TestA {
2    public void methodA() throws RuntimeException {
3      // do some number crunching
4    }
5  }

1  public class TestB1 extends TestA {
2    public void methodA() throws ArithmeticException {
3      // do some number crunching
4    }
5  }

1  public class TestB2 extends TestA {
2    public void methodA() throws Exception {
3      // do some number crunching
4    }
5  }

TestB2 nekompilējas, jo Exception nav apakšklase klasei RuntimeException.

Metožu pārdefinēšanas piemēri

1  import java.io.*;
2  
3  public class TestMultiA {
4      public void methodA()
5              throws IOException, RuntimeException {
6          // do some IO stuff
7      }
8  }

1  import java.io.*;
2  
3  public class TestMultiB1 extends TestMultiA {
4    public void methodA()
5         throws FileNotFoundException, UTFDataFormatException,
6           ArithmeticException {
7      // do some IO and number crunching stuff
8    }
9  }

1  import java.io.*;
2  import java.sql.*;
3  
4  public class TestMultiB2 extends TestMultiA {
5    public void methodA()
6         throws FileNotFoundException, UTFDataFormatException,
7           ArithmeticException, SQLException {
8      // do some IO, number crunching, and SQL stuff
9    }
10  }

1  public class TestMultiB3 extends TestMultiA {
2    public void methodA() throws java.io.FileNotFoundException {
3      // do some file IO
4    }
5  }

TestMultiB1 kompilējas, jo abi pirmie izņēmumi ir apakšklases IOException.

TestMultiB2 nekompilējas - nevar pievienot jaunus - ar virsklases metodi nesavietojamus izņēmumus, piemēram, SQLException.

TestMultiB3 kompilējas, jo ir atļauts deklarēt mazāk izņēmumu nekā ir atbilstošajā virsklases metodē.

Savu izņēmumu deklarēšana

1   public class ServerTimedOutException extends Exception {
2       private int port;
3  
4       public ServerTimedOutException(String message, int port) {
5           super(message);
6           this.port = port;
7       }
8  
9  // Use getMessage method to get the reason the exception was made
10  
11      public int getPort() {
12          return port;
13      }
14  }

Lietotāja definētu izņēmumu apstrāde

1  public void connectMe(String serverName)
2      throws ServerTimedOutException {
3    int success;
4    int portToConnect = 80;
5  
6    success = open(serverName, portToConnect);
7  
8    if (success == -1) {
9      throw new ServerTimedOutException("Could not connect",
10                                        portToConnect);
11    }
12  }

1  public void findServer() {
2      try {
3        connectMe(defaultServer);
4      } catch (ServerTimedOutException e) {
5        System.out.println("Server timed out, trying alternative");
6        try {
7          connectMe(alternativeServer);
8        } catch (ServerTimedOutException e1) {
9          System.out.println("Error: " + e1.getMessage() +
10                             " connecting to port " + e1.getPort());
11        }
12      }
13    }

Piesardzīgā programmēšana (defensive programming)

  • Lieto izpildes laika pieņēmumus (runtime assertions), piemēram, procedūras sākumā, lai pārbaudītu argumentu atbilstību (precondition), procedūras beigās, lai pārbaudītu pēcnosacījumu (postcondition) un tad, kad programma gatavojas radīt būtiskus blakusefektus (dzēst datus, apstarot pacientus utml.)
  • Izpildes laika pieņēmums nozīmē izsaukt procedūru ar šādu prototipu:
    public static void assert (boolean b, String location)
    

    kurš "b==false" gadījumā veic nepieciešamos kļūdu reģistrācijas pasākumus ("error logging") un met izņēmumu, kurš aptur programmas tālāku izpildi. (JUnit piedāvā labāku pieņēmumu veidošanas karkasu jeb "framework")

  • Veiksmīgi izvēlēti izpildes laika pieņēmumi būtiski nepalēnina programmu un tos ir vēlams (vismaz ar kļūdu reģistrēšanas funkcionalitāti) atstāt piegādātajā programmas versijā.

Jautājumi paškontrolei

  • Kas ir izņēmumi?
  • Lietot try, catch, and finally komandas
  • Aprakstīt izņēmumu kategorijas
  • Identificēt bieži sastopamus izņēmumus
  • Rakstīt programmas, kurās ir lietotāja definēti izņēmumi un to apstrāde.

Diskusiju tēmas

  • Kādās situācijās jāveido jaunas izņēmumu klases?
  • Vai ir situācijas, kur konstruktors izraisa izņēmumu?