Undtagelseshåndtering i Java

1. Oversigt

I denne vejledning gennemgår vi det grundlæggende i undtagelseshåndtering i Java såvel som nogle af dets gotchas.

2. Første principper

2.1. Hvad er det?

For bedre at forstå undtagelser og håndtering af undtagelser, lad os foretage en sammenligning i det virkelige liv.

Forestil dig, at vi bestiller et produkt online, men undervejs er der en leveringsfejl. Et godt selskab kan håndtere dette problem og omdirigere vores pakke yndefuldt, så den stadig ankommer til tiden.

Ligeledes i Java kan koden opleve fejl under udførelsen af ​​vores instruktioner. godt undtagelse håndtering kan håndtere fejl og yndefuldt omdirigere programmet for at give brugeren stadig en positiv oplevelse.

2.2. Hvorfor bruge det?

Vi skriver normalt kode i et idealiseret miljø: filsystemet indeholder altid vores filer, netværket er sundt, og JVM har altid nok hukommelse. Nogle gange kalder vi dette den "lykkelige vej".

I produktionen kan filsystemer dog ødelægge, netværk går i stykker, og JVM'er løber tør for hukommelse. Vores kodes trivsel afhænger af, hvordan den behandler “ulykkelige stier”.

Vi skal håndtere disse betingelser, fordi de påvirker strømmen af ​​applikationen negativt og formet undtagelser:

offentlig statisk liste getPlayers () kaster IOException {Path path = Paths.get ("players.dat"); Liste spillere = Files.readAllLines (sti); returnere players.stream () .map (Player :: new) .collect (Collectors.toList ()); }

Denne kode vælger ikke at håndtere IOUndtagelse, videresender den i stedet til opkaldsstakken. I et idealiseret miljø fungerer koden fint.

Men hvad der kan ske i produktionen, hvis spillere.dat mangler?

Undtagelse i tråden "main" java.nio.file.NoSuchFileException: players.dat <- player.dat-filen findes ikke på sun.nio.fs.WindowsException.translateToIOException (Ukendt kilde) på sun.nio.fs.WindowsException .rethrowAsIOException (ukendt kilde) // ... mere stakspor på java.nio.file.Files.readAllLines (ukendt kilde) på java.nio.file.Files.readAllLines (ukendt kilde) ved Exceptions.getPlayers (Exceptions.java : 12) <- Undtagelse opstår i getPlayers () -metoden, på linje 12 ved Exceptions.main (Exceptions.java:19) <- getPlayers () kaldes af main (), on line 19

Uden at håndtere denne undtagelse kan et ellers sundt program muligvis stoppe med at køre helt! Vi skal sørge for, at vores kode har en plan for, hvornår ting går galt.

Bemærk også en yderligere fordel her til undtagelser, og det er selve stacksporingen. På grund af dette stackspor kan vi ofte lokalisere krenkende kode uden at skulle vedhæfte en fejlretning.

3. Undtagelseshierarki

Ultimativt, undtagelser er bare Java-objekter, hvor alle strækker sig fra Kan kastes:

 ---> Fejl, der kan kastes | (markeret) (ikke markeret) | RuntimeException (ikke markeret)

Der er tre hovedkategorier af ekstraordinære forhold:

  • Kontrollerede undtagelser
  • Ikke-markerede undtagelser / Runtime-undtagelser
  • Fejl

Runtime og ukontrollerede undtagelser henviser til den samme ting. Vi kan ofte bruge dem om hinanden.

3.1. Kontrollerede undtagelser

Kontrollerede undtagelser er undtagelser, som Java-kompilatoren kræver, at vi håndterer. Vi skal enten erklærende kaste undtagelsen op i opkaldsstakken, eller vi skal selv håndtere den. Mere om begge disse på et øjeblik.

Oracles dokumentation fortæller os, at vi bruger kontrollerede undtagelser, når vi med rimelighed kan forvente, at den, der ringer op til vores metode, skal kunne komme sig.

Et par eksempler på kontrollerede undtagelser er IOUndtagelse og ServletUndtagelse.

3.2. Ikke-markerede undtagelser

Ikke-markerede undtagelser er undtagelser, som Java-kompilatoren gør ikke kræver, at vi håndterer.

Kort sagt, hvis vi opretter en undtagelse, der strækker sig RuntimeException, det vil ikke være markeret; Ellers kontrolleres det.

Og selvom dette lyder praktisk, fortæller Oracles dokumentation os, at der er gode grunde til begge begreber, som f.eks. At skelne mellem en situationsfejl (markeret) og en brugsfejl (ikke markeret).

