Î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:
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 == INSUFFICIENT_PERMISSIONS) { // handle error } else {// SUCCESS int readResult = read(); if (readResult == DISK_ERROR) { // handle error } else { // SUCCESS ... } }
Mecanismul bazat pe excepţii înlătură ambele neajunsuri menţionate mai sus. Codul ar arăta aşa:
try { open(); read(); ... } catch (FILE_NOT_FOUND) { // handle error } catch (INSUFFICIENT_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.
Când o eroare se produce într-o funcţie, aceasta creează un obiect excepţie şi îl 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.
Exemplu de aruncare a unei excepţii:
List<String> l = getArrayListObject(); if (null == l) 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:
new
)
În realitate, clasa Exception
este părintele majorităţii claselor excepţie din Java. Enumerăm câteva excepţii standard:
null
).Iterator
care nu mai conţine un element următor.
În momentul în care se instanţiază un obiect-excepţie, în acesta se reţine întregul lanţ de apeluri de funcţii 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 e
este obiectul excepţie.
Când o excepţie a fost aruncată, runtime system
încearcă să o trateze (prindă). Tratarea 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 (null == l) throw new Exception(); } public void catchFunction() { try { f(); } catch (Exception e) { System.out.println("Exception found!"); } }
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
:
try
, în interiorul căruia se apelează f
. De obicei, pentru a prinde o excepţie, trebuie să specificăm o zonă în care aşteptăm ca excepţia să se producă (guarded region). Această zonă este introdusă prin try
.catch
(Exception e
). La producerea excepţiei, blocul catch
corespunzător va fi executat. În cazul nostru se va afişa mesajul “S-a generat o excepţie”
. După aceea, programul va continua să ruleze normal în continuare.Observaţi un alt exemplu:
public void f() throws NullPointerException, EmptyListException { List<String> l = generateList(); if (l == null) throw new NullPointerException(); if (l.isEmpty()) throw new EmptyListException(); } public void catchFunction() { try { f(); } catch (NullPointerException e) { System.out.println("Null Pointer Exception found!"); } catch (EmptyListException e) { System.out.println("Empty List Exception found!"); } }
În acest exemplu funcţia f
a fost modificată astfel încât să existe posibilitatea de a arunca NullPointerException
sau EmptyListException
. Observaţi faptul că în catchFunction
avem două blocuri catch
. În funcție de excepția aruncată de f
, numai un singur bloc catch
se va executa.
Prin urmare:
catch
pentru a implementa o tratare preferenţială a excepţiilor, în funcţie de tipul acestoratry
, se va intra într-un singur bloc catch
(cel aferent excepției aruncate)throws
.
Dacă o excepţie nu este tratată nici în main
, aceasta va conduce la încheierea execuţiei programului!
Î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
.
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.
Nu toate excepţiile trebuie prinse cu try-catch
. Pentru a înțelege de ce, să analizăm clasificarea excepţiilor:
Clasa Throwable:
throw
.catch
.Checked exceptions, ce corespund clasei Exception:
FileNotFoundException
.Errors, ce corespund clasei Error:
OutOfMemoryError
.stack trace
).Runtime Exceptions, ce corespund clasei RuntimeException:
null
va produce NullPointerException
. Fireşte, putem prinde excepţia. Mai natural însă ar fi să eliminăm din program un astfel de bug care ar produce excepţia.try-catch
. Toate excepţiile sunt checked cu excepţia celor de tip Error, RuntimeException şi subclasele acestora, adica cele de tip unchecked.
Nu este indicată prinderea excepţiilor unchecked (de tip Error sau RuntimeException) cu try-catch
.
Putem arunca 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"); }
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 cu tipul excepţiei apărute. 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.
try { ... } catch(IOException | FileNotFoundException ex) { ... }
try-with-resources
, care ne permite să declarăm resursele într-un bloc de try
, cu asigurarea că resursele vor fi închise după executarea acelui bloc. Resursele declarate trebuie să implementeze interfața AutoCloseable.try (PrintWriter writer = new PrintWriter(file)) { writer.println("Hello World"); }
Metodele suprascrise (overriden) pot arunca numai excepţiile specificate de metoda din clasa de bază sau excepţii derivate din acestea.
În proiectarea orientată pe obiect, pattern-ul “Chain-of-responsibility” (lanț de responsabilitate) este un model de design constând dintr-o sursă de obiecte de comandă și o serie de obiecte de procesare. Fiecare obiect de procesare conține logică care definește tipurile de obiecte de comandă pe care le poate gestiona; restul sunt transferate către următorul obiect de procesare din lanț. De asemenea, există un mecanism pentru adăugarea de noi obiecte de procesare la sfârșitul acestui lanț. Astfel, lanțul de responsabilitate este o versiune orientată pe obiecte a if … else if … else if …… else … endif
, cu avantajul că blocurile condiție-acțiune pot fi dinamic rearanjate și reconfigurate la timpul de execuție.
Într-o variantă a modelului standard al lanțului de responsabilitate, un handler poate acționa ca un dispatcher, capabil să trimită comenzi în diverse direcții, formând un tree de responsabilități. În unele cazuri, acest lucru poate apărea recursiv, cu procesarea obiectelor care apelează obiecte de procesare de nivel superior cu comenzi care încearcă să rezolve o parte mai mică a problemei; în acest caz, recurența continuă până când comanda este procesată, sau întregul arbore a fost explorat. Un interpretor XML ar putea funcționa în acest mod.
Modelul lanțului de responsabilitate este aproape identic cu modelul decoratorului, diferența fiind că pentru decorator, toate clasele se ocupă de cerere, iar pentru lanțul de responsabilitate, exact una dintre clasele din lanț se ocupă de cerere.
try-with-resources
sau try-catch-finally
.Calculator
, ce conţine trei metode:add
: primeşte două numere şi întoarce un double
divide
: primeşte două numere şi întoarce un double
average
: primeşte o colecţie ce conţine obiecte double
, şi întoarce media acestora ca un numar de tip double
. Pentru calculul mediei, sunt folosite metodele add
şi divide
.Calculator
):NullParameterException
: este aruncată dacă vreunul din parametrii primiți este null
OverflowException
: este aruncată dacă suma a două numere e egală cu Double.POSITIVE_INFINITY
UnderflowException
: este aruncată dacă suma a două numere e egală cu Double.NEGATIVE_INFINITY
main
din clasa MainEx2
, evidențiind prin teste toate cazurile posibile care generează excepţii.Calculator
. Discutați cu asistentul.Logger
pe baza pattern-ului Chain-of-responsibility, definit mai sus, pe care îl vom folosi să păstram un jurnal de evenimente al unui program (vezi adaptarea în Referințe):LogLevel
, ce va acționa ca un bitwise flag, care va conține valorile - Info, Debug, Warning, Error, FunctionalMessage, FunctionalError
. Această enumerație va expune și o metodă statică all()
care va întoarce o colecție de EnumSet<LogLevel>
în care vor fi toate valorile de mai sus (Hint: EnumSet.allOf()
). Exemplu practic de folosire.LoggerBase
:EnumSet<LogLevel>
care va defini pentru ce nivele de log se va afisa mesajulLoggerBase
la care se trimite mesajulsetNext
ce va primi un LoggerBase
și va seta următorul delegat din lista de responsabilitatewriteMessage
ce va primi mesajul care trebuie afișatmessage
ce va primi mesajul care trebuie afișat și o severitate de tip LogLevel
. Dacă instanța de logger conține această severitate în colecția primite în constructor, atunci se va apela metoda writeMessage
. Apoi se vor pasa mesajul și severitatea către următorul delegat din lista de responsabilitate (dacă există unul)LoggerBase
și implementa metoda writeMessage
:LogLevel
(Hint: all()
) și va prefixa mesajele cu [Console]
FunctionalMessage
și FunctionalError
și va prefixa mesajele cu [Email]
Warning
și Error
și va prefixa mesajele cu [File]
EnumSet.of()
main
din clasa MainEx4
.