Table of Contents

Excepții

Introducere

Ce este o excepţie? În esenţǎ, o excepţie este un eveniment care se produce în timpul execuţiei unui program şi care perturbǎ fluxul normal al instrucţiunilor acestuia. De exemplu, în cadrul unui program care copiazǎ un fişier, astfel de evenimente excepţionale pot fi:

Utilitatea conceptului de excepţie

O abordare foarte des intâlnitǎ, ce precedǎ apariţia conceptului de excepţie, este întoarcerea unor valori speciale din funcţii, care sǎ desemneze situaţia apǎrutǎ. De exemplu, în C, funcţia fopen întoarce NULL dacǎ deschiderea fişierului a eşuat. Aceastǎ abordare are douǎ dezavantaje principale:

int openResult = open();
if (openResult == FILE_NOT_FOUND) {
    // handle error
} else if (openResult == INUFFICIENT_PERMISSIONS) {
    // handle error
} else { // SUCCESS
    int readResult = read();
    if (readResult == DISK_ERROR) {
        // handle error
    } else { // SUCCESS
        ...
    }
}

Mecanismul bazat pe excepţii înlaturǎ ambele neajunsuri menţionate mai sus. Codul ar arǎta aşa:

try {
    open();
    read();
    ...
} catch (FILE_NOT_FOUND) {
    // handle error
} catch (INUFFICIENT_PERMISSIONS) {
    // handle error
} catch (DISK_ERROR) {
    // handle error
}

Se observǎ includerea instrucţiunilor ce aparţin fluxului normal de execuţie într-un bloc try, şi precizarea condiţiilor excepţionale posibile la sfârşit, în câte un bloc catch. Logica este urmǎtoarea: se executǎ instrucţiune cu instrucţiune secvenţa din blocul try şi, la apariţia unei situaţii excepţionale, semnalate de o instrucţiune, se abandoneazǎ restul instrucţiunilor rǎmase neexecutate şi se sare direct la blocul catch corespunzǎtor.

Excepţii în Java

Când o eroare se produce într-o funcţie, aceasta creeazǎ un obiect excepţie şi il paseazǎ cǎtre runtime system. Un astfel de obiect conţine informaţii despre situaţia apǎrutǎ:

Pasarea menţionatǎ mai sus poartǎ numele de aruncarea (throwing) unei excepţii.

Aruncarea excepţiilor

Exemplu de aruncare a unei excepţii:

List<String> l = getArrayListObject();
if (l == null)
    throw new Exception("The list is empty");

În acest exemplu, încercǎm sǎ obţinem un obiect de tip ArrayList; dacǎ funcţia getArrayListObject întoarce null, aruncǎm o excepţie. Pe exemplul de mai sus putem face urmǎtoarele observaţii:

În realitate, clasa Exception este pǎrintele majoritǎţii claselor excepţie din Java. Enumerǎm câteva excepţii standard:

În momentul în care se instanţiazǎ un obiect excepţie, în acesta se reţine întregul lanţ de apeluri de funcţie, prin care s-a ajuns la instrucţiunea curentǎ. Aceastǎ succesiune se numeşte stack trace, şi se poate afişa prin apelul e.printStackTrace() unde este obiectul excepţie.

Prinderea excepţiilor

Când o excepţie a fost aruncatǎ, runtime system încearcǎ sǎ o trateze (prindǎ - catch). Tratarea (sau prinderea) unei excepţii este fǎcutǎ de o porţiune de cod specialǎ.

Sǎ observǎm urmǎtorul exemplu:

public void f() throws Exception {
    List<String> l = null;
 
    if (l == null)
        throw new Exception();
}
 
public void catchFunction() {
    try {
        f();
 
    } catch (Exception e) {
        System.out.println("S-a generat o exceptie");
    }
}

Se observǎ cǎ, dacǎ o funcţie aruncǎ o excepţie şi nu o prinde, trebuie, în general, sǎ adauge clauza throws în antet. Funcţia f va arunca întotdeauna o excepţie (din cauza cǎ l este mereu null). Observaţi cu atenţie funcţia catchFunction:

Observaţi urmǎtorul exemplu:

public void f() throws MyException, WeirdException {
    List<String> l = null;
 
    if (l == null)
        throw new MyException();
 
    throw new WeirdException("This exception never gets thrown");
}
 
public void catchFunction() {
    try {
        f();                    
 
    } catch (MyException e) {
        System.out.println("S-a generat o exceptie Null Pointer");
    } catch (WeirdException e) {
        System.out.println("S-a generat o exceptie ciudata");
    }
}

În acest exemplu, funcţia f a fost modificatǎ astfel încât sǎ arunce MyException. Observaţi faptul cǎ, în catchFunction avem doua blocuri catch. Cum excepţia aruncatǎ de f este de tip MyException, numai primul bloc catch se va executa. Prin urmare:

funcţie de tipul acestora.

Nivelul la care o excepţie este tratatǎ depinde de logica aplicaţiei. Acesta nu trebuie sǎ fie neaparat nivelul imediat urmǎtor, ce invocǎ secţiunea generatoare de excepţii. Desigur, propagarea de-a lungul mai multor nivele (metode) presupune utilizarea clauzei throws. Dacǎ o excepţie nu este tratatǎ nici în main, aceasta va conduce la încheierea execuţiei programului!

