Uitzonderingsafhandeling in Java

Inleiding

Een uitzondering is een foutgebeurtenis die kan optreden tijdens de uitvoering van een programma en de normale gang ervan verstoort. Java biedt een robuuste en objectgeoriënteerde manier om uitzonderingsscenario’s te behandelen, bekend als Java Exception Handling.

Uitzonderingen in Java kunnen voortkomen uit verschillende situaties, zoals verkeerde gegevens ingevoerd door de gebruiker, hardwarestoring, netwerkverbindingstoring of een database-server die niet beschikbaar is. De code die specificeert wat te doen in specifieke uitzonderingsscenario’s, wordt exception handling genoemd.

Het gooien en vangen van uitzonderingen

Java maakt een uitzonderingsobject aan wanneer er een fout optreedt tijdens de uitvoering van een instructie. Het uitzonderingsobject bevat veel debugginformatie, zoals de methodiekhierarchie, het regelnummer waar de uitzondering zich heeft voorgedaan, en het type uitzondering.

Als er een uitzondering optreedt in een methode, wordt het proces van het maken van het uitzonderingsobject en het overdragen ervan aan de runtime-omgeving “het gooien van de uitzondering” genoemd. De normale stroom van het programma stopt en de Java Runtime Environment (JRE) probeert de handler voor de uitzondering te vinden. De uitzonderingsafhandelaar is het blok code dat het uitzonderingsobject kan verwerken.

  • De logica om de uitzonderingsafhandelaar te vinden begint met zoeken in de methode waar de fout is opgetreden.
  • Als er geen geschikte handler wordt gevonden, wordt deze verplaatst naar de aanroepende methode.
  • En zo verder.

Dus als de methodeoproepstack A->B->C is en er doet zich een uitzondering voor in methode C, dan zal de zoektocht naar de juiste handler verplaatsen van C->B->A.

Als er een geschikte uitzonderingsafhandelaar is gevonden, wordt het uitzonderingsobject doorgegeven aan de handler om het te verwerken. De handler wordt gezegd “de uitzondering op te vangen”. Als er geen geschikte uitzonderingsafhandelaar is gevonden, wordt het programma beëindigd en worden er gegevens over de uitzondering naar de console afgedrukt.

Het Java-uitzonderingsafhandelingsframework wordt alleen gebruikt om runtime-fouten af te handelen. De compileerfouten moeten worden opgelost door de ontwikkelaar die de code schrijft, anders wordt het programma niet uitgevoerd.

Java-uitzonderingsafhandelingssleutelwoorden

Java biedt specifieke trefwoorden voor het hanteren van uitzonderingen.

  1. throw – We weten dat als er een fout optreedt, er een uitzonderingsobject wordt gemaakt en vervolgens de Java-runtime begint met het verwerken ervan. Soms willen we mogelijk expliciet uitzonderingen genereren in onze code. Bijvoorbeeld, in een programma voor gebruikersauthenticatie moeten we uitzonderingen naar clients gooien als het wachtwoord null is. Het trefwoord throw wordt gebruikt om uitzonderingen naar de runtime te gooien om ze te laten afhandelen.
  2. throws – Wanneer we een uitzondering gooien in een methode en deze niet afhandelen, moeten we het trefwoord throws gebruiken in de methodehandtekening om het aanroepende programma te laten weten welke uitzonderingen door de methode kunnen worden gegooid. De aanroepende methode kan deze uitzonderingen afhandelen of doorgeven aan zijn aanroepende methode met behulp van het trefwoord throws. We kunnen meerdere uitzonderingen opgeven in de throws-clausule, en het kan ook worden gebruikt met de main()-methode.
  3. try-catch – We gebruiken het try-catch-blok voor uitzonderingsafhandeling in onze code. try is het begin van het blok en catch staat aan het einde van het try-blok om de uitzonderingen af te handelen. We kunnen meerdere catch-blokken hebben met een try-blok. Het try-catch-blok kan ook genest zijn. Het catch-blok vereist een parameter die van het type Exception moet zijn.
  4. eindelijk – het eindelijk blok is optioneel en kan alleen worden gebruikt met een try-catch blok. Aangezien een uitzondering het uitvoeringsproces onderbreekt, kunnen we enkele bronnen open hebben die niet worden gesloten, dus kunnen we het eindelijk blok gebruiken. Het eindelijk blok wordt altijd uitgevoerd, of er nu een uitzondering is opgetreden of niet.

