20 najboljih praksi obrade izuzetaka u Javi — prestanite da upadate u zamke!
"Sanmej, danas ću ti preneti 20 najboljih praksi obrade izuzetaka, da kasnije u razvoju ne bi upadala u zamke," rekao sam sa osmehom Sanmej.
"Važi, Erge, slušam pažljivo," Sanmej se takođe blago nasmešila i rado prihvatila.
"Dobro, onda neću da trošim reči. Krećemo."
01. Ne hvatajte RuntimeException ako ne morate
Java razvojni priručnik koji izdaje Alibaba propisuje:
Pokušajte da ne hvatate RuntimeException, poput NullPointerException, IndexOutOfBoundsException itd.; to treba izbegavati proverom unapred (pre-check).
Pozitivan primer:
if (obj != null) {
//...
}Negativan primer:
try {
obj.method();
} catch (NullPointerException e) {
//...
}"O, a šta ako se neki izuzetak ne može otkriti proverom unapred?" upitala je Sanmej.
"Zaista postoje takve situacije, na primer NumberFormatException — iako spada u RuntimeException, ne može se proveriti unapred, pa ga ipak treba uhvatiti pomoću catch," rekao sam.
02. Radije koristite try-with-resource za zatvaranje resursa
Kada je potrebno zatvoriti resurs, nastojte da ne koristite try-catch-finally, a zabranjeno je direktno zatvarati resurs unutar try bloka.
Negativan primer:
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}"Zašto?" upitala je Sanmej.
"Razlog je vrlo jednostavan: ako do izuzetka dođe pre close(), resurs se neće zatvoriti. Najbolje je koristiti try-with-resource," rekao sam.
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}"Osim ako resurs ne implementira interfejs AutoCloseable," dodao sam.
"A šta onda u tom slučaju?" upitala je Sanmej.
"Zatvori tok u finally bloku," rekao sam.
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}03. Ne hvatajte Throwable
Throwable je roditeljska klasa za exception i error; ako u catch klauzuli uhvatite Throwable, verovatno ćete uhvatiti i greške koje prevazilaze mogućnosti obrade programa.
public void doNotCatchThrowable() {
try {
} catch (Throwable t) {
// ne raditi ovako
}
}"Zašto zapravo?" upitala je Sanmej.
"Zato što neke error-e program ne treba da obrađuje, a verovatno ih ni ne može, na primer OutOfMemoryError ili StackOverflowError — prvi je ne normalna greška koja nastaje kada Java virtuelna mašina ne može da dobije dovoljno memorijskog prostora, a druga kada nit zahteva dubinu steka veću od dozvoljene maksimalne dubine; ako ih uhvatite, prikrićete ozbiljnu grešku koju program treba da otkrije," rekao sam.
"Kao primer, konj može da vuče jedan vagon tereta, sa dva bi mogao da podlegne, ali jednim catch-om se problem više ne vidi," dodao sam.
04. Ne izostavljajte beleženje informacija o izuzetku
Često, zbog nepažnje, uhvatimo izuzetak a da ne zapišemo informacije o njemu, pa kada se problem zaista pojavi u produkciji, nemamo tragova koje bismo pregledali.
public void doNotIgnoreExceptions() {
try {
} catch (NumberFormatException e) {
// izuzetak nije zabeležen
}
}Trebalo bi zabeležiti informacije o grešci.
public void logAnException() {
try {
} catch (NumberFormatException e) {
log.error("O, greška se ipak dogodila: " + e);
}
}05. Ne beležite izuzetak i zatim ga ponovo bacajte
To je potpuno suvišno i lako dovodi do zabune u informacijama o grešci.
Negativan primer:
try {
} catch (NumberFormatException e) {
log.error(e);
throw e;
}Ako bacate, bacite; ne beležite — beležiti pa baciti je besmisleno.
Negativan primer:
public void wrapException(String input) throws MyBusinessException {
try {
} catch (NumberFormatException e) {
throw new MyBusinessException("Opis poruke o grešci:", e);
}
}Ista logika važi i ovde: pošto ste već uhvatili, ne bacajte u potpisi metoda.
06. Ne koristite return u finally bloku
Java razvojni priručnik koji izdaje Alibaba propisuje:
Nakon što se return iskaz u try bloku uspešno izvrši, on se ne vraća odmah, već nastavlja da izvršava iskaze u finally bloku; ako u finally bloku takođe postoji return iskaz, return iz try bloka će biti prekriven.
Negativan primer:
private int x = 0;
public int checkReturn() {
try {
return ++x;
} finally {
return ++x;
}
}"O, zaista, x u try bloku vraća vrednost 1, a u finally bloku vraća 2," rekla je Sanmej.
"Tako je," klimnuo sam.
07. Bacajte konkretno definisan checked exception, a ne Exception
public void foo() throws Exception { // pogrešan način
}Obavezno izbegavajte gore navedeni kod — on narušava svrhu checked izuzetka. Deklarisani metod treba što više da baca konkretne checked izuzetke.
Na primer, ako metod može da baci SQLException, treba eksplicitno deklarisati da baca SQLException, a ne tip Exception. Tako drugi programeri bolje razumeju nameru koda i način obrade izuzetka, i mogu na osnovu definicije i dokumentacije SQLException odrediti način i strategiju obrade.
08. Hvatajte konkretne podklase, a ne klasu Exception
try {
someMethod();
} catch (Exception e) { // pogrešan način
LOGGER.error("method has failed", e);
}Ako u catch bloku hvatate tip Exception, uhvatićete sve izuzetke, što može doneti programu nepotrebne probleme. Konkretno, hvatanje tipa Exception može dovesti do sledećih problema:
- Teško prepoznavanje i lociranje izuzetka: hvatanjem tipa Exception mogu se uhvatiti i izuzeci koji ne treba da se obrađuju, što otežava prepoznavanje i lociranje izuzetka.
- Otežano debugovanje i rešavanje grešaka: hvatanje tipa Exception otežava debugovanje i rešavanje grešaka, jer se ne može utvrditi konkretan tip izuzetka i uzrok njegovog nastanka.
Evo primera koji objašnjava zašto treba hvatati konkretne podklase umesto tipa Exception.
Pretpostavimo da imamo metod readFromFile(String filePath) koji čita podatke iz zadate datoteke. Tokom implementacije mogu se javiti dva izuzetka: FileNotFoundException i IOException.
Ako u metodu koristimo sledeći catch blok za hvatanje izuzetka:
try {
// kod za čitanje podataka
} catch (Exception e) {
// kod za obradu izuzetka
}Ovo hvata sve tipove izuzetaka, uključujući Checked Exception i Unchecked Exception. To može dovesti do sledećih problema:
- Kada se dogodi RuntimeException, biće uhvaćen, čime se može prikriti stvarna informacija o izuzetku.
- Pri debugovanju i rešavanju grešaka ne može se utvrditi konkretan tip i uzrok izuzetka, čime se povećava težina debugovanja.
- Tokom rada programa mogu se uhvatiti izuzeci koje ne treba obrađivati (poput NullPointerException, IllegalArgumentException itd.), čime se smanjuju performanse i stabilnost programa.
Stoga, radi boljeg lociranja i obrade izuzetka, treba hvatati konkretne podklase, na primer:
try {
// kod za čitanje podataka
} catch (FileNotFoundException e) {
// kod za obradu izuzetka "datoteka nije pronađena"
} catch (IOException e) {
// kod za obradu izuzetka ulazno-izlazne greške
}Ovo omogućava tačnije hvatanje izuzetka, čime se povećava robusnost i stabilnost programa.
09. Prilikom definisanja sopstvenog izuzetka, ne gubite stack trace
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " + e.getMessage()); // pogrešan način
}Ovo narušava stack trace originalnog izuzetka; ispravan postupak je:
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " , e); // ispravan način
}Na primer, evo klase sopstvenog izuzetka koja redefiniše metod printStackTrace() radi ispisa stack trace-a:
public class MyException extends Exception {
public MyException(String message, Throwable cause) {
super(message, cause);
}
@Override
public void printStackTrace() {
System.err.println("MyException:");
super.printStackTrace();
}
}Ovo čuva stack trace, a istovremeno pruža i sopstvenu poruku izuzetka. Pri bacanju MyException može se dobiti potpun stack trace, što olakšava lociranje i rešavanje izuzetka.
10. U finally bloku ne bacajte nijedan izuzetak
try {
someMethod(); //Throws exceptionOne
} finally {
cleanUp(); // ako finally još baci izuzetak, exceptionOne će zauvek biti izgubljen
}finally blok služi za definisanje koda koji se izvršava bez obzira na to da li je u try bloku došlo do izuzetka. finally blok se obično koristi za operacije koje moraju da se izvrše — otpuštanje resursa, zatvaranje datoteka i sl.
Bacanje izuzetka u finally bloku može dovesti do toga da originalni izuzetak bude prikriven. Na primer, u gornjem slučaju, čim cleanUp baci izuzetak, izuzetak iz someMethod biće prekriven.
11. Ne koristite printStackTrace() u produkciji
U Javi, metod printStackTrace() ispisuje stack trace izuzetka na standardni tok za greške. Ovaj metod je vrlo koristan za debugovanje i rešavanje grešaka. Međutim, u produkciji ne treba koristiti printStackTrace(), jer može dovesti do sledećih problema:
printStackTrace()ispisuje stack trace na standardni tok za greške, što može otkriti osetljive podatke, poput putanja datoteka, korisničkih imena, lozinki itd.printStackTrace()ispisuje stack trace na standardni tok za greške, što može uticati na performanse i stabilnost programa. U visoko konkurentnom produkcionom okruženju, velika količina stack trace-ova može dovesti do pada sistema ili neočekivanog ponašanja.- Pošto je produkcija često višenitni, distribuirani i složeni sistem, stack trace koji ispiše
printStackTrace()možda nije potpun niti tačan.
U produkciji treba koristiti sistem za logovanje radi beleženja izuzetaka, na primer log4j, slf4j, logback itd. Sistem logovanja može zapisivati izuzetke u datoteku ili bazu, bez otkrivanja osetljivih podataka i bez uticaja na performanse i stabilnost. Istovremeno, sistem logovanja pruža dodatne funkcije: kontrolu nivoa, rotaciju logova, email obaveštenja itd.
Na primer, možete koristiti logback za beleženje izuzetka, kao što je prikazano:
try {
// neki kod
} catch (Exception e) {
logger.error("An error occurred: ", e);
}12. Za izuzetke koje ne planirate da obrađujete, koristite try-finally bez catch
try {
method1(); // poziva Method 2
} finally {
cleanUp(); // očisti ovde
}Ako method1 pristupa Method 2, a Method 2 baci izuzetke koje ne želite da obradite u Method 1, ali ipak želite da izvršite određeno čišćenje pri pojavi izuzetka, možete direktno očistiti u finally bloku, bez catch bloka.
13. Zapamtite princip: baci rano (throw early), uhvati kasno (catch late)
"Baci rano, uhvati kasno" je princip obrade izuzetaka u Javi. Princip kaže da izuzetak treba baciti što ranije u kodu, kako bi se mogao pravovremeno obraditi kada nastane. Istovremeno, u catch bloku treba hvatati izuzetak što kasnije, kako bi se pri hvatanju dobilo više kontekstnih informacija i time bolje obradilo.
Evo primera za "baci rano": ako metod zahteva prosleđivanje parametara i taj parametar mora da ispunjava određeni uslov, kada parametar ne ispunjava uslov treba odmah baciti izuzetak, umesto da se u metodu radi nešto drugo. To osigurava da izuzetak bude pravovremeno obrađen kada nastane, izbegavajući ozbiljnije probleme.
Još jedan primer za "uhvati kasno": ako metod poziva druge metode koji mogu baciti izuzetak, a unutar metoda se odmah uhvati izuzetak, obrada može biti nedovoljna.
Pogledajmo ovaj kod:
public class ExceptionDemo1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
try {
int num = parseInt(str);
System.out.println("Rezultat konverzije: " + num);
} catch (NumberFormatException e) {
System.out.println("Konverzija neuspešna: " + e.getMessage());
}
}
public static int parseInt(String str) {
if (str == null || "".equals(str)) {
throw new NullPointerException("String je prazan");
}
if (!str.matches("\\d+")) {
throw new NumberFormatException("String nije broj");
}
return Integer.parseInt(str);
}
}U ovom primeru definisan je metod parseInt() koji konvertuje string u ceo broj. U tom metodu se prvo proverava da li je string prazan; ako jeste, odmah se baca NullPointerException. Zatim se proverava da li je string broj; ako nije, baca se NumberFormatException. Na kraju, metod Integer.parseInt() pretvara string u ceo broj i vraća ga.
U main() metodu primera poziva se parseInt() i try-catch blokom hvata NumberFormatException koji može biti bačen. Ako konverzija uspe, ispisuje se rezultat; inače se ispisuje poruka o neuspehu.
Ovaj primer koristi princip "baci rano, uhvati kasno": u parseInt() baca se izuzetak što ranije, a u main() hvata što kasnije, kako bi se pri hvatanju dobilo više kontekstnih informacija i bolje obradilo.
Pokrenite primer i unesite brojčani string — videćete ispis rezultata konverzije. Ako unesete nebrojčani string, ispisuje se poruka o neuspehu konverzije.
14. Bacajte samo izuzetke relevantne za metod
Relevantnost je vrlo važna za čistoću koda. Metod koji pokušava da pročita datoteku, ako baci NullPointerException, ne daje korisniku vrednu informaciju. Nasuprot tome, bolje je da takav izuzetak bude umotan u sopstveni izuzetak. NoSuchFileFoundException je korisnicima metoda mnogo korisniji.
public class Demo {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Rezultat je: " + result);
} catch (ArithmeticException e) {
System.err.println("Greška: " + e.getMessage());
}
}
public static int divide(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("Deljenje nulom");
}
return a / b;
}
}U ovom primeru baca se samo ArithmeticException, koji je relevantan za metod — to čini kod jasnijim i lakšim za održavanje.
15. Nikada ne koristite izuzetke za kontrolu toka u kodu
Korišćenje izuzetaka za kontrolu toka u kodu dovodi do problema sa čitljivošću, održivošću i performansama.
public class Demo {
public static void main(String[] args) {
String input = "1,2,3,a,5";
String[] values = input.split(",");
for (String value : values) {
try {
int num = Integer.parseInt(value);
System.out.println(num);
} catch (NumberFormatException e) {
System.err.println(value + " nije važeći broj");
}
}
}
}Iako ovaj primer ispravno obrađuje nebrojčane znakove u ulaznom stringu, on koristi izuzetke za kontrolu toka, što čini kod haotičnim i teškim za razumevanje. Za upravljanje tokom programa treba koristiti druge odgovarajuće kontrolne strukture (poput if, switch, petlji itd.).
16. Validirajte korisnički unos što ranije, kako biste uhvatili izuzetke rano u obradi zahteva
Na primer: u poslu registracije korisnika, ako se uradi ovako:
- Validiraj korisnika
- Ubaci korisnika
- Validiraj adresu
- Ubaci adresu
- Ako bude problema, poništi sve (rollback)
Ovo je pogrešan pristup — baza podataka će u različitim situacijama ostati u nekonzistentnom stanju. Treba prvo validirati sve, a zatim vršiti ažuriranje baze. Ispravan postupak je:
- Validiraj korisnika
- Validiraj adresu
- Ubaci korisnika
- Ubaci adresu
- Ako bude problema, poništi sve
Na primer, ako preko JDBC-a ubacujemo podatke u bazu, najbolje je prvo validirati pa ubacivati, umesto validateUserInput, insertUserData, validateAddressInput, insertAddressData.
Connection conn = null;
try {
// Poveži se sa bazom podataka
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password");
// Započni transakciju
conn.setAutoCommit(false);
// Validiraj korisnički unos
validateUserInput();
// Ubaci podatke o korisniku
insertUserData(conn);
// Validiraj unos adrese
validateAddressInput();
// Ubaci podatke o adresi
insertAddressData(conn);
// Izvrši commit transakcije ako je sve uspešno
conn.commit();
} catch (SQLException e) {
// Poništi transakciju (rollback) ako dođe do greške
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
System.err.println("Greška: " + ex.getMessage());
}
}
System.err.println("Greška: " + e.getMessage());
} finally {
// Zatvori konekciju sa bazom podataka
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
System.err.println("Greška: " + e.getMessage());
}
}
}17. Jedan izuzetak treba da bude zabeležen u jednom logu
Ne radite ovako:
log.debug("Korišćenje keš sektora A");
log.debug("Korišćenje retry sektora B");U jednonitnom okruženju to izgleda u redu, ali u višenitnom okruženju između ove dve susedne linije može se ispisati mnogo drugog sadržaja, što otežava pronalaženje problema. Treba uraditi ovako:
LOGGER.debug("Korišćenje keš sektora A, korišćenje retry sektora B");18. Prosledite izuzetku sve relevantne informacije
Korisna poruka izuzetka i stack trace su vrlo važni; ako vaš log ne može da locira mesto izuzetka, čemu onda log?
// Zabeleži poruku izuzetka i stack trace
LOGGER.debug("Greška pri čitanju datoteke", e);Trebalo bi koliko je moguće proslediti i poruku izuzetka String message, Throwable cause i stack trace.
19. Zaustavite prekinutu nit
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {} // ne radi ovako
doSomethingCool();
}InterruptedException signalizira da program treba da prestane sa onim što radi — na primer, istek vremena transakcije ili gašenje thread pool-a.
Trebalo bi dati sve od sebe da se završi ono što se radi i da se dovrši tekuća nit, umesto da se ignorisano InterruptedException. Izmenjeni program:
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
break;
}
}
doSomethingCool();20. Za ponavljajuće try-catch, koristite šablonski metod (Template Method)
Slični catch blokovi su beskorisni i samo povećavaju ponavljanje koda; za takav problem može se koristiti šablonski metod.
Na primer, obrada izuzetka pri pokušaju zatvaranja konekcije sa bazom.
class DBUtil{
public static void closeConnection(Connection conn){
try{
conn.close();
} catch(Exception ex){
// Zabeleži izuzetak - ne može se zatvoriti konekcija
}
}
}Ovakav metod koristiće se na mnogim mestima u aplikaciji. Ne rasipajte ovaj kod svuda, nego definišite gornji metod i koristite ga ovako:
public void dataAccessCode() {
Connection conn = null;
try{
conn = getConnection();
....
} finally{
DBUtil.closeConnection(conn);
}
}"Dobro, Sanmej, to bi bilo 20 praksi obrade izuzetaka za sada; u stvarnom razvoju naićićeš na još zamki, pa će ti utisak biti dublji ako ih sama iskusiš," rekao sam.
"A šta ako me posle, na poslu, grdi šef?" rekla je Sanmej uvređeno.
"Pa, početnik mora da napiše nekoliko bug-ova da bi opravdao taj naziv početnika," rekao sam s lakoćom.
"Dobro," uzdahnula je Sanmej bespomoćno.