Blocuri try-catch imbricate

În general, vom dispune în acelaşi bloc try-catch instrucţiunile care pot fi privite ca înfǎptuind un acelaşi scop. Astfel, dacǎ o operaţie din secvenţa esueazǎ, se renunţǎ la instrucţiunile rǎmase şi se sare la un bloc catch. Putem specifica operaţii opţionale, al cǎror eşec sǎ nu influenţeze întreaga secvenţǎ. Pentru aceasta folosim blocuri try-catch imbricate:

try {
    op1();
 
    try {
        op2();
        op3();
 
    } catch (Exception e) { ... }
 
    op4();
    op5();
 
} catch (Exception e) { ... }

Dacǎ apelul op2 eşueazǎ, se renunţǎ la apelul op3, se executǎ blocul catch interior, dupǎ care se continuǎ cu apelul op4.

Blocul finally

Presupunem cǎ, în secvenţa de mai sus, care deschide şi citeşte un fişier, avem nevoie sǎ închidem fişierul deschis, atât în cazul normal, cât şi în eventualitatea apariţiei unei erori. În aceste condiţii, se poate ataşa un bloc finally dupǎ ultimul bloc catch, care se va executa în ambele cazuri menţionate. Secvenţa de cod urmǎtoare conţine o structurǎ try-catch-finally:

try {
    open();
    read();
    ...
} catch (FILE_NOT_FOUND) {
    // handle error
} catch (INUFFICIENT_PERMISSIONS) {
    // handle error
} catch (DISK_ERROR) {
    // handle error
} finally {
    // close file
}

Blocul finally se dovedeşte foarte util când în blocurile try-catch se gǎsesc instrucţiuni return. El se va executa şi în acest caz, exact înainte de execuţia instrucţiunii return, aceasta fiind executatǎ ulterior.

Tipuri de excepţii

Nu toate excepţiile trebuie prinse cu try-catch. Pentru a înțelege de ce, sǎ analizǎm clasificarea excepţiilor din figure 1:

Fig. 1: Tipuri de excepții

Excepţiile checked sunt cele prinse de blocurile try-catch. Toate excepţiile sunt checked cu excepţia celor de tip Error, RuntimeException şi subclasele acestora.

Excepţiile error nu trebuie (în mod obligatoriu) prinse folosind try-catch. Opţional, programatorul poate alege sǎ le prindǎ.

Excepţiile runtime nu trebuie (în mod obligatoriu) prinse folosind try-catch. Ele sunt de tip RuntimeException. Aţi întâlnit deja exemple de excepţii runtime, în urma diferitelor neatenţii de programare: NullPointerException, ArrayIndexOutOfBoundsException etc. Putem arunca o RuntimeException fǎrǎ sǎ o menţionǎm în clauza throws din antet:

public void f(Object o) {
    if (o == null)
        throw new NullPointerException("o is null");
}

Definirea de excepţii noi

Când aveţi o situaţie în care alegerea unei excepţii (de aruncat) nu este evidentǎ, puteţi opta pentru a scrie propria voastrǎ excepţie, care sǎ extindǎ Exception, RuntimeException sau Error. Exemplu:

class TemperatureException extends Exception {}
 
class TooColdException extends TemperatureException {}
 
class TooHotException extends TemperatureException {}

În aceste condiţii, trebuie acordatǎ atenţie ordinii în care se vor defini blocurile catch. Acestea trebuie precizate de la clasa excepţie cea mai particularǎ, pânǎ la cea mai generalǎ (în sensul moştenirii). De exemplu, pentru a întrebuinţa excepţiile de mai sus, blocul try-catch ar trebui sǎ arate ca mai jos:

try {
    ...
 
} catch (TooColdException e) {
    ...
} catch (TemperatureException e) {
    ...
} catch (Exception e) {
    ...
}

Afirmaţia de mai sus este motivatǎ de faptul cǎ întotdeauna se alege primul bloc catch care se potriveşte. Un bloc catch referitor la o clasǎ excepţie pǎrinte, ca TemperatureException prinde şi excepţii de tipul claselor copil, ca TooColdException. Poziţionarea unui bloc mai general înaintea unuia mai particular ar conduce la ignorarea blocului particular.

Excepţiile în contextul moştenirii

Metodele suprascrise (overriden) pot arunca numai excepţiile specificate de metoda din clasa de bazǎ sau excepţii derivate din acestea.

Exerciţii

1. (2p)Modificaţi rezolvarea exerciţiului 1 din laboratorul de Colecții, astfel încât fiecare semnalare de eroare sǎ fie înlocuitǎ cu aruncarea unei excepţii.

2. (2p) Scrieţi o metodǎ (scurtǎ) care sǎ genereze OutOfMemoryError şi o alta care sǎ genereze StackOverflowError. Verificaţi posibilitatea de a continua rularea dupǎ interceptarea acestei erori. Comparaţi rǎspunsul cu posibilitatea de a realiza acelaşi lucru într-un limbaj compilat, ce ruleazǎ direct pe platforma gazdǎ (ca C).

3. (4p) Definiţi o clasǎ care sǎ implementeze operaţii pe numere întregi. Operaţiile vor arunca excepţii.

4. (2p) Demonstraţi într-un program execuţia blocului finally chiar şi în cazul unui return din metoda.

Resurse