Een Voorbeeld van Uitzonderingsafhandeling

ExceptionHandling.java
package com.journaldev.exceptions;

import java.io.FileNotFoundException;
import java.io.IOException;

public class ExceptionHandling {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		try {
			testException(-5);
			testException(-10);
		} catch(FileNotFoundException e) {
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		} finally {
			System.out.println("Releasing resources");
		}
		testException(15);
	}

	public static void testException(int i) throws FileNotFoundException, IOException {
		if (i < 0) {
			FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
			throw myException;
		} else if (i > 10) {
			throw new IOException("Only supported for index 0 to 10");
		}
	}
}
  • De methode testException() gooit uitzonderingen met behulp van het throw trefwoord. De methodehandtekening gebruikt het throws trefwoord om de beller te laten weten welk type uitzonderingen het zou kunnen gooien.
  • In de main() methode handel ik uitzonderingen af met behulp van het try-catch blok in de main() methode. Wanneer ik het niet afhandel, propageer ik het naar runtime met de throws clausule in de main() methode.
  • De testException(-10) wordt nooit uitgevoerd vanwege de uitzondering en vervolgens wordt het eindelijk blok uitgevoerd.

De printStackTrace() is een van de nuttige methoden in de Exception klasse voor debugdoeleinden.

Deze code geeft het volgende uit:

Output
java.io.FileNotFoundException: Negative Integer -5 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10) Releasing resources Exception in thread "main" java.io.IOException: Only supported for index 0 to 10 at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27) at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)

Enkele belangrijke punten om op te merken:

  • We kunnen geen catch of eindelijk clausule hebben zonder een try verklaring.
  • A try statement should have either catch block or finally block, it can have both blocks.
  • We kunnen geen code schrijven tussen try-catch-finally-blokken.
  • We kunnen meerdere catch-blokken hebben met één try-verklaring.
  • try-catch-blokken kunnen genest zijn, net als if-else-verklaringen.
  • We kunnen slechts één finally-blok hebben met een try-catch-verklaring.

Java Exception-hiërarchie

Zoals eerder vermeld, wordt bij het genereren van een uitzondering een uitzonderingsobject gemaakt. Java-uitzonderingen zijn hiërarchisch en overerving wordt gebruikt om verschillende soorten uitzonderingen te categoriseren. Throwable is de bovenliggende klasse van de Java Exception-hiërarchie en heeft twee onderliggende objecten – Error en Exception. Exceptions zijn verder onderverdeeld in gecontroleerde Exceptions en runtime-Exceptions.

  1. Fouten: Errors zijn uitzonderlijke scenario’s die buiten de reikwijdte van de toepassing vallen en waarvan het niet mogelijk is ze te voorzien en te herstellen. Bijvoorbeeld hardwarefouten, Java Virtual Machine (JVM)-crash of out-of-memory fout. Daarom hebben we een aparte hiërarchie van Errors en zouden we niet moeten proberen deze situaties af te handelen. Enkele veelvoorkomende Errors zijn OutOfMemoryError en StackOverflowError.
  2. Gecontroleerde Uitzonderingen: Gecontroleerde Exceptions zijn uitzonderingsscenario’s die we kunnen verwachten in een programma en waarvan we proberen te herstellen. Bijvoorbeeld, FileNotFoundException. We moeten deze uitzondering opvangen en een bruikbaar bericht aan de gebruiker verstrekken en het correct registreren voor debugdoeleinden. De Exception is de bovenliggende klasse van alle gecontroleerde Exceptions. Als we een gecontroleerde Exception gooien, moeten we deze opvangen in dezelfde methode, of we moeten deze doorgeven aan de beller met behulp van het throws-trefwoord.
  3. Uitzondering tijdens uitvoering: Uitzonderingen tijdens uitvoering worden veroorzaakt door slechte programmering. Bijvoorbeeld, proberen een element uit een array op te halen. We moeten eerst de lengte van de array controleren voordat we proberen het element op te halen, anders kan het een ArrayIndexOutOfBoundException veroorzaken tijdens de uitvoering. RuntimeException is de bovenliggende klasse van alle uitzonderingen tijdens uitvoering. Als we in een methode een willekeurige Exception gooien, is het niet vereist om deze te specificeren in de methodeondertekening throws-clausule. Uitzonderingen tijdens uitvoering kunnen worden vermeden met betere programmering.

