Sunteți pe pagina 1din 13

Într-un mediu ideal, nimic rău nu se întâmplă în timpul executării unei

aplicații. Fișierul de care aplicația are nevoie este întotdeauna


disponibil pe sistemul de fișiere, nu există probleme cu conexiunea la
rețea, iar mașina virtuală Java nu rămâne niciodată fără memorie
atunci când trebuie să instanțieze un obiect.

Spre deosebire de aceste condiții, realitatea este puțin diferită.


Aplicațiile încearcă adesea să deschidă fișiere inexistente, să obțină
anumite date prin intermediul unei rețele care nu funcționează sau să
solicite mai multă memorie decât poate să livreze JVM. Obiectivul
nostru este să scriem un cod ce va răspunde într-un mod
corespunzător la urgențele ce deviază de la execuția normală a
programului. Aceste situații sunt cunoscute drept excepții.

Înțelegerea conceptului de excepție

O excepție reprezintă o eroare ce apare în timpul execuției (runtime)


programului. Eroarea este o deviație de la comportamentul normal al
aplicației. O excepție este de fapt un eveniment care se petrece în
timpul execuției programului atunci când apare o eroare și care
întrerupe cursul normal al execuției programului.

Excepțiile pot fi cauzate de diverse motive. După cum am menționat


deja, uneori, o aplicație poate solicita un anumit fișier din sistemul de
fișiere care nu există. În acest caz, nu există o eroare în cod, cu
excepția faptului că programatorul a presupus că fișierul necesar va
exista întotdeauna, ceea ce reprezintă o practică proastă. În plus de
aceasta, motivul pentru apariția excepțiilor poate fi și un cod scris
prost. Să privim următorul exemplu ce ilustrează un cod care nu este
suficient de bine scris și care, prin urmare, conține o eroare ce
generează o excepție.

public class TestClass {

public static void main(String[] args) {

int[] nums = new int[4];


nums[7] = 10;
}
}

În codul de mai sus, am încercat să inițializăm o valoare a elementului


aflat în șir la poziția 7, însă șirul nums are doar patru elemente. Acesta
este motivul pentru care codul de mai sus va cauza o excepție.

Consecințele apariției excepțiilor

O excepție poate să apară în cadrul metodelor. Dacă vă gândiți puțin,


vă veți da seama că acest lucru este logic. Dacă ceva nu este bine
definit în timpul creării unei componente, programul nu va fi
interpretat. Când o eroare apare în timpul execuției unei metode, va
crea un obiect ce va fi înmânat sistemului de execuție. Acest obiect
este cunoscut ca obiect excepție (exception object) și conține
informații despre eroarea însăși, inclusiv tipul de eroare și starea
sistemului la momentul de apariție a erorii.

Procesul de creare a unei excepții și de livrare a acestui obiect către


sistemul de execuție se numește aruncarea excepției (throwing an
exception). După ce metoda în care a apărut o eroare aruncă excepția,
sistemul de execuție va încerca să găsească partea din cod ce va
procesa excepția rezultată. În structura ierarhică a metodelor, care se
numește call stack, este căutată metoda ce conține codul pentru
manipularea excepției rezultate, respectiv pentru captarea sa
(catching). În cazul în care codul pentru procesarea excepției rezultate
nu este găsit în nicio metodă, excepția sfârșește în mașina virtuală.
Problema este că JVM va opri execuția aplicației într-un astfel de caz și
va afișa mesajul excepției, respectiv erorii. Dacă rulăm codul din
exemplul de mai sus, putem vedea eroarea rezultată:
Imaginea 24.1 Consecința unei excepții netratate

Acest lucru nu este ceva ce ați dori să i se întâmple unui utilizator care
folosește aplicația dvs. Deși mesajele de eroare care există în cadrul
excepțiilor sunt utile în timpul fazei de dezvoltare a aplicației, astfel de
scenarii sunt inacceptabile în aplicația finală. Acesta este motivul
pentru care utilizăm tehnica denumită ”captarea excepției" (exception
catching).

Gestionarea excepțiilor

Captarea excepțiilor, respectiv tratarea lor se face utilizând blocurile


