Afhandeling van uitzonderingen in Java

1. Overzicht

In deze tutorial zullen we de basisprincipes van het afhandelen van uitzonderingen in Java doorlopen, evenals enkele van de valstrikken ervan.

2. Eerste beginselen

2.1. Wat is het?

Laten we een vergelijking uit de praktijk maken om uitzonderingen en de afhandeling van uitzonderingen beter te begrijpen.

Stel je voor dat we een product online bestellen, maar onderweg is er een storing in de levering. Een goed bedrijf kan dit probleem oplossen en ons pakket gracieus omleiden, zodat het alsnog op tijd aankomt.

Evenzo kan de code in Java fouten ondervinden tijdens het uitvoeren van onze instructies. Is goed afhandeling van uitzonderingen kan omgaan met fouten en het programma op een elegante manier omleiden om de gebruiker nog steeds een positieve ervaring te geven.

2.2. Waarom zou u het gebruiken?

We schrijven meestal code in een geïdealiseerde omgeving: het bestandssysteem bevat altijd onze bestanden, het netwerk is gezond en de JVM heeft altijd voldoende geheugen. Soms noemen we dit het "gelukkige pad".

Tijdens de productie kunnen bestandssystemen corrumperen, netwerken kapot gaan en JVM's hebben onvoldoende geheugen. Het welzijn van onze code hangt af van hoe deze omgaat met "ongelukkige paden".

We moeten omgaan met deze voorwaarden omdat ze de stroom van de aanvraag negatief en vorm beïnvloeden uitzonderingen:

openbare statische lijst getPlayers () gooit IOException {Path path = Paths.get ("players.dat"); Lijstspelers = Files.readAllLines (pad); return players.stream () .map (Player :: new) .collect (Collectors.toList ()); }

Deze code kiest ervoor om de IOException, door het in plaats daarvan door te geven aan de call-stack. In een geïdealiseerde omgeving werkt de code prima.

Maar wat kan er gebeuren in de productie als players.dat ontbreekt?

Uitzondering in thread "main" java.nio.file.NoSuchFileException: players.dat <- Players.dat-bestand bestaat niet op sun.nio.fs.WindowsException.translateToIOException (onbekende bron) op sun.nio.fs.WindowsException .rethrowAsIOException (onbekende bron) // ... meer stacktracering op java.nio.file.Files.readAllLines (onbekende bron) op java.nio.file.Files.readAllLines (onbekende bron) op exceptions.getPlayers (uitzonderingen.java : 12) <- Uitzondering doet zich voor in de methode getPlayers (), op regel 12 op Exceptional.main (Exception.java:19) <- getPlayers () wordt aangeroepen door main (), op regel 19

Zonder deze uitzondering te behandelen, kan een verder gezond programma helemaal stoppen met werken! We moeten ervoor zorgen dat onze code een plan heeft voor het geval er iets misgaat.

Let hier ook op nog een voordeel op uitzonderingen, en dat is de stacktracering zelf. Vanwege deze stacktrace kunnen we vaak aanstootgevende code lokaliseren zonder een debugger te hoeven koppelen.

3. Uitzonderingshiërarchie

Uiteindelijk, uitzonderingen zijn slechts Java-objecten waarvan ze allemaal zich uitstrekken vanaf Gooibaar:

 ---> Throwable uitzonderingsfout | (aangevinkt) (niet aangevinkt) | RuntimeException (niet aangevinkt)

Er zijn drie hoofdcategorieën van uitzonderlijke omstandigheden:

  • Gecontroleerde uitzonderingen
  • Ongecontroleerde uitzonderingen / Runtime-uitzonderingen
  • Fouten

Runtime en ongecontroleerde uitzonderingen verwijzen naar hetzelfde. We kunnen ze vaak door elkaar gebruiken.

3.1. Uitzonderingen gecontroleerd

Gecontroleerde uitzonderingen zijn uitzonderingen die de Java-compiler door ons moet afhandelen. We moeten ofwel de uitzondering declaratief naar boven gooien in de call-stack, of we moeten het zelf afhandelen. Over beide in een oogwenk meer.