Enkele handige methoden van Exception-klassen

Java Exception en al zijn subklassen bieden geen specifieke methoden, en alle methoden zijn gedefinieerd in de basisklasse – Throwable. De klassen Exception worden gemaakt om verschillende soorten Exception-scenario’s te specificeren, zodat we gemakkelijk de oorzaak kunnen identificeren en de Exception kunnen afhandelen volgens het type ervan. De klasse Throwable implementeert de interface Serializable voor interoperabiliteit.

Enkele van de bruikbare methoden van de klasse Throwable zijn:

  1. public String getMessage() – Deze methode retourneert de berichttekst van Throwable en het bericht kan worden opgegeven bij het maken van de uitzondering via de constructor.
  2. public String getLocalizedMessage() – Deze methode wordt geleverd zodat subklassen deze kunnen overschrijven om een taalspecifiek bericht te leveren aan het aanroepende programma. De implementatie van de klasse Throwable van deze methode gebruikt de methode getMessage() om het uitzonderingsbericht te retourneren.
  3. public synchronized Throwable getCause() – Deze methode retourneert de oorzaak van de uitzondering of null als de oorzaak onbekend is.
  4. public String toString() – Deze methode retourneert informatie over Throwable in String-formaat, de geretourneerde String bevat de naam van de klasse Throwable en het gelokaliseerde bericht.
  5. public void printStackTrace() – Deze methode drukt de stack trace-informatie af naar de standaardfoutstroom, deze methode is overbelast, en we kunnen PrintStream of PrintWriter als argument doorgeven om de stack trace-informatie naar het bestand of de stroom te schrijven.

Java 7 Automatisch Resourcebeheer en Catch-blokverbeteringen

Als je veel uitzonderingen catcht in een enkel try-blok, zul je merken dat de code van het catch-blok voornamelijk bestaat uit redundante code om de fout te loggen. In Java 7 was een van de functies een verbeterd catch-blok waar we meerdere uitzonderingen kunnen catchen in een enkel catch-blok. Hier is een voorbeeld van het catch-blok met deze functie:

catch (IOException | SQLException ex) {
    logger.error(ex);
    throw new MyException(ex.getMessage());
}

Er zijn enkele beperkingen zoals dat het uitzonderingsobject definitief is en we het niet kunnen wijzigen binnen het catch-blok, lees de volledige analyse op Java 7 Catch Block Improvements.

Meestal gebruiken we het finally-blok alleen om de resources te sluiten. Soms vergeten we ze te sluiten en krijgen we runtime-uitzonderingen wanneer de resources zijn uitgeput. Deze uitzonderingen zijn moeilijk te debuggen, en we moeten misschien op elke plaats waar we die resource gebruiken controleren of we hem sluiten. In Java 7 was een van de verbeteringen try-with-resources, waar we een resource in de try-verklaring zelf kunnen maken en het binnen het try-catch-blok kunnen gebruiken. Wanneer de uitvoering uit het try-catch-blok komt, sluit de runtime-omgeving deze resources automatisch. Hier is een voorbeeld van het try-catch-blok met deze verbetering:

try (MyResource mr = new MyResource()) {
	System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
	e.printStackTrace();
}

A Custom Exception Class Example

Java biedt ons veel uitzonderingsklassen om te gebruiken, maar soms moeten we onze eigen aangepaste uitzonderingsklassen maken. Bijvoorbeeld, om de aanroeper op de hoogte te stellen van een specifiek type uitzondering met de juiste boodschap. We kunnen aangepaste velden hebben voor het bijhouden, zoals foutcodes. Stel bijvoorbeeld dat we een methode schrijven om alleen tekstbestanden te verwerken, dan kunnen we de aanroeper de juiste foutcode geven wanneer een ander type bestand als invoer wordt verzonden.