try-catch. Să vedem cum putem capta excepția din exemplul anterior.

public static void main(String[] args) {

int nums[] = new int[4];

try {
nums[7] = 10;

}
catch (Exception exc) {
System.out.println("Index out-of-bounds!");
}
}

În aceste mod, nu vor exista întreruperi în execuția aplicației, în schimb


vom obține un mesaj la ieșire:

Index out-of-bounds!

În exemplul nostru recunoaștem două cuvinte cheie - try și catch.


Aceste cuvinte cheie identifică blocurile ce au legătură cu tratarea
excepției. Primul bloc, definit ca bloc try, conține un cod în care
așteptăm o eroare, în timp ce al doilea bloc conține codul ce va trata
eroarea respectivă. Blocul catch acceptă întotdeauna un parametru de
tipul Exception. Clasa Exception este clasa de bază a excepțiilor, iar o
excepție a acestei clase într-un parametru va fi întotdeauna captată (la
fel ca în exemplu).

Pe lângă blocurile try și catch, mai există și un al treilea - finally. Acest


bloc se execută de fiecare dată, indiferent de rezultatul blocurilor
precedente. Sistemele pentru închiderea resurselor (pentru închiderea
fluxului, a conexiunilor cu baza de date etc.) se află întotdeauna în
acest bloc.

Să ne întoarcem la blocul catch. Acest bloc acceptă un parametru cu


un tip de excepție așteptată. În exemplul așteptat, aplicația va capta o
excepție de tipul Exception. Însă, aceasta nu este o practică bună în
tratarea excepțiilor. În loc să utilizăm tipul general (Exception), este
întotdeauna mai bine să se capteze mai întâi o expresie specializată,
iar apoi excepțiile generale. În practică, aceasta înseamnă că exemplul
modificat va arăta astfel:

public static void main(String[] args) {

int nums[] = new int[4];

try {
nums[7] = 10;

}
catch (ArrayIndexOutOfBoundsException exc) {
System.out.println("Index out-of-bounds!");
}
}

Acest exemplu va capta excepția specializată ”Index out-of-bounds”,


însă nu și alte excepții, prin urmare, dacă am avea o altă line ce aruncă
o altă excepție în blocul try, aceasta va rămâne necaptată, respectiv
vom avea o eroare în execuția programului:

public static void main(String[] args) {

int nums[] = new int[4];

try {
double variable = 5/0;
nums[7] = 10;

}
catch (ArrayIndexOutOfBoundsException exc) {
System.out.println("Index out-of-bounds!");
}
}

În exemplul de mai sus, am adăugat o altă linie în care am încercat să


facem împărțirea la zero, ceea ce este imposibil și va cauza o eroare.
Această eroare va cauza o excepție ce nu va fi captată, fiindcă a fost
specificat un tip de excepție specializată ca parametru în blocul catch.
Acesta este motivul pentru care vom obține următoarea ieșire:

Exception in thread "main" java.lang.ArithmeticException: /


by zero at mypackage.TestClass.main

Pentru a corecta acest exemplu, trebuie să captăm și cea de-a doua


excepție așteptată. De asemenea, este recomandat să adăugăm o
excepție generală de clasa Exception la finalul listei de excepții
așteptate. Acest lucru este mult mai ușor începând cu Java 7, fiindcă
putem capta excepții multiple în cadrul unui singur bloc catch.

public static void main(String[] args) {

int nums[] = new int[4];

try {
double variable = 5 / 0;
nums[7] = 10;

}
catch (ArrayIndexOutOfBoundsException | ArithmeticException e) {
System.out.println(e);

}
catch (Exception e) {
System.out.println(e);
}
}

În exemplul de mai sus putem vedea cum se pot capta mai multe
excepții în cadrul unui singur bloc catch, cu ajutorul caracterului "|“.
Regula este ca excepțiile să fie captate de la specifice la cele globale,
motiv pentru care avem un bloc ce captează o excepție generală la
finalul listei. Acesta va fi activat în cazul în care excepția nu aparține
niciunui grup de excepții specializate.

Cum se utilizează blocul finally?

