Rukovanje iznimkama u Javi

1. Pregled

U ovom ćemo uputstvu proći kroz osnove rukovanja iznimkama na Javi, kao i neke od njenih poteškoća.

2. Prvi principi

2.1. Što je?

Da bismo bolje razumjeli iznimke i postupanje s njima, napravimo usporedbu u stvarnom životu.

Zamislite da proizvod naručujemo putem interneta, ali dok smo na putu, dolazi do kvara u isporuci. Dobra tvrtka može se nositi s ovim problemom i graciozno preusmjeriti naš paket tako da i dalje stigne na vrijeme.

Isto tako, u Javi kod može imati pogrešaka tijekom izvršavanja naših uputa. Dobro rukovanje iznimkama može se nositi s pogreškama i graciozno preusmjeriti program kako bi korisniku pružio pozitivno iskustvo.

2.2. Zašto ga koristiti?

Kod obično pišemo u idealiziranom okruženju: datotečni sustav uvijek sadrži naše datoteke, mreža je zdrava i JVM uvijek ima dovoljno memorije. Ponekad to nazivamo "sretnim putem".

Međutim, u proizvodnji datotečni sustavi mogu oštetiti, mreže se pokvariti i JVM-ovima ponestane memorije. Dobrobit našeg koda ovisi o tome kako se nosi s "nesretnim putovima".

Moramo se nositi s tim uvjetima jer oni negativno utječu na tijek prijave i oblikuju se iznimke:

javni statički popis getPlayers () baca IOException {Put puta = Paths.get ("players.dat"); Popis igrača = Files.readAllLines (put); vratiti igrače.stream () .map (Player :: novo) .collect (Collectors.toList ()); }

Ovaj se kod odlučuje ne baviti IOException, umjesto toga prenoseći ga nizom poziva. U idealiziranom okruženju kod dobro funkcionira.

Ali što bi se moglo dogoditi u proizvodnji ako igrači.dat nedostaje?

Iznimka u niti "main" java.nio.file.NoSuchFileException: igrači.dat <- igrači.dat datoteka ne postoji na sun.nio.fs.WindowsException.translateToIOException (nepoznati izvor) na sun.nio.fs.WindowsException .rethrowAsIOException (nepoznati izvor) // ... više tragova steka na java.nio.file.Files.readAllLines (nepoznati izvor) na java.nio.file.Files.readAllLines (nepoznati izvor) na Exceptions.getPlayers (Exceptions.java : 12) <- Iznimka se pojavljuje u metodi getPlayers (), u retku 12 na Exceptions.main (Exceptions.java:19) <- getPlayers () poziva main (), u retku 19

Bez rješavanja ove iznimke, inače zdrav program može prestati uopće raditi! Moramo biti sigurni da naš kôd ima plan kada stvari pođu po zlu.

Ovdje također imajte na umu još jednu prednost za iznimke, a to je sam trag steka. Zbog ovog praćenja steka često možemo odrediti uvredljivi kôd bez potrebe za pričvršćivanjem programa za ispravljanje pogrešaka.

3. Hijerarhija iznimke

U konačnici, iznimke su samo Java objekti čiji se svi protežu od Bacljivo:

 ---> Pogreška izuzeća u bacanju | (označeno) (neoznačeno) | RuntimeException (nije označeno)

Tri su glavne kategorije iznimnih uvjeta:

  • Označene iznimke
  • Neprovjerene iznimke / iznimke tijekom izvođenja
  • Pogreške

Iznimke tijekom izvođenja i neprovjereni odnosi odnose se na isto. Često ih možemo koristiti naizmjenično.

3.1. Označene iznimke

Označene iznimke su iznimke s kojima Java kompajler zahtijeva rukovanje. Moramo ili deklarativno baciti iznimku prema hrpi poziva, ili je moramo sami riješiti. O jednom i drugom više za trenutak.

Oracleova dokumentacija govori nam da koristimo provjerene iznimke kada možemo opravdano očekivati ​​da se pozivatelj naše metode može oporaviti.

Nekoliko primjera provjerenih iznimaka je IOException i ServletException.

3.2. Neoznačene iznimke

Neoznačene iznimke su iznimke koje Java prevodilac čini ne zahtijevati od nas da se nosimo.

Jednostavno rečeno, ako stvorimo iznimku koja se proteže RuntimeException, bit će neoznačeno; u suprotnom, provjerit će se.