Oracle's documentatie vertelt ons om gecontroleerde uitzonderingen te gebruiken wanneer we redelijkerwijs kunnen verwachten dat de beller van onze methode kan herstellen.

Een paar voorbeelden van gecontroleerde uitzonderingen zijn IOException en ServletException.

3.2. Uitzonderingen niet aangevinkt

Niet-aangevinkte uitzonderingen zijn uitzonderingen die de Java-compiler doet niet vereisen dat we omgaan.

Simpel gezegd, als we een uitzondering maken die zich uitstrekt RuntimeException, het zal worden uitgeschakeld; anders wordt het gecontroleerd.

En hoewel dit handig klinkt, vertelt de documentatie van Oracle ons dat er goede redenen zijn voor beide concepten, zoals een onderscheid maken tussen een situationele fout (gecontroleerd) en een gebruiksfout (niet gecontroleerd).

Enkele voorbeelden van ongecontroleerde uitzonderingen zijn NullPointerException, IllegalArgumentException, en Beveiligingsuitzondering.

3.3. Fouten

Fouten vertegenwoordigen ernstige en meestal onherstelbare omstandigheden, zoals incompatibiliteit van een bibliotheek, oneindige recursie of geheugenlekken.

En ook al strekken ze zich niet uit RuntimeException, ze zijn ook niet aangevinkt.

In de meeste gevallen zou het raar voor ons zijn om te behandelen, instantiëren of uit te breiden Fouten. Meestal willen we dat deze zich helemaal naar boven verspreiden.

Een paar voorbeelden van fouten zijn een StackOverflowError en Onvoldoende geheugen fout.

4. Uitzonderingen afhandelen

In de Java API zijn er tal van plaatsen waar dingen mis kunnen gaan, en sommige van deze plaatsen zijn gemarkeerd met uitzonderingen, hetzij in de handtekening of in de Javadoc:

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

Zoals een beetje eerder gezegd, als we deze 'risicovolle' methoden noemen, we moet de gecontroleerde uitzonderingen afhandelen, en wij mei omgaan met de ongecontroleerde. Java biedt ons verschillende manieren om dit te doen:

4.1. gooit

De eenvoudigste manier om een ​​uitzondering te 'afhandelen', is door deze opnieuw te plaatsen:

public int getPlayerScore (String playerFile) gooit FileNotFoundException {Scanner-inhoud = nieuwe scanner (nieuw bestand (playerFile)); retourneer Integer.parseInt (contents.nextLine ()); }

Omdat FileNotFoundException is een gecontroleerde uitzondering, dit is de eenvoudigste manier om aan de compiler te voldoen, maar het betekent wel dat iedereen die onze methode aanroept er nu ook mee moet omgaan!

parseInt kan een NumberFormatException, maar omdat het niet is aangevinkt, hoeven we het niet af te handelen.

4.2. proberen te vangen

Als we de uitzondering zelf willen proberen af ​​te handelen, kunnen we een proberen te vangen blok. We kunnen het aan door onze uitzondering opnieuw te gooien:

public int getPlayerScore (String playerFile) {probeer {Scanner-inhoud = nieuwe scanner (nieuw bestand (playerFile)); retourneer Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException noFile) {throw new IllegalArgumentException ("Bestand niet gevonden"); }}

Of door herstelstappen uit te voeren:

public int getPlayerScore (String playerFile) {probeer {Scanner-inhoud = nieuwe scanner (nieuw bestand (playerFile)); retourneer Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException noFile) {logger.warn ("Bestand niet gevonden, score opnieuw instellen."); retourneer 0; }}

4.3. Tenslotte

Nu zijn er momenten dat we code hebben die moet worden uitgevoerd, ongeacht of er zich een uitzondering voordoet, en dit is waar de Tenslotte trefwoord komt binnen.

In onze voorbeelden tot dusverre is er een vervelende bug op de loer in de schaduw, namelijk dat Java standaard geen bestandsgrepen naar het besturingssysteem retourneert.

