Čuvajte se opasnosti generičkih iznimaka

Tijekom rada na nedavnom projektu pronašao sam dio koda koji je izvršio čišćenje resursa. Budući da je imao mnogo različitih poziva, mogao bi stvoriti šest različitih iznimaka. Izvorni programer, pokušavajući pojednostaviti kôd (ili samo spremiti tipkanje), izjavio je da metoda baca Exceptionumjesto šest različitih iznimki koje se mogu izbaciti. To je prisililo pozivni kod da bude umotan u pokušaj / ulovni blok koji je uhvaćen Exception. Programer je zaključio da budući da je kôd namijenjen čišćenju, slučajevi kvarova nisu važni, pa je blok catch ostao prazan dok se sustav ionako isključio.

Očito je da ovo nisu najbolje prakse programiranja, ali čini se da ništa nije strašno pogrešno ... osim malog logičkog problema u trećem retku izvornog koda:

Popis 1. Izvorni kod za čišćenje

private void cleanupConnections () baca ExceptionOne, ExceptionTwo {for (int i = 0; i <connections.length; i ++) {connection [i] .release (); // Baca ExceptionOne, ExceptionTwo connection [i] = null; } veze = null; } zaštićena apstraktna void cleanupFiles () baca ExceptionThree, ExceptionFour; zaštićena apstraktna praznina removeListeners () baca ExceptionFive, ExceptionSix; public void cleanupEverything () baca Exception {cleanupConnections (); datoteke čišćenja (); removeListeners (); } javna void done () {try {doStuff (); cleanupEverything (); doMoreStuff (); } ulov (izuzetak e) {}}

U drugom dijelu koda, connectionsniz se ne pokreće dok se ne stvori prva veza. Ali ako se veza nikad ne stvori, tada je niz veza null. Dakle, u nekim slučajevima poziv na connections[i].release()rezultat rezultira a NullPointerException. To je relativno jednostavan problem za popraviti. Jednostavno dodajte ček za connections != null.

Međutim, iznimka se nikad ne prijavljuje. Baca se cleanupConnections(), opet baca cleanupEverything()i napokon hvata done(). done()Metoda ne učiniti ništa s izuzetkom, to ni ne prijaviti ga. A budući da cleanupEverything()je samo pozvan done(), iznimka se nikad ne vidi. Tako se kod nikad ne popravlja.

Dakle, u scenariju neuspjeha, metode cleanupFiles()and removeListeners()nikada se ne pozivaju (tako se njihovi resursi nikada ne objavljuju) i doMoreStuff()nikada se ne pozivaju, tako da se završna obrada done()nikad ne dovršava. Da stvar bude gora, done()ne poziva se kad se sustav isključi; umjesto toga pozvan je dovršiti svaku transakciju. Dakle, resursi cure u svakoj transakciji.

Ovaj je problem očito glavni: pogreške se ne prijavljuju i resursi cure. Ali sam kôd izgleda prilično nevin i, prema načinu pisanja koda, ovaj se problem pokazao teškim za uvid. Međutim, primjenom nekoliko jednostavnih smjernica, problem se može pronaći i riješiti:

  • Ne zanemarujte iznimke
  • Ne hvatajte generičke Exceptions
  • Ne bacajte generičke Exceptions

Ne zanemarujte iznimke

Najočitiji problem koda izlistavanja 1 jest taj što se pogreška u programu potpuno zanemaruje. Baca se neočekivana iznimka (iznimke su po svojoj prirodi neočekivane), a kôd nije spreman nositi se s tom iznimkom. Iznimka se čak ni ne prijavljuje jer kôd pretpostavlja da očekivane iznimke neće imati posljedica.

U većini slučajeva treba barem zabilježiti iznimku. Nekoliko paketa zapisivanja (pogledajte bočnu traku "Iznimke bilježenja") može evidentirati sistemske pogreške i iznimke bez značajnog utjecaja na performanse sustava. Većina sustava evidentiranja također omogućuje ispis tragova stogova, pružajući tako dragocjene informacije o tome gdje i zašto se dogodila iznimka. Konačno, budući da se dnevnici obično pišu u datoteke, zapis iznimaka može se pregledati i analizirati. Pogledajte Popis 11 na bočnoj traci za primjer bilježenja tragova stogova.

Evidentiranje iznimki nije kritično u nekoliko specifičnih situacija. Jedno od njih je čišćenje resursa u konačnoj klauzuli.

Iznimke u napokon

Na popisu 2 neki se podaci čitaju iz datoteke. Datoteka se mora zatvoriti bez obzira čita li podatak izuzetak, pa je close()metoda umotana u završnu klauzulu. Ali ako pogreška zatvori datoteku, po tom se pitanju ne može puno učiniti:

Popis 2

public void loadFile (String fileName) baca IOException {InputStream in = null; isprobajte {in = new FileInputStream (fileName); readSomeData (u); } napokon {if (in! = null) {try {in.close (); } catch (IOException ioe) {// ignorirano}}}}

Imajte na umu da i loadFile()dalje prijavljuje IOExceptionmetodu pozivanja ako stvarno učitavanje podataka ne uspije zbog problema s I / O (ulaz / izlaz). Također imajte na umu da, iako se izuzetak iz close()zanemaruje, kôd to izričito navodi u komentaru kako bi bio jasan svima koji rade na kodu. Ovaj isti postupak možete primijeniti na čišćenje svih I / O tokova, zatvaranje utičnica i JDBC veza itd.

Važna stvar kod ignoriranja iznimki je osiguravanje da samo jedna metoda bude omotana blokom za ignoriranje try / catch (tako da se i dalje pozivaju druge metode u priloženom bloku) i da se uhvati određena iznimka. Ova se posebna okolnost izrazito razlikuje od hvatanja generika Exception. U svim ostalim slučajevima, iznimka bi trebala biti (najmanje) zabilježena, po mogućnosti s tragom stoga.

Ne hvatajte generičke iznimke

Često u složenom softveru zadani blok koda izvršava metode koje donose razne iznimke. Dinamički utovar klase i instantiating objekt može baciti nekoliko različitih iznimaka, uključujući ClassNotFoundException, InstantiationException, IllegalAccessException, i ClassCastException.

Umjesto dodavanja četiri različita bloka catch u blok try, zauzeti programer može jednostavno umotati pozive metode u try / catch blok koji hvata generičke Exceptions (vidi Popis 3 dolje). Iako se ovo čini bezopasno, mogle bi se pojaviti neke neželjene nuspojave. Na primjer, ako className()je null, bacit Class.forName()će a NullPointerException, što će biti uhvaćeno u metodi.

U tom slučaju, catch block hvata iznimke koje nikada nije namjeravao uhvatiti, jer NullPointerExceptionje a podrazred RuntimeException, a koji je pak podrazred Exception. Tako generičke catch (Exception e)hvata sve potklase RuntimeException, uključujući NullPointerException, IndexOutOfBoundsExceptioni ArrayStoreException. Tipično programer ne namjerava uhvatiti te iznimke.

U popisu 3, null classNamerezultati u a NullPointerException, koji pozivajućoj metodi ukazuje na to da je ime klase nevaljano:

Popis 3

javno SomeInterface buildInstance (Niz klaseName) {SomeInterface impl = null; isprobajte {Class clazz = Class.forName (className); impl = (NekiInterface) clazz.newInstance (); } catch (Iznimka e) {log.error ("Pogreška pri stvaranju klase:" + ime klase); } return impl; }

Sljedeća posljedica generičke klauzule klauzule je da je bilježenje evidencije ograničeno jer catchne zna određenu iznimku koja se hvata. Neki programeri, suočeni s ovim problemom, pribjegavaju dodavanju oznake da bi vidjeli vrstu iznimke (vidi Popis 4), što je u suprotnosti sa svrhom korištenja blokova catch:

Popis 4

catch (Iznimka e) {if (e instanceof ClassNotFoundException) {log.error ("Nevaljano ime klase:" + ime klase + "," + e.toString ()); } else {log.error ("Nije moguće stvoriti klasu:" + className + "," + e.toString ()); }}

Popis 5 pruža cjelovit primjer hvatanja određenih izuzetaka za koje bi programer mogao biti zainteresiran. instanceofOperator nije potreban jer su uhvaćene određene iznimke. Svaki od provjerenih iznimaka ( ClassNotFoundException, InstantiationException, IllegalAccessException) je uhvaćen i bavila. Posebni slučaj koji bi proizveo ClassCastException(klasa se učitava ispravno, ali ne implementira SomeInterfacesučelje) također se provjerava provjerom za tu iznimku:

Popis 5

javno SomeInterface buildInstance (Niz klaseName) {SomeInterface impl = null; isprobajte {Class clazz = Class.forName (className); impl = (NekiInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Nevaljano ime klase:" + ime klase + "," + e.toString ()); } catch (InstantiationException e) {log.error ("Nije moguće stvoriti klasu:" + className + "," + e.toString ()); } catch (IllegalAccessException e) {log.error ("Nije moguće stvoriti klasu:" + className + "," + e.toString ()); } catch (ClassCastException e) {log.error ("Nevaljana vrsta klase," + ime klase + "ne implementira" + SomeInterface.class.getName ()); } return impl; }

U nekim je slučajevima poželjno vratiti poznatu iznimku (ili možda stvoriti novu iznimku) nego se pokušati nositi s njom u metodi. To omogućuje pozivajućoj metodi da obrađuje uvjet pogreške stavljanjem iznimke u poznati kontekst.

Popis 6 u nastavku daje alternativnu verziju buildInterface()metode koja baca a ClassNotFoundExceptionako se problem pojavi tijekom učitavanja i instanciranja klase. U ovom primjeru, metoda pozivanja mora primiti ili pravilno instancirani objekt ili iznimku. Dakle, metoda pozivanja ne treba provjeriti je li vraćeni objekt null.

Note that this example uses the Java 1.4 method of creating a new exception wrapped around another exception to preserve the original stack trace information. Otherwise, the stack trace would indicate the method buildInstance() as the method where the exception originated, instead of the underlying exception thrown by newInstance():

Listing 6

public SomeInterface buildInstance(String className) throws ClassNotFoundException { try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); throw e; } catch (InstantiationException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (IllegalAccessException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new ClassNotFoundException(className + " does not implement " + SomeInterface.class.getName(), e); } } 

In some cases, the code may be able to recover from certain error conditions. In these cases, catching specific exceptions is important so the code can figure out whether a condition is recoverable. Look at the class instantiation example in Listing 6 with this in mind.

In Listing 7, the code returns a default object for an invalid className, but throws an exception for illegal operations, like an invalid cast or a security violation.

Note:IllegalClassException is a domain exception class mentioned here for demonstration purposes.

Listing 7

public SomeInterface buildInstance(String className) throws IllegalClassException { SomeInterface impl = null; try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (InstantiationException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (IllegalAccessException e) { throw new IllegalClassException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new IllegalClassException(className + " does not implement " + SomeInterface.class.getName(), e); } if (impl == null) { impl = new DefaultImplemantation(); } return impl; } 

When generic Exceptions should be caught

Certain cases justify when it is handy, and required, to catch generic Exceptions. These cases are very specific, but important to large, failure-tolerant systems. In Listing 8, requests are read from a queue of requests and processed in order. But if any exceptions occur while the request is being processed (either a BadRequestException or any subclass of RuntimeException, including NullPointerException), then that exception will be caught outside the processing while loop. So any error causes the processing loop to stop, and any remaining requests will not be processed. That represents a poor way of handling an error during request processing:

Listing 8

public void processAllRequests () {Zahtjev req = null; pokušajte {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // baca BadRequestException} else {// Red zahtjeva je prazan, mora se napraviti break; }}} catch (BadRequestException e) {log.error ("Nevažeći zahtjev:" + req, e); }}