Iako ovo zvuči prikladno, Oracleova dokumentacija govori nam da postoje oba razloga za oba koncepta, poput razlikovanja između situacijske pogreške (označeno) i pogreške korištenja (neoznačeno).

Neki primjeri neprovjerenih iznimaka su NullPointerException, IllegalArgumentException, i SecurityException.

3.3. Pogreške

Pogreške predstavljaju ozbiljne i obično nepopravljive uvjete poput nekompatibilnosti knjižnice, beskonačne rekurzije ili curenja memorije.

I premda se ne protežu RuntimeException, oni su također neprovjereni.

U većini slučajeva bilo bi nam čudno postupati, instancirati ili produžiti Pogreške. Obično želimo da se oni šire do kraja.

Nekoliko primjera pogrešaka su a StackOverflowError i OutOfMemoryError.

4. Rukovanje iznimkama

U Java API-u postoji puno mjesta na kojima stvari mogu poći po zlu, a neka od tih mjesta označena su iznimkama, bilo u potpisu ili Javadocu:

/ ** * @exception FileNotFoundException ... * / javni skener (Niz datotekeName) baca FileNotFoundException {// ...}

Kao što je rečeno malo ranije, kada nazivamo ove "rizične" metode, mi mora rješavamo provjerene iznimke, a mi svibanj baratati onim neprovjerenim. Java nam daje nekoliko načina da to učinimo:

4.1. baca

Najjednostavniji način da se "riješi" izuzetak je ponovno vraćanje:

public int getPlayerScore (String playerFile) baca FileNotFoundException {Sadržaj skenera = novi skener (nova datoteka (playerFile)); vrati Integer.parseInt (contents.nextLine ()); }

Jer FileNotFoundException je provjerena iznimka, ovo je najjednostavniji način da se zadovolji kompajler, ali to znači da svatko tko pozove našu metodu sada mora to i riješiti!

raščlaniti može baciti a NumberFormatException, ali budući da nije označen, ne moramo ga rješavati.

4.2. pokušaj uhvatiti

Ako želimo sami pokušati riješiti iznimku, možemo koristiti a pokušaj uhvatiti blok. To možemo riješiti vraćanjem svoje iznimke:

public int getPlayerScore (String playerFile) {try {Sadržaj skenera = novi skener (nova datoteka (playerFile)); vrati Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException noFile) {throw new IllegalArgumentException ("Datoteka nije pronađena"); }}

Ili izvođenjem koraka oporavka:

public int getPlayerScore (String playerFile) {try {Sadržaj skenera = novi skener (nova datoteka (playerFile)); vrati Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException noFile) {logger.warn ("Datoteka nije pronađena, poništavanje rezultata."); return 0; }}

4.3. konačno

Sada postoje trenuci kada imamo kôd koji treba izvršiti bez obzira na to dogodi li se iznimka, i tu je konačno dolazi ključna riječ.

U našim dosadašnjim primjerima u sjenama vreba gadna greška, a to je da Java po defaultu neće vraćati ručke datoteka u operativni sustav.

Svakako, bez obzira čitamo li datoteku ili ne, želimo biti sigurni da smo izvršili odgovarajuće čišćenje!

Pokušajmo prvo na „lijeni“ način:

public int getPlayerScore (String playerFile) baca FileNotFoundException {Sadržaj skenera = null; isprobajte {contents = novi skener (nova datoteka (playerFile)); vrati Integer.parseInt (contents.nextLine ()); } napokon {if (content! = null) {contents.close (); }}} 

Evo, konačno blok označava koji kod želimo da Java radi bez obzira na to što se događa s pokušajem čitanja datoteke.

Čak i ako a FileNotFoundException ako se baci niz poziva, Java će pozvati sadržaj konačno prije nego što to učini.

I oboje se možemo nositi s iznimkom i pobrinite se da se naši resursi zatvore:

public int getPlayerScore (String playerFile) {Sadržaj skenera; isprobajte {contents = novi skener (nova datoteka (playerFile)); vrati Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException noFile) {logger.warn ("Datoteka nije pronađena, poništavanje rezultata."); return 0; } konačno {pokušaj {if (sadržaj! = null) {contents.close (); }} catch (IOException io) {logger.error ("Ne mogu zatvoriti čitač!", io); }}}

Jer Zatvoriti je također "rizična" metoda, također trebamo uhvatiti njezinu iznimku!

Ovo može izgledati prilično komplicirano, ali svaki komad trebamo da bismo riješili svaki potencijalni problem koji se može ispravno pojaviti.

4.4. probati-s-resursima

Srećom, od Jave 7 možemo pojednostavniti gornju sintaksu kada radimo sa stvarima koje se šire AutoCloseable:

javni int getPlayerScore (String playerFile) {try (sadržaj skenera = novi skener (nova datoteka (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException e) {logger.warn ("Datoteka nije pronađena, poništavanje rezultata."); return 0; }}

Kad stavimo reference koje jesu Automatsko zatvaranje u probati izjavu, tada ne trebamo sami zatvarati resurs.

Još uvijek možemo koristiti a konačno blokirati, međutim, da izvršimo bilo koju drugu vrstu čišćenja koju želimo.

Pogledajte naš članak posvećen probati-sa-resursima da biste saznali više.

4.5. Višestruko ulov Blokovi

Kôd ponekad može dovesti više od jedne iznimke, a mi možemo imati više od jedne ulov ručka bloka svaka pojedinačno:

javni int getPlayerScore (String playerFile) {try (sadržaj skenera = novi skener (nova datoteka (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (IOException e) {logger.warn ("Datoteka igrača se ne bi učitala!", e); return 0; } catch (NumberFormatException e) {logger.warn ("Datoteka igrača oštećena!", e); return 0; }}

Višestruki ulovi daju nam priliku da se sa svakom iznimkom nosimo drugačije, ako se za tim ukaže potreba.

Ovdje također imajte na umu da nismo uhvatili FileNotFoundException, i to zato što to proširuje IOException. Jer hvatamo IOException, Java će razmotriti bilo koji od svojih podrazreda koji se također obrađuju.

Recimo, ipak, da moramo liječiti FileNotFoundException drukčije od općenitijeg IOException:

javni int getPlayerScore (String playerFile) {try (sadržaj skenera = novi skener (nova datoteka (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException e) {logger.warn ("Datoteka igrača nije pronađena!", e); return 0; } catch (IOException e) {logger.warn ("Datoteka igrača se ne bi učitala!", e); return 0; } catch (NumberFormatException e) {logger.warn ("Datoteka igrača oštećena!", e); return 0; }}

Java nam omogućuje da obrađujemo iznimke potklasa odvojeno, ne zaboravite ih smjestiti više na popis ulova.

4.6. Unija ulov Blokovi

Kad znamo da će način na koji radimo s pogreškama biti jednak, Java 7 je uvela mogućnost hvatanja više iznimaka u istom bloku:

javni int getPlayerScore (String playerFile) {try (sadržaj skenera = novi skener (nova datoteka (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (IOException | NumberFormatException e) {logger.warn ("Učitavanje rezultata nije uspjelo!", e); return 0; }}

5. Bacanje iznimaka

Ako ne želimo sami rješavati iznimke ili želimo generirati svoje iznimke da ih drugi obrade, onda se moramo upoznati s bacanje ključna riječ.

Recimo da imamo sljedeću provjerenu iznimku koju smo sami stvorili:

javna klasa TimeoutException proširuje Exception {javni TimeoutException (string poruka) {super (poruka); }}

i imamo metodu koja bi potencijalno trebala dugo vremena da se završi:

javni popis loadAllPlayers (String playersFile) {// ... potencijalno duga operacija}

5.1. Bacanje provjerene iznimke

Poput vraćanja iz metode, možemo bacanje u bilo kojem trenutku.

Naravno, trebali bismo baciti kada pokušavamo naznačiti da je nešto pošlo po zlu:

javni popis loadAllPlayers (String playersFile) baca TimeoutException {while (! tooLong) {// ... potencijalno duga operacija} throw new TimeoutException ("Ova operacija je predugo trajala"); }

Jer TimeoutException je provjereno, također moramo koristiti baca ključnu riječ u potpisu, tako da će pozivatelji naše metode znati s njom postupati.

5.2. Bacanjeing. neprovjerene iznimke

Ako želimo učiniti nešto poput, recimo, provjere valjanosti unosa, umjesto toga možemo koristiti neprovjerenu iznimku:

javni popis loadAllPlayers (String playersFile) baca TimeoutException {if (! isFilenameValid (playersFile)) {throw new IllegalArgumentException ("Naziv datoteke nije valjan!"); } // ...} 

Jer IlegalArgumentException nije označeno, ne moramo označavati metodu, iako smo dobrodošli.

Neki metodu ionako označavaju kao oblik dokumentacije.

5.3. Omotavanje i ponovno bacanje

Možemo odabrati i vraćanje iznimke koju smo uhvatili:

javni popis loadAllPlayers (String playersFile) baca IOException {try {// ...} catch (IOException io) {throw io; }}

Ili napravite zamotavanje i ponovno bacanje:

javni popis loadAllPlayers (String playersFile) baca PlayerLoadException {try {// ...} catch (IOException io) {throw new PlayerLoadException (io); }}

Ovo može biti lijepo za objedinjavanje mnogih različitih iznimki u jednu.

5.4. Ponovno bacanje Bacljivo ili Iznimka

Sada za poseban slučaj.

Ako su jedine moguće iznimke koje bi zadani blok koda mogao izazvati neprovjereno iznimke, tada možemo uhvatiti i vratiti Bacljivo ili Iznimka bez dodavanja u naš potpis metode:

javni popis loadAllPlayers (String playersFile) {try {throw new NullPointerException (); } catch (Throwable t) {baciti t; }}

Iako je jednostavan, gornji kod ne može baciti provjerenu iznimku i zbog toga, iako vraćamo provjerenu iznimku, ne moramo označiti potpis s baca klauzula.

Ovo je zgodno s proxy klasama i metodama. Više o tome možete pronaći ovdje.

5.5. Nasljeđivanje

Kad metode označimo s baca , utječe na to kako potklase mogu nadjačati našu metodu.

U okolnostima u kojima naša metoda baca provjerenu iznimku:

izuzeci javne klase {javni popis loadAllPlayers (String playersFile) baca TimeoutException {// ...}}

Podrazred može imati „manje rizičan“ potpis:

javna klasa FewerExceptions proširuje iznimke {@Override public List loadAllPlayers (String playersFile) {// overridden}}

Ali ne iviše rizičniji ”potpis:

javna klasa MoreExceptions proširuje iznimke {@Override javni popis loadAllPlayers (String playersFile) baca MyCheckedException {// overridden}}

To je zato što se ugovori određuju u vrijeme sastavljanja prema referentnoj vrsti. Ako stvorim instancu VišeIznimki i spremite ga u Iznimke:

Iznimke iznimke = novi MoreExceptions (); exceptions.loadAllPlayers ("datoteka");

Tada će mi JVM samo reći ulov the TimeoutException, što je pogrešno otkad sam to rekao MoreExceptions # loadAllPlayers baca drugačiju iznimku.

Jednostavno rečeno, podrazredi mogu baciti manje provjerili iznimke od njihove superklase, ali ne više.

6. Anti-uzorci

6.1. Gutanje iznimki

Eto, postoji još jedan način na koji smo mogli zadovoljiti prevoditelja:

public int getPlayerScore (String playerFile) {try {// ...} catch (Exception e) {} // <== catch and swallow return 0; }

Navedeno se nazivaprogutavši iznimku. Većinu vremena to bi bilo malo podlo za nas jer to ne rješava problem i sprečava i drugi kod da može riješiti problem.

Postoje slučajevi kada postoji provjerena iznimka za koju smo sigurni da se nikad neće dogoditi. U tim bismo slučajevima još uvijek trebali dodati barem komentar koji navodi da smo namjerno pojeli iznimku:

public int getPlayerScore (String playerFile) {try {// ...} catch (IOException e) {// to se nikada neće dogoditi}}

Drugi način na koji možemo "progutati" iznimku je jednostavno ispis iznimke u tok pogreške:

javni int getPlayerScore (String playerFile) {try {// ...} catch (Iznimka e) {e.printStackTrace (); } return 0; }

Poboljšali smo svoju situaciju tako što smo barem negdje ispisali pogrešku za kasniju dijagnozu.

Bilo bi ipak bolje da upotrijebimo loger:

public int getPlayerScore (String playerFile) {try {// ...} catch (IOException e) {logger.error ("Ne mogu učitati rezultat", e); povratak 0; }}

Iako nam je vrlo povoljno na ovaj način postupati s iznimkama, moramo biti sigurni da ne progutamo važne informacije koje bi pozivači našeg koda mogli upotrijebiti za rješavanje problema.

Konačno, nenamjerno možemo progutati iznimku ne uključivši je kao uzrok kad bacamo novu iznimku:

javni int getPlayerScore (String playerFile) {try {// ...} catch (IOException e) {throw new PlayerScoreException (); }} 

Ovdje se tapšemo po leđima jer smo pozivatelja upozorili na pogrešku, ali ne uspijevamo uključiti IOException kao uzrok. Zbog toga smo izgubili važne informacije koje bi pozivači ili operateri mogli koristiti za dijagnosticiranje problema.

Bilo bi nam bolje da radimo:

javni int getPlayerScore (String playerFile) {try {// ...} catch (IOException e) {throw new PlayerScoreException (e); }}

Primijetite suptilnu razliku uključivanja IOException kao uzrok od PlayerScoreException.

6.2. Koristeći povratak u konačno Blok

Drugi način da se progutaju iznimke je da povratak od konačno blok. To je loše jer će naglim vraćanjem JVM izuzeti izuzetak, čak i ako ga je izbacio naš kôd:

javni int getPlayerScore (String playerFile) {int score = 0; probajte {baciti novi IOException (); } napokon {povratni rezultat; // <== IOException je ispušten}}

Prema specifikaciji jezika Java:

Ako se izvršavanje bloka try naglo dovrši iz bilo kojeg drugog razloga R, tada se konačno izvršava blok i tada postoji izbor.

Ako je konačno blok dovršava normalno, a zatim se naredba try naglo dovršava iz razloga R.

Ako je konačno blok naglo dovršava iz razloga S, zatim se naredba try naglo dovršava iz razloga S (a razlog R se odbacuje).

6.3. Koristeći bacanje u konačno Blok

Slično korištenju povratak u konačno blok, izuzetak ubačen u konačno blok će imati prednost nad iznimkom koja nastane u bloku catch.

Ovo će "izbrisati" izvornu iznimku iz datoteke probati blok, a mi gubimo sve te dragocjene informacije:

javni int getPlayerScore (String playerFile) {try {// ...} catch (IOException io) {throw new IllegalStateException (io); // <== pojeo konačno} napokon {baciti novi OtherException (); }}

6.4. Koristeći bacanje kao ići

Neki su ljudi također popustili iskušenju korištenja bacanje kao ići izjava:

javna void doSomething () {try {// hrpa koda baciti novi MyException (); // druga hrpa koda} catch (MyException e) {// treća gomila koda}}

To je čudno jer kôd pokušava koristiti iznimke za kontrolu protoka za razliku od rukovanja pogreškama.

7. Uobičajene iznimke i pogreške

Evo nekoliko uobičajenih iznimaka i pogrešaka na koje svi povremeno naletimo:

7.1. Označene iznimke

  • IOException - Ova je iznimka obično način da se kaže da nešto na mreži, datotečnom sustavu ili bazi podataka nije uspjelo.

7.2. RuntimeExceptions

  • ArrayIndexOutOfBoundsException - ova iznimka znači da smo pokušali pristupiti nepostojećem indeksu niza, kao kad pokušavamo dobiti indeks 5 iz niza duljine 3.
  • ClassCastException - ova iznimka znači da smo pokušali izvesti ilegalnu glumačku postavu, poput pokušaja pretvorbe a Niz u a Popis. Obično to možemo izbjeći izvođenjem obrane instanceof provjere prije lijevanja.
  • IlegalArgumentException - ova je iznimka generički način da kažemo da je jedan od navedenih parametara metode ili konstruktora nevaljan.
  • IllegalStateException - Ova je iznimka generički način da kažemo da je naše unutarnje stanje, poput stanja našeg objekta, nevaljano.
  • NullPointerException - Ova iznimka znači da smo pokušali uputiti na a null objekt. Obično to možemo izbjeći izvođenjem obrane null provjere ili pomoću Neobvezno.
  • NumberFormatException - Ova iznimka znači da smo pokušali pretvoriti a Niz u broj, ali niz je sadržavao nedopuštene znakove, poput pokušaja pretvaranja "5f3" u broj.

7.3. Pogreške

  • StackOverflowError - ova iznimka znači da je trag steka prevelik. To se ponekad može dogoditi u masivnim aplikacijama; međutim, to obično znači da se u našem kodu događa neka beskonačna rekurzija.
  • NoClassDefFoundError - ova iznimka znači da se klasa nije uspjela učitati ili zbog toga što nije bila na putu klase ili zbog neuspjeha u statičkoj inicijalizaciji.
  • OutOfMemoryError - ova iznimka znači da JVM nema više memorije na raspolaganju za više objekata. To je ponekad zbog curenja memorije.

8. Zaključak

U ovom smo članku prošli kroz osnove postupanja s iznimkama, kao i neke primjere dobre i loše prakse.

Kao i uvijek, sav kôd pronađen u ovom članku možete pronaći na GitHub-u!