Of we het bestand nu kunnen lezen of niet, we willen er zeker van zijn dat we de juiste opruiming uitvoeren!

Laten we dit eerst op de "luie" manier proberen:

public int getPlayerScore (String playerFile) gooit FileNotFoundException {Scanner content = null; probeer {content = nieuwe scanner (nieuw bestand (playerFile)); retourneer Integer.parseInt (contents.nextLine ()); } eindelijk {if (content! = null) {contents.close (); }}} 

Hier de Tenslotte block geeft aan welke code we Java willen laten draaien, ongeacht wat er gebeurt met het proberen om het bestand te lezen.

Zelfs als een FileNotFoundException wordt opgeworpen in de call-stack, zal Java de inhoud aanroepen van Tenslotte voordat je dat doet.

We kunnen ook allebei de uitzondering afhandelen en zorg ervoor dat onze bronnen worden gesloten:

public int getPlayerScore (String playerFile) {Scanner inhoud; probeer {content = nieuwe scanner (nieuw bestand (playerFile)); retourneer Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException noFile) {logger.warn ("Bestand niet gevonden, score opnieuw instellen."); retourneer 0; } eindelijk {probeer {if (content! = null) {contents.close (); }} catch (IOException io) {logger.error ("Kon de lezer niet sluiten!", io); }}}

Omdat dichtbij is ook een "riskante" methode, we moeten ook de uitzondering opvangen!

Dit ziet er misschien behoorlijk ingewikkeld uit, maar we hebben elk onderdeel nodig om elk mogelijk probleem dat zich kan voordoen, correct aan te pakken.

4.4. proberen-met-middelen

Gelukkig kunnen we vanaf Java 7 de bovenstaande syntaxis vereenvoudigen bij het werken met zaken die zich uitbreiden AutoCloseable:

public int getPlayerScore (String playerFile) {try (Scanner content = new Scanner (new File (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException e) {logger.warn ("Bestand niet gevonden, score opnieuw instellen."); retourneer 0; }}

Wanneer we referenties plaatsen die zijn AutoClosable in de proberen aangifte, dan hoeven we de bron niet zelf te sluiten.

We kunnen nog steeds een Tenslotte blokkeer echter om elke andere vorm van opruiming te doen die we willen.

Bekijk ons ​​artikel gewijd aan proberen-met-middelen voor meer informatie.

4.5. Meerdere vangst Blokken

Soms kan de code meer dan één uitzondering genereren en kunnen we er meer dan één hebben vangst blokgreep elk afzonderlijk:

public int getPlayerScore (String playerFile) {try (Scanner content = new Scanner (new File (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (IOException e) {logger.warn ("Player-bestand kan niet worden geladen!", e); retourneer 0; } catch (NumberFormatException e) {logger.warn ("Player-bestand is beschadigd!", e); retourneer 0; }}

Meerdere vangsten geven ons de kans om elke uitzondering anders te behandelen, mocht dat nodig zijn.

Merk hier ook op dat we niet hebben gepakt FileNotFoundException, en dat is omdat het breidt IOException uit. Omdat we vangen IOException, Java beschouwt elk van zijn subklassen ook als behandeld.

Laten we echter zeggen dat we moeten behandelen FileNotFoundException anders dan de meer algemene IOException:

public int getPlayerScore (String playerFile) {try (Scanner content = new Scanner (new File (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (FileNotFoundException e) {logger.warn ("Spelerbestand niet gevonden!", e); retourneer 0; } catch (IOException e) {logger.warn ("Player-bestand kan niet worden geladen!", e); retourneer 0; } catch (NumberFormatException e) {logger.warn ("Player-bestand is beschadigd!", e); retourneer 0; }}

Met Java kunnen we uitzonderingen voor subklassen afzonderlijk behandelen, vergeet niet om ze hoger in de vangstlijst te plaatsen.

4.6. Unie vangst Blokken

Als we echter weten dat de manier waarop we met fouten omgaan hetzelfde zal zijn, introduceerde Java 7 de mogelijkheid om meerdere uitzonderingen in hetzelfde blok op te vangen:

public int getPlayerScore (String playerFile) {try (Scanner content = new Scanner (new File (playerFile))) {return Integer.parseInt (contents.nextLine ()); } catch (IOException | NumberFormatException e) {logger.warn ("Kan score niet laden!", e); retourneer 0; }}

5. Uitzonderingen gooien

Als we de uitzondering niet zelf willen afhandelen of als we onze uitzonderingen willen genereren zodat anderen deze kunnen afhandelen, dan moeten we vertrouwd raken met de werpen trefwoord.

Laten we zeggen dat we de volgende gecontroleerde uitzondering hebben die we zelf hebben gemaakt:

public class TimeoutException breidt uitzondering {public TimeoutException (String bericht) {super (bericht) uit; }}

en we hebben een methode die mogelijk lang kan duren om te voltooien:

openbare lijst loadAllPlayers (String playersFile) {// ... mogelijk lange operatie}

5.1. Een gecontroleerde uitzondering gooien

Alsof we terugkeren van een methode, kunnen we dat werpen op elk punt.

Natuurlijk moeten we gooien als we proberen aan te geven dat er iets mis is gegaan:

openbare lijst loadAllPlayers (String playersFile) gooit TimeoutException {while (! tooLong) {// ... mogelijk lange operatie} gooi nieuwe TimeoutException ("Deze operatie duurde te lang"); }

Omdat TimeoutException is aangevinkt, moeten we ook de gooit trefwoord in de handtekening, zodat bellers van onze methode weten dat ze ermee moeten omgaan.

5.2. Werpeneen ongecontroleerde uitzondering

Als we bijvoorbeeld invoer willen valideren, kunnen we in plaats daarvan een niet-aangevinkte uitzondering gebruiken:

openbare lijst loadAllPlayers (String playersFile) gooit TimeoutException {if (! isFilenameValid (playersFile)) {gooi nieuwe IllegalArgumentException ("Bestandsnaam is niet geldig!"); } // ...} 

Omdat IllegalArgumentException niet is aangevinkt, hoeven we de methode niet te markeren, hoewel we welkom zijn.

Sommigen markeren de methode sowieso als een vorm van documentatie.

5.3. Inpakken en opnieuw gooien

We kunnen er ook voor kiezen om een ​​uitzondering die we hebben opgemerkt, opnieuw te plaatsen:

openbare lijst loadAllPlayers (String playersFile) gooit IOException {try {// ...} catch (IOException io) {throw io; }}

Of doe een wrap en werp opnieuw:

openbare lijst loadAllPlayers (String playersFile) gooit PlayerLoadException {probeer {// ...} catch (IOException io) {gooi nieuwe PlayerLoadException (io); }}

Dit kan handig zijn om veel verschillende uitzonderingen in één te consolideren.

5.4. Opnieuw gooien Gooibaar of Uitzondering

Nu voor een speciaal geval.

Als de enige mogelijke uitzonderingen die een bepaald codeblok zou kunnen veroorzaken, zijn niet aangevinkt uitzonderingen, dan kunnen we vangen en opnieuw werpen Gooibaar of Uitzondering zonder ze toe te voegen aan onze methodehandtekening:

openbare lijst loadAllPlayers (String playersFile) {probeer {throw new NullPointerException (); } catch (Throwable t) {throw t; }}

Hoewel het eenvoudig is, kan de bovenstaande code geen gecontroleerde uitzondering genereren en daarom, hoewel we een gecontroleerde uitzondering opnieuw gooien, hoeven we de handtekening niet te markeren met een gooit clausule.

Dit is handig met proxyklassen en -methoden. Meer hierover vind je hier.

5.5. Erfenis

Als we methoden markeren met een gooit trefwoord, beïnvloedt het hoe subklassen onze methode kunnen overschrijven.

In het geval dat onze methode een gecontroleerde uitzondering genereert:

public class uitzonderingen {public List loadAllPlayers (String playersFile) gooit TimeoutException {// ...}}

Een subklasse kan een 'minder risicovolle' handtekening hebben:

public class FewerExceptions breidt uitzonderingen uit {@Override public List loadAllPlayers (String playersFile) {// overschreven}}

Maar niet een "meer riskantere ”handtekening:

public class MoreExceptions breidt uitzonderingen uit {@Override public List loadAllPlayers (String playersFile) gooit MyCheckedException {// overschreven}}

Dit komt doordat contracten bij het samenstellen worden bepaald door het referentietype. Als ik een instantie van Meer Uitzonderingen en bewaar het in Uitzonderingen:

Uitzonderingen uitzonderingen = nieuwe MoreExceptions (); exception.loadAllPlayers ("bestand");

Dan zegt de JVM alleen dat ik dat moet doen vangst de TimeoutException, wat niet klopt sinds ik dat heb gezegd MoreExceptions # loadAllPlayers gooit een andere uitzondering.

Simpel gezegd, subklassen kunnen gooien minder gecontroleerde uitzonderingen dan hun superklasse, maar niet meer.

6. Anti-patronen

6.1. Uitzonderingen slikken

Nu is er nog een andere manier waarop we de compiler tevreden hadden kunnen stellen:

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

Het bovenstaande wordt genoemdeen uitzondering inslikken. Meestal zou het een beetje gemeen voor ons zijn om dit te doen, omdat het het probleem niet aanpakt en het zorgt ervoor dat andere code het probleem ook niet kan oplossen.

Er zijn momenten waarop er een gecontroleerde uitzondering is waarvan we zeker weten dat deze nooit zal gebeuren. In die gevallen moeten we toch in ieder geval een opmerking toevoegen waarin staat dat we de uitzondering opzettelijk hebben opgegeten:

public int getPlayerScore (String playerFile) {try {// ...} catch (IOException e) {// dit zal nooit gebeuren}}

Een andere manier waarop we een uitzondering kunnen 'inslikken', is door de uitzondering op de foutenstroom gewoon af te drukken:

public int getPlayerScore (String playerFile) {probeer {// ...} catch (uitzondering e) {e.printStackTrace (); } retourneren 0; }

We hebben onze situatie een beetje verbeterd door de fout op zijn minst ergens op te schrijven voor een latere diagnose.

Het zou echter beter zijn om een ​​logger te gebruiken:

public int getPlayerScore (String playerFile) {try {// ...} catch (IOException e) {logger.error ("Kan de score niet laden", e); retourneer 0; }}

Hoewel het voor ons erg handig is om uitzonderingen op deze manier te behandelen, moeten we ervoor zorgen dat we geen belangrijke informatie inslikken die bellers van onze code zouden kunnen gebruiken om het probleem op te lossen.

Ten slotte kunnen we onbedoeld een uitzondering inslikken door deze niet als oorzaak op te nemen wanneer we een nieuwe uitzondering werpen:

public int getPlayerScore (String playerFile) {probeer {// ...} catch (IOException e) {gooi nieuwe PlayerScoreException (); }} 

Hier geven we onszelf een schouderklopje omdat we onze beller op een fout hebben gewezen, maar we slagen er niet in om de IOException als oorzaak. Hierdoor zijn we belangrijke informatie kwijtgeraakt die bellers of operators kunnen gebruiken om het probleem te diagnosticeren.

We kunnen beter doen:

public int getPlayerScore (String playerFile) {probeer {// ...} catch (IOException e) {gooi nieuwe PlayerScoreException (e); }}

Let op het subtiele verschil tussen het opnemen van IOException als de oorzaak van PlayerScoreException.

6.2. Gebruik makend van terugkeer in een Tenslotte Blok

Een andere manier om uitzonderingen te slikken, is door terugkeer van de Tenslotte blok. Dit is slecht omdat, door abrupt terug te keren, de JVM de uitzondering laat vallen, zelfs als deze door onze code werd weggegooid:

openbare int getPlayerScore (String playerFile) {int score = 0; probeer {gooi nieuwe IOException (); } eindelijk {return score; // <== de IOException is verwijderd}}

Volgens de Java-taalspecificatie:

Als de uitvoering van het try-blok om een ​​andere reden abrupt wordt voltooid R, dan wordt het laatste blok uitgevoerd, en dan is er een keuze.

Als het Tenslotte blok normaal wordt voltooid, waarna de try-instructie abrupt wordt voltooid om reden R.

Als het Tenslotte blok wordt abrupt voltooid om reden S, waarna de instructie try abrupt wordt voltooid om reden S (en reden R wordt verwijderd).

6.3. Gebruik makend van werpen in een Tenslotte Blok

Net als bij gebruik terugkeer in een Tenslotte block, de uitzondering gegooid in een Tenslotte block heeft voorrang op de uitzondering die optreedt in het catch-blok.

Hierdoor wordt de oorspronkelijke uitzondering van het proberen blokkeren, en we verliezen al die waardevolle informatie:

public int getPlayerScore (String playerFile) {probeer {// ...} catch (IOException io) {gooi nieuwe IllegalStateException (io); // <== opgegeten door de laatste} eindelijk {gooi nieuwe OtherException (); }}

6.4. Gebruik makend van werpen als een ga naar

Sommige mensen gaven ook toe aan de verleiding om te gebruiken werpen als een ga naar uitspraak:

public void doSomething () {probeer {// bos van code gooi nieuwe MyException (); // tweede bundel code} catch (MyException e) {// derde bundel code}}

Dit is vreemd omdat de code probeert uitzonderingen te gebruiken voor stroombesturing in plaats van foutafhandeling.

7. Veelvoorkomende uitzonderingen en fouten

Hier zijn enkele veelvoorkomende uitzonderingen en fouten die we allemaal van tijd tot tijd tegenkomen:

7.1. Uitzonderingen gecontroleerd

  • IOException - Deze uitzondering is meestal een manier om te zeggen dat er iets op het netwerk, het bestandssysteem of de database is mislukt.

7.2. RuntimeExceptions

  • ArrayIndexOutOfBoundsException - deze uitzondering betekent dat we hebben geprobeerd toegang te krijgen tot een niet-bestaande array-index, zoals wanneer we proberen om index 5 uit een array met lengte 3 te halen.
  • ClassCastException - deze uitzondering betekent dat we hebben geprobeerd een illegale cast uit te voeren, zoals het converteren van een Draad in een Lijst. We kunnen het meestal vermijden door defensief uit te voeren instantie van controles voor het casten.
  • IllegalArgumentException - deze uitzondering is voor ons een algemene manier om te zeggen dat een van de opgegeven methode- of constructorparameters ongeldig is.
  • IllegalStateException - Deze uitzondering is voor ons een algemene manier om te zeggen dat onze interne toestand, net als de toestand van ons object, ongeldig is.
  • NullPointerException - Deze uitzondering betekent dat we hebben geprobeerd te verwijzen naar een nul voorwerp. We kunnen het meestal vermijden door ofwel defensief uit te voeren nul cheques of door gebruik te maken van Optioneel.
  • NumberFormatException - Deze uitzondering betekent dat we hebben geprobeerd een Draad in een getal, maar de tekenreeks bevatte ongeldige tekens, zoals proberen om "5f3" in een getal om te zetten.

7.3. Fouten

  • StackOverflowError - deze uitzondering betekent dat de stacktracering te groot is. Dit kan soms gebeuren bij grote toepassingen; het betekent echter meestal dat er een oneindige recursie plaatsvindt in onze code.
  • NoClassDefFoundError - deze uitzondering betekent dat een klasse niet kan worden geladen omdat het niet op het klassenpad staat of omdat de statische initialisatie is mislukt.
  • Onvoldoende geheugen fout - deze uitzondering betekent dat de JVM niet meer geheugen beschikbaar heeft om voor meer objecten toe te wijzen. Soms is dit te wijten aan een geheugenlek.

8. Conclusie

In dit artikel hebben we de basisprincipes van het afhandelen van uitzonderingen doorgenomen, evenals enkele goede en slechte praktijkvoorbeelden.

Zoals altijd is alle code in dit artikel te vinden op GitHub!


$config[zx-auto] not found$config[zx-overlay] not found