Uneori vom dori să definim un bloc de cod ce va fi executat atunci


când blocurile try/catch își încheie logica. De exemplu, când apare o
excepție în metoda care gestionează conexiunea la rețea, conexiunea
va rămâne deschisă, ceea ce poate cauza diverse probleme. Astfel de
probleme pot fi ușor rezolvate cu ajutorul unui bloc finally, ce poate fi
utilizat pentru a rezolva situații similare în cadrul său.
Exemplul anterior va arăta astfel dacă îi adăugăm un bloc finally:

public static void main(String[] args) {

int nums[] = new int[4];

try {
double variable = 5 / 0;
nums[7] = 10;

}
catch (ArrayIndexOutOfBoundsException | ArithmeticException e) {
System.out.println(e);

}
catch (Exception e) {
System.out.println(e);
}
finally {
System.out.println("Leaving try.");
}
}

Blocul finally va fi executat în fiecare situație când execuția


programului abandonează blocul try/catch.

ARM (Automatic Resource Management)

Începând cu versiunea 7, Java are o caracteristică foarte utilă când


vine vorba de eliberarea resurselor și de utilizarea blocului finally.
Această caracteristică este în mod special utilă atunci când lucrăm cu
fluxurile și cu conexiunile, când trebuie să eliberăm resursele ocupate
după ce anumite operații s-au încheiat. Bineînțeles, putem face acest
lucru în cadrul blocului finally, care a fost utilizat în acest scop înainte
de apariția acestei caracteristici. Această caracteristică reprezintă
echivalentul directivei using a mediului de dezvoltare .NET. Haideți să
vedem cum putem utiliza această caracteristică în practică.

În versiunile Java anterioare versiunii 7, închiderea fluxului trebuia


făcută astfel.

public static void main(String[] args) {

String myText = "Hello World";


FileOutputStream fs = null;
try {
fs = new FileOutputStream("myFile.txt");
fs.write(myText.getBytes());
} catch (IOException exc) {
System.out.println(exc);
} finally {

try {
if (fs != null) {
fs.close();
}
} catch (IOException ex) {
System.out.println(ex);
}
}
}

Când utilizăm Automatic Resource Management, tot ce trebuie să


facem este următorul lucru:

public static void main(String[] args) {

String myText = "Hello World";

try (FileOutputStream fs = new FileOutputStream("myFile.txt");) {


fs.write(myText.getBytes());
} catch (IOException exc) {
System.out.println(exc);
}
}

Putem vedea că inițializarea fluxului a fost făcută în cadrul blocului try.


În acest mod, resursele definite între paranteze vor fi eliberate în mod
automat. De asemenea, se pot defini mai multe inițializări ale
resurselor. Această inovație este o opțiune foarte utilă și o soluție
elegantă pentru eliberarea resurselor. Când vom începe să lucrăm cu
fluxurile în modulul următor, această caracteristică se va dovedi mai
mult decât utilă.

Generarea și aruncarea excepțiilor

Până acum, am văzut cum putem capta excepții în limbajul de


programare Java. Însă, pentru a capta o excepție, trebuie mai întâi să
fie generată de o parte a codului. Orice cod poate genera o excepție:
codul de utilizator, codul dintr-un pachet Java scris de alți programatori
sau codul claselor încorporate. Indiferent cine efectuează generarea și
aruncarea excepțiilor, acest lucru se face cu cuvântul cheie throw.

Toate metodele trebuie să utilizeze declarația throw pentru a genera și


arunca o excepție. Declarația throw necesită un argument și acesta
trebuie să fie o instanță a clasei ce moștenește clasa Throwable.
Aceasta înseamnă că cel puțin una dintre clasele părinte din lanț
trebuie să fie un ”copil” al clasei Throwable. Iată un exemplu de
aruncare a unei excepții:

throw someThrowableObject;

Haideți să vedem acum cum se generează o excepție în cadrul unei


metode:

public class Main {


static void errorMethod() throws Exception
{
Exception exc = new Exception();
throw exc;
}

public static void main(String[] args) throws Exception {


errorMethod();
}
}