Nogle eksempler på ukontrollerede undtagelser er NullPointerException, UlovligtArgumentUndtagelse, og Sikkerhedsundtagelse.

3.3. Fejl

Fejl repræsenterer alvorlige og normalt uoprettelige forhold som et biblioteks inkompatibilitet, uendelig rekursion eller hukommelseslækage.

Og selvom de ikke strækker sig RuntimeException, de er ikke markeret.

I de fleste tilfælde ville det være underligt for os at håndtere, instantiere eller udvide Fejl. Normalt vil vi have, at disse forplantes helt op.

Et par eksempler på fejl er a StackOverflowError og OutOfMemoryError.

4. Håndtering af undtagelser

I Java API er der masser af steder, hvor ting kan gå galt, og nogle af disse steder er markeret med undtagelser, enten i signaturen eller Javadoc:

/ ** * @exception FileNotFoundException ... * / public Scanner (String fileName) kaster FileNotFoundException {// ...}

Som nævnt lidt tidligere, når vi kalder disse "risikable" metoder, vi skal håndtere de afkrydsede undtagelser, og vi kan håndter de ukontrollerede. Java giver os flere måder at gøre dette på:

4.1. kaster

Den enkleste måde at "håndtere" en undtagelse på er at omlægge den:

public int getPlayerScore (String playerFile) kaster FileNotFoundException {Scanner indhold = ny Scanner (ny fil (playerFile)); returnere Integer.parseInt (contents.nextLine ()); }

Fordi FileNotFoundException er en kontrolleret undtagelse, dette er den enkleste måde at tilfredsstille compileren på, men det betyder, at enhver, der kalder vores metode, nu også skal håndtere den!

parseInt kan kaste en NumberFormatException, men fordi det ikke er markeret, er vi ikke forpligtet til at håndtere det.

4.2. prøve-fangst

Hvis vi selv vil prøve at håndtere undtagelsen, kan vi bruge en prøve-fangst blok. Vi kan håndtere det ved at genkaste vores undtagelse:

public int getPlayerScore (String playerFile) {prøv {Scannerindhold = ny Scanner (ny fil (playerFile)); returnere Integer.parseInt (contents.nextLine ()); } fange (FileNotFoundException noFile) {smid nyt IllegalArgumentException ("Filen blev ikke fundet"); }}

Eller ved at udføre gendannelsestrin:

public int getPlayerScore (String playerFile) {prøv {Scannerindhold = ny Scanner (ny fil (playerFile)); returnere Integer.parseInt (contents.nextLine ()); } fange (FileNotFoundException noFile) {logger.warn ("Fil ikke fundet, nulstiller score."); returnere 0; }}

4.3. langt om længe

Nu er der tidspunkter, hvor vi har kode, der skal udføres, uanset om der forekommer en undtagelse, og det er her langt om længe nøgleord kommer ind.

I vores eksempler hidtil har der været en grim bug, der lurer i skyggen, hvilket er, at Java som standard ikke returnerer filhåndtag til operativsystemet.

Uanset om vi kan læse filen eller ej, vil vi sikre os, at vi foretager den passende oprydning!

Lad os prøve denne "dovne" måde først:

public int getPlayerScore (String playerFile) kaster FileNotFoundException {Scanner indhold = null; prøv {indhold = ny scanner (ny fil (playerFile)); returnere Integer.parseInt (contents.nextLine ()); } endelig {if (indhold! = null) {indhold.close (); }}} 

Her, den langt om længe blok angiver hvilken kode vi vil have Java til at køre uanset hvad der sker med at prøve at læse filen.

Selv hvis en FileNotFoundException kastes opkaldstakken, kalder Java indholdet af langt om længe inden du gør det.

Vi kan også begge håndtere undtagelsen og sørg for, at vores ressourcer lukkes:

public int getPlayerScore (String playerFile) {Scannerindhold; prøv {indhold = ny scanner (ny fil (playerFile)); returnere Integer.parseInt (contents.nextLine ()); } fange (FileNotFoundException noFile) {logger.warn ("Fil ikke fundet, nulstiller score."); returnere 0; } endelig {prøv {hvis (indhold! = null) {indhold.close (); }} fange (IOException io) {logger.error ("Kunne ikke lukke læseren!", io); }}}

Fordi tæt er også en "risikabel" metode, er vi også nødt til at fange dens undtagelse!

Dette kan se ret kompliceret ud, men vi har brug for hvert stykke til at håndtere hvert potentielt problem, der kan opstå korrekt.

4.4. prøve-med ressourcer

Heldigvis kan vi fra Java 7 forenkle ovenstående syntaks, når vi arbejder med ting, der strækker sig Kan lukkes automatisk:

public int getPlayerScore (String playerFile) {try (Scanner contents = new Scanner (new File (playerFile))) {return Integer.parseInt (contents.nextLine ()); } fange (FileNotFoundException e) {logger.warn ("Fil ikke fundet, nulstiller score."); returnere 0; }}

Når vi placerer referencer, der er Kan lukkes automatisk i prøve erklæring, så behøver vi ikke selv at lukke ressourcen.

Vi kan stadig bruge en langt om længe blokere dog for at foretage enhver anden form for oprydning, vi ønsker.

Tjek vores artikel dedikeret til prøve-med ressourcer til at lære mere.

4.5. Mange fangst Blokke

Nogle gange kan koden kaste mere end en undtagelse, og vi kan have mere end en fangst blokhåndtag hver for sig:

public int getPlayerScore (String playerFile) {try (Scanner contents = new Scanner (new File (playerFile))) {return Integer.parseInt (contents.nextLine ()); } fange (IOException e) {logger.warn ("Spillerfil ville ikke indlæses!", e); returnere 0; } fange (NumberFormatException e) {logger.warn ("Player-filen blev beskadiget!", e); returnere 0; }}

Flere fangster giver os chancen for at håndtere hver undtagelse forskelligt, hvis behovet skulle opstå.

Bemærk også her, at vi ikke fangede FileNotFoundException, og det er fordi det udvider IOException. Fordi vi fanger IOUndtagelse, Java vil overveje, at nogen af ​​dens underklasser også håndteres.

Lad os dog sige, at vi skal behandle FileNotFoundException anderledes end det mere generelle IOUndtagelse:

public int getPlayerScore (String playerFile) {try (Scanner contents = new Scanner (new File (playerFile))) {return Integer.parseInt (contents.nextLine ()); } fange (FileNotFoundException e) {logger.warn ("Spillerfil ikke fundet!", e); returnere 0; } fange (IOException e) {logger.warn ("Spillerfil ville ikke indlæses!", e); returnere 0; } fange (NumberFormatException e) {logger.warn ("Player-filen blev beskadiget!", e); returnere 0; }}

Java giver os mulighed for at håndtere undtagelser i underklassen separat, husk at placere dem højere på listen over fangster.

4.6. Union fangst Blokke

Når vi ved, at den måde, vi håndterer fejl på, vil være den samme, introducerede Java 7 dog muligheden for at fange flere undtagelser i samme blok:

public int getPlayerScore (String playerFile) {try (Scanner contents = new Scanner (new File (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (IOException | NumberFormatException e) {logger.warn ("Kunne ikke indlæse score!", e); returnere 0; }}

5. Kaste undtagelser

Hvis vi ikke selv ønsker at håndtere undtagelsen, eller vi vil generere vores undtagelser for andre at håndtere, er vi nødt til at blive fortrolig med kaste nøgleord.

Lad os sige, at vi har følgende kontrollerede undtagelse, som vi selv har oprettet:

offentlig klasse TimeoutException udvider undtagelse {public TimeoutException (streng besked) {super (besked); }}

og vi har en metode, der potentielt kan tage lang tid at gennemføre:

public List loadAllPlayers (String playersFile) {// ... potentielt lang drift}

5.1. Kaste en kontrolleret undtagelse

Som at vende tilbage fra en metode kan vi kaste når som helst.

Selvfølgelig skal vi kaste, når vi prøver at indikere, at noget er gået galt:

public List loadAllPlayers (String playersFile) kaster TimeoutException {while (! tooLong) {// ... potentielt lang operation} smid ny TimeoutException ("Denne operation tog for lang tid"); }

Fordi TimeoutException er markeret, skal vi også bruge kaster nøgleord i signaturen, så opkaldere efter vores metode ved, hvordan de håndterer det.

5.2. Kasteing en ukontrolleret undtagelse

Hvis vi vil gøre noget som f.eks. Validere input, kan vi i stedet bruge en ikke-markeret undtagelse:

public List loadAllPlayers (String playersFile) kaster TimeoutException {if (! isFilenameValid (playersFile)) {throw new IllegalArgumentException ("Filnavn er ikke gyldigt!"); } // ...} 

Fordi IllegalArgumentException er ikke markeret, behøver vi ikke markere metoden, selvom vi er velkomne til.

Nogle markerer metoden alligevel som en form for dokumentation.

5.3. Indpakning og omkastning

Vi kan også vælge at genkaste en undtagelse, vi har fanget:

public List loadAllPlayers (String playersFile) kaster IOException {prøv {// ...} fangst (IOException io) {throw io; }}

Eller tag en omvikling og omkast:

public List loadAllPlayers (String playersFile) kaster PlayerLoadException {try {// ...} catch (IOException io) {throw new PlayerLoadException (io); }}

Dette kan være rart at konsolidere mange forskellige undtagelser i en.

5.4. Omkastning Kan kastes eller Undtagelse

Nu til en særlig sag.

Hvis de eneste mulige undtagelser, som en given kodeblok kan rejse, er ikke markeret undtagelser, så kan vi fange og omlægge Kan kastes eller Undtagelse uden at tilføje dem til vores metodesignatur:

public List loadAllPlayers (String playersFile) {prøv {kast ny NullPointerException (); } fangst (kastbar t) {kast t; }}

Selvom det er simpelt, kan ovenstående kode ikke kaste en afkrydset undtagelse, og selvom vi omlægger en afkrydset undtagelse, behøver vi ikke markere signaturen med en kaster klausul.

Dette er praktisk med proxy-klasser og metoder. Mere om dette kan findes her.

5.5. Arv

Når vi markerer metoder med en kaster nøgleord, det påvirker, hvordan underklasser kan tilsidesætte vores metode.

Under de omstændigheder, hvor vores metode kaster en kontrolleret undtagelse:

offentlig klasse Undtagelser {public List loadAllPlayers (String playersFile) kaster TimeoutException {// ...}}

En underklasse kan have en "mindre risikabel" signatur:

offentlig klasse FewerExceptions udvider undtagelser {@Override public List loadAllPlayers (String playersFile) {// tilsidesat}}

Men ikke en “mere mere risikofyldt ”underskrift:

offentlig klasse MoreExceptions udvider Undtagelser {@Override public List loadAllPlayers (String playersFile) kaster MyCheckedException {// tilsidesat}}

Dette skyldes, at kontrakter bestemmes på kompileringstidspunktet af referencetypen. Hvis jeg opretter en forekomst af MoreExceptions og gem det til Undtagelser:

Undtagelser undtagelser = nye MoreExceptions (); exceptions.loadAllPlayers ("fil");

Så vil JVM kun fortælle mig det fangst det TimeoutException, hvilket er forkert, da jeg har sagt det MoreExceptions # loadAllPlayers kaster en anden undtagelse.

Kort sagt, underklasser kan kaste færre kontrollerede undtagelser end deres superklasse, men ikke mere.

6. Antimønstre

6.1. Indtagelse af undtagelser

Nu er der en anden måde, som vi kunne have tilfredsstillet kompilatoren:

public int getPlayerScore (String playerFile) {prøv {// ...} fangst (undtagelse e) {} // <== fangst og sluge retur 0; }

Ovenstående kaldessluge en undtagelse. Det meste af tiden ville det være et lille middel for os at gøre dette, fordi det ikke løser problemet og det holder også anden kode i stand til at løse problemet.

Der er tidspunkter, hvor der er en kontrolleret undtagelse, som vi er overbeviste om, at det bare aldrig vil ske. I disse tilfælde skal vi stadig i det mindste tilføje en kommentar om, at vi med vilje spiste undtagelsen:

public int getPlayerScore (String playerFile) {prøv {// ...} fangst (IOException e) {// dette vil aldrig ske}}

En anden måde, hvorpå vi kan "sluge" en undtagelse, er at udskrive undtagelsen til fejlstrømmen:

public int getPlayerScore (String playerFile) {prøv {// ...} fangst (Undtagelse e) {e.printStackTrace (); } returner 0; }

Vi har forbedret vores situation lidt ved i det mindste at skrive fejlen et eller andet sted til senere diagnose.

Det ville dog være bedre for os at bruge en logger:

public int getPlayerScore (String playerFile) {try {// ...} catch (IOException e) {logger.error ("Kunne ikke indlæse scoren", e); returnere 0; }}

Selvom det er meget praktisk for os at håndtere undtagelser på denne måde, er vi nødt til at sikre os, at vi ikke sluger vigtige oplysninger, som opkaldere af vores kode kan bruge til at afhjælpe problemet.

Endelig kan vi utilsigtet sluge en undtagelse ved ikke at medtage den som en årsag, når vi kaster en ny undtagelse:

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

Her klapper vi os selv på ryggen for at advare vores opkalder om en fejl, men vi undlader at inkludere IOUndtagelse som årsag. På grund af dette har vi mistet vigtige oplysninger, som opkaldere eller operatører kunne bruge til at diagnosticere problemet.

Det ville være bedre for os at gøre:

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

Bemærk den subtile forskel på inkludering IOUndtagelse som den årsag af PlayerScoreException.

6.2. Ved brug af Vend tilbage i en langt om længe Blok

En anden måde at sluge undtagelser er at Vend tilbage fra langt om længe blok. Dette er dårligt, fordi JVM ved at vende brat vender tilbage undtagelsen, selvom den blev kastet fra vores kode:

public int getPlayerScore (String playerFile) {int score = 0; prøv {kast ny IOException (); } endelig {return score; // <== IOException er droppet}}

I henhold til Java Language Specification:

Hvis udførelsen af ​​prøveblokken fuldføres brat af en anden årsag R, så udføres den endelige blok, og så er der et valg.

Hvis den langt om længe blok udfyldes normalt, derefter udfyldes prøveerklæringen brat af grunden R.

Hvis den langt om længe blok udfyldes pludseligt af grund S, derefter udfyldes forsøgsudtalelsen pludseligt af grund S (og grund R kasseres).

6.3. Ved brug af kaste i en langt om længe Blok

Svarende til brug Vend tilbage i en langt om længe blok, undtagelsen kastet i en langt om længe blok har forrang over den undtagelse, der opstår i fangstblokken.

Dette vil "slette" den oprindelige undtagelse fra prøve blokere, og vi mister al den værdifulde information:

public int getPlayerScore (String playerFile) {try {// ...} catch (IOException io) {throw new IllegalStateException (io); // <== spist af den endelig} endelig {smid ny OtherException (); }}

6.4. Ved brug af kaste som en gå til

Nogle mennesker gav også fristelsen til at bruge kaste som en gå til udmelding:

offentlig ugyldighed doSomething () {prøv {// flok kode kaste ny MyException (); // anden flok kode} fangst (MyException e) {// tredje flok kode}}

Dette er underligt, fordi koden forsøger at bruge undtagelser til flowkontrol i modsætning til fejlhåndtering.

7. Almindelige undtagelser og fejl

Her er nogle almindelige undtagelser og fejl, som vi alle støder på fra tid til anden:

7.1. Kontrollerede undtagelser

  • IOUndtagelse - Denne undtagelse er typisk en måde at sige, at noget på netværket, filsystemet eller databasen mislykkedes.

7.2. RuntimeExceptions

  • ArrayIndexOutOfBoundsException - denne undtagelse betyder, at vi forsøgte at få adgang til et ikke-eksisterende matrixindeks, som når vi forsøgte at få indeks 5 fra en matrix med længde 3.
  • ClassCastException - denne undtagelse betyder, at vi forsøgte at udføre en ulovlig rollebesætning, som at prøve at konvertere en Snor ind i en Liste. Vi kan normalt undgå det ved at udføre defensivt forekomst af kontroller inden støbning.
  • IllegalArgumentException - denne undtagelse er en generisk måde for os at sige, at en af ​​de angivne metode- eller konstruktorparametre er ugyldig.
  • IllegalStateException - Denne undtagelse er en generisk måde for os at sige, at vores interne tilstand, ligesom tilstanden for vores objekt, er ugyldig.
  • NullPointerException - Denne undtagelse betyder, at vi forsøgte at henvise til a nul objekt. Vi kan normalt undgå det ved enten at udføre defensiv nul kontrol eller ved hjælp af Valgfri.
  • NumberFormatException - Denne undtagelse betyder, at vi forsøgte at konvertere en Snor til et nummer, men strengen indeholdt ulovlige tegn, som at prøve at konvertere “5f3” til et tal.

7.3. Fejl

  • StackOverflowError - denne undtagelse betyder, at stakksporingen er for stor. Dette kan undertiden ske i massive applikationer; dog betyder det normalt, at der sker en uendelig rekursion i vores kode.
  • NoClassDefFoundError - denne undtagelse betyder, at en klasse ikke kunne indlæses enten på grund af ikke at være på klassestien eller på grund af fejl i statisk initialisering.
  • OutOfMemoryError - denne undtagelse betyder, at JVM ikke har mere hukommelse tilgængelig til at allokere til flere objekter. Nogle gange skyldes dette en hukommelseslækage.

8. Konklusion

I denne artikel har vi gennemgået det grundlæggende i undtagelseshåndtering samt nogle eksempler på god og dårlig praksis.

Som altid kan al kode, der findes i denne artikel, findes på GitHub!