Eerst, maak MyException:

MyException.java
package com.journaldev.exceptions;

public class MyException extends Exception {

	private static final long serialVersionUID = 4664456874499611218L;

	private String errorCode = "Unknown_Exception";

	public MyException(String message, String errorCode) {
		super(message);
		this.errorCode=errorCode;
	}

	public String getErrorCode() {
		return this.errorCode;
	}
}

Vervolgens, maak een CustomExceptionExample:

CustomExceptionExample.java
package com.journaldev.exceptions;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class CustomExceptionExample {

	public static void main(String[] args) throws MyException {
		try {
			processFile("file.txt");
		} catch (MyException e) {
			processErrorCodes(e);
		}
	}

	private static void processErrorCodes(MyException e) throws MyException {
		switch (e.getErrorCode()) {
			case "BAD_FILE_TYPE":
				System.out.println("Bad File Type, notify user");
				throw e;
			case "FILE_NOT_FOUND_EXCEPTION":
				System.out.println("File Not Found, notify user");
				throw e;
			case "FILE_CLOSE_EXCEPTION":
				System.out.println("File Close failed, just log it.");
				break;
			default:
				System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
				e.printStackTrace();
		}
	}

	private static void processFile(String file) throws MyException {
		InputStream fis = null;

		try {
			fis = new FileInputStream(file);
		} catch (FileNotFoundException e) {
			throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
		} finally {
			try {
				if (fis != null) fis.close();
			} catch (IOException e) {
				throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
			}
		}
	}
}

We kunnen een aparte methode hebben om verschillende soorten foutcodes te verwerken die we krijgen van verschillende methoden. Sommige ervan worden verbruikt omdat we de gebruiker daar misschien niet van op de hoogte willen stellen, of sommige ervan gooien we terug om de gebruiker op de hoogte te stellen van het probleem.

Hier breid ik Exception uit, zodat wanneer deze uitzondering wordt gegenereerd, deze moet worden afgehandeld in de methode of teruggegeven aan het aanroepende programma. Als we RuntimeException uitbreiden, is het niet nodig om dit te specificeren in de throws-clausule.

Dit was een ontwerpbeslissing. Het gebruik van Gecontroleerde Exceptions heeft het voordeel dat het ontwikkelaars helpt te begrijpen welke uitzonderingen ze kunnen verwachten en passende maatregelen te nemen om ze af te handelen.

Best Practices voor Exception Handling in Java

  • Gebruik Specifieke Uitzonderingen – Basis klassen van de uitzonderingshiërarchie bieden geen nuttige informatie, daarom heeft Java zoveel uitzonderingsklassen, zoals IOException met verdere subklassen zoals FileNotFoundException, EOFException, enzovoort. We moeten altijd specifieke uitzonderingsklassen throw en catch zodat de aanroeper gemakkelijk de hoofdoorzaak van de uitzondering zal kennen en ze kan verwerken. Dit maakt het debuggen eenvoudiger en helpt clienttoepassingen uitzonderingen op de juiste manier af te handelen.
  • Goed vroeg of Falen-Snel – We moeten proberen uitzonderingen zo vroeg mogelijk te throw. Neem de bovenstaande processFile() methode, als we het null argument aan deze methode doorgeven, krijgen we de volgende uitzondering:
Output
Exception in thread "main" java.lang.NullPointerException at java.io.FileInputStream.<init>(FileInputStream.java:134) at java.io.FileInputStream.<init>(FileInputStream.java:97) at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)

Tijdens het debuggen moeten we goed naar de stack trace kijken om de werkelijke locatie van de uitzondering te identificeren. Als we onze implementatielogica wijzigen om deze uitzonderingen vroegtijdig te controleren zoals hieronder:

private static void processFile(String file) throws MyException {
	if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");

	// ... verdere verwerking
}

dant zal de uitzonderingsstack trace aangeven waar de uitzondering is opgetreden met een duidelijke boodschap:

Output
com.journaldev.exceptions.MyException: File name can't be null at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37) at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
  • Vang Laat – Omdat Java afdwingt om de gecontroleerde uitzondering te behandelen of deze te verklaren in de methodehandtekening, hebben ontwikkelaars soms de neiging om de uitzondering op te vangen en de fout te loggen. Maar deze praktijk is schadelijk omdat het aanroepende programma geen melding krijgt van de uitzondering. We moeten uitzonderingen alleen opvangen als we ze op de juiste manier kunnen afhandelen. Bijvoorbeeld, in de bovenstaande methode gooi ik uitzonderingen terug naar de aanroepende methode om ze te laten afhandelen. Dezelfde methode kan worden gebruikt door andere toepassingen die de uitzondering mogelijk op een andere manier willen verwerken. Bij het implementeren van een functie moeten we altijd uitzonderingen terug gooien naar de aanroeper en hen laten beslissen hoe ze ermee om moeten gaan.
  • Resources Sluiten – Omdat uitzonderingen de verwerking van het programma stoppen, moeten we alle bronnen sluiten in een finally-blok of de Java 7 try-with-resources-verbetering gebruiken om Java-runtime deze voor u te laten sluiten.
  • Loggen van Uitzonderingen – We zouden altijd uitzonderingsberichten moeten loggen en bij het gooien van uitzonderingen een duidelijk bericht moeten geven, zodat de beller gemakkelijk weet waarom de uitzondering is opgetreden. We zouden altijd een leeg catch-blok moeten vermijden dat alleen de uitzondering opvangt en geen zinvolle details van de uitzondering biedt voor het debuggen.
  • Enkelvoudig catch-blok voor meerdere uitzonderingen – Meestal loggen we uitzonderingsdetails en geven we een bericht aan de gebruiker, in dit geval moeten we de Java 7-functie gebruiken voor het afhandelen van meerdere uitzonderingen in een enkel catch-blok. Deze aanpak zal onze codeomvang verminderen en het zal er ook schoner uitzien.
  • Het Gebruik van Aangepaste Uitzonderingen – Het is altijd beter om een uitzonderingsafhandelingsstrategie te definiëren bij het ontwerpen en in plaats van meerdere uitzonderingen te gooien en te vangen, kunnen we een aangepaste uitzondering maken met een foutcode, en het aanroepende programma kan deze foutcodes afhandelen. Het is ook een goed idee om een hulpprogramma-methode te maken om verschillende foutcodes te verwerken en ze te gebruiken.
  • Naamgevingsconventies en Verpakking – Wanneer je je aangepaste uitzondering maakt, zorg er dan voor dat het eindigt met Exception, zodat het vanuit de naam zelf duidelijk is dat het een uitzonderingsklasse is. Zorg er ook voor dat je ze verpakt zoals dat gebeurt in de Java Development Kit (JDK). Bijvoorbeeld, IOException is de basissuggestie voor alle IO-operaties.
  • Gebruik Uitzonderingen Verstandig – Uitzonderingen zijn kostbaar, en soms is het helemaal niet nodig om uitzonderingen te gooien, en kunnen we een boolean variabele teruggeven aan het aanroepende programma om aan te geven of een bewerking succesvol was of niet. Dit is handig wanneer de bewerking optioneel is, en je wilt niet dat je programma vastloopt omdat het mislukt. Bijvoorbeeld, tijdens het bijwerken van de aandelenkoersen in de database vanuit een webservice van een derde partij, willen we misschien vermijden uitzonderingen te gooien als de verbinding mislukt.
  • Documenteer de Geworpen Uitzonderingen – Gebruik Javadoc @throws om duidelijk de uitzonderingen aan te geven die door de methode worden gegooid. Het is zeer nuttig wanneer je een interface aanbiedt voor andere applicaties om te gebruiken.

Conclusie

In dit artikel heb je geleerd over uitzonderingsafhandeling in Java. Je hebt geleerd over throw en throws. Je hebt ook geleerd over try (en try-with-resources), catch, en finally blokken.

Source:
https://www.digitalocean.com/community/tutorials/exception-handling-in-java