Acest exemplu va produce o eroare, ca rezultat al Exception. Obiectul


exc de tipul Exception a fost instanțiat în errorMethod și va fi aruncat
atunci când apelăm această metodă.

Constructorul Exception poate fi atât neparametrizat (la fel ca în


exemplul anterior), cât și parametrizat, atunci când textul unui mesaj
al excepției este expediat ca parametru. De asemenea, practica este
ca excepția să fie instanțiată și aruncată în mod anonim:

throw new Exception("Error message.");

Într-o arhitectură de proiect mai serioasă, vom crea clase de excepții


dedicate, ale căror caracteristici vor corespunde cu sistemul curent. În
acest caz, va fi moștenit copilul corespunzător al clasei Throwable.
Superclasa tuturor excepțiilor este clasa Throwable, iar clasa Exception
moștenește clasa Throwable. Clasele ce reprezintă excepțiile verificate
și neverificate (Exception și Error) moștenesc de asemenea clasa
Throwable, motiv pentru care este mai potrivit să se folosească clasa
Exception pentru excepția pe care o vom crea în codul de mai jos.

Să spunem că am dorit să creăm o excepție definită de utilizator


pentru un număr care este prea mare:

public class ToBigNumber extends Exception { }

public class Main {


static void checkNumber(int num) throws ToBigNumber
{
if(num>100)
throw new ToBigNumber();
}

public static void main(String[] args){


try
{
checkNumber(110);
}
catch(ToBigNumber ex)
{
System.out.println(ex);
}
}
}

În codul de mai sus, am creat clasa definită de utilizator ToBigNumber


ce va reprezenta excepția. Această clasă trebuie să moștenească clasa
Throwable sau o altă clasă ce moștenește această clasă, dacă dorim să
fie o excepție. Am spus deja că cea mai potrivită clasă pentru exemplul
de mai sus este clasa Exception.

Acesta este motivul pentru care am creat o metodă pentru verificarea


numărului expediat, checkNumber. Această metodă acceptă o valoare
întreagă ca parametru, iar dacă această valoare este mai mare decât
100, va fi generată o excepție a clasei ToBigNumber. Am apelat apoi
metoda checkNumber din metoda main, expediindu-i un număr mai
mare decât 100. Acesta va genera o excepție. Ieșirea va afișa numele
pachetului și clasa excepției:

myjavaprogram.ToBigNumber

Pentru a defini un mesaj rezonabil în timpul captării excepției, se poate


efectua reimplementarea metodei moștenite în clasa excepției. De
exemplu, metoda toString:
public class ToBigNumber extends Exception
{
@Override
public String toString() {
return "The number is to big!";
}
}

Putem de asemenea să reimplementăm metoda getMessage:

@Override
public String getMessage() {
return "The number is to big!";
}

Ați observat probabil în exemplele anterioare că este necesar să se


declare extensia throws în semnătura unei metode care poate să
genereze o excepție, urmată de denumirea clasei excepției. Acest
lucru evidențiază ce excepții poate respectiva metodă să arunce. Fără
această extensie, mediul (și interpreter-ul) va raporta o eroare. Motivul
pentru aceasta este faptul că moștenirea clasei Exception implică
implementarea excepției verificate. O excepție verificată necesită o
extensie în metoda unde excepțiile pot să apară și captarea excepțiilor
doar cu ajutorul blocurilor try/catch. Mai există de asemenea și
excepțiile neverificate. Excepțiile neverificate nu necesită în mod
obligatoriu blocurile try/catch și declarațiile throws.

Motivul pentru existența acestei diferențe între excepții este faptul că


unele excepții sunt așteptate (de ex. inexistența unui fișier), în timp ce
alte excepții nu pot fi anticipate (lipsa memoriei, supraîncărcarea
procesorului etc.).

Pentru a declara o excepție ca neverificată, trebuie să moștenim clasa


Error:
public class ToBigNumber extends Error { }

public class Main {


static void checkNumber(int num)
{
if(num>100)
throw new ToBigNumber();
}

public static void main(String[] args){


checkNumber(110);

}
}

S-ar putea să vă placă și