Fehlerbehandlung in Java

Einführung

Eine Ausnahme ist ein Fehlerereignis, das während der Ausführung eines Programms auftreten kann und seinen normalen Ablauf stört. Java bietet einen robusten und objektorientierten Ansatz zur Behandlung von Ausnahmeszenarien, bekannt als Java Exception Handling.

Ausnahmen in Java können aus verschiedenen Situationen entstehen, wie z. B. falschen Daten, die vom Benutzer eingegeben wurden, Hardwarefehlern, Netzwerkverbindungsfehlern oder einem nicht erreichbaren Datenbankserver. Der Code, der angibt, was in bestimmten Ausnahmesituationen zu tun ist, wird als Ausnahmebehandlung bezeichnet.

Ausnahmen werfen und abfangen

Java erstellt ein Ausnahmeobjekt, wenn während der Ausführung einer Anweisung ein Fehler auftritt. Das Ausnahmeobjekt enthält viele Debugging-Informationen wie Methodenhierarchie, Zeilennummer, an der die Ausnahme aufgetreten ist, und Typ der Ausnahme.

Wenn eine Ausnahme in einer Methode auftritt, wird der Vorgang des Erstellens des Ausnahmeobjekts und dessen Übergabe an die Laufzeitumgebung als „Ausnahme werfen“ bezeichnet. Der normale Ablauf des Programms wird unterbrochen, und die Java-Laufzeitumgebung (JRE) versucht, den Handler für die Ausnahme zu finden. Ein Ausnahme-Handler ist der Block von Code, der das Ausnahmeobjekt verarbeiten kann.

  • Die Logik zur Suche nach dem Ausnahme-Handler beginnt mit der Suche in der Methode, in der der Fehler aufgetreten ist.
  • Wenn kein geeigneter Handler gefunden wird, wird er zur aufrufenden Methode weitergeleitet.
  • Und so weiter.

Wenn also der Aufrufstapel der Methode A->B->C ist und eine Ausnahme in der Methode C auftritt, wird die Suche nach dem geeigneten Handler von C->B->A verschoben.

Wenn ein geeigneter Ausnahme-Handler gefunden wird, wird das Ausnahmeobjekt an den Handler übergeben, um es zu verarbeiten. Der Handler wird als „Ausnahme abfangen“ bezeichnet. Wenn kein geeigneter Ausnahme-Handler gefunden wird, wird das Programm beendet und Informationen über die Ausnahme in die Konsole gedruckt.

Das Java-Ausnahmebehandlungsframework wird nur zur Behandlung von Laufzeitfehlern verwendet. Kompilierungsfehler müssen vom Entwickler behoben werden, der den Code schreibt, sonst wird das Programm nicht ausgeführt.

Java-Schlüsselwörter zur Ausnahmebehandlung

Java bietet spezifische Schlüsselwörter für den Umgang mit Ausnahmen.

  1. throw – Wir wissen, dass bei einem Fehler ein Ausnahmeobjekt erstellt wird und die Java-Laufzeitumgebung mit der Verarbeitung beginnt, um sie zu behandeln. Manchmal möchten wir Ausnahmen explizit in unserem Code generieren. Zum Beispiel sollten wir in einem Benutzerauthentifizierungsprogramm Ausnahmen an Clients werfen, wenn das Passwort null ist. Das Schlüsselwort throw wird verwendet, um Ausnahmen an die Laufzeitumgebung zu werfen, damit sie behandelt werden können.
  2. throws – Wenn wir in einer Methode eine Ausnahme werfen und sie nicht behandeln, müssen wir das Schlüsselwort throws in der Methodensignatur verwenden, um dem aufrufenden Programm die Ausnahmen mitzuteilen, die von der Methode geworfen werden können. Die aufrufende Methode kann diese Ausnahmen behandeln oder mit dem Schlüsselwort throws an ihre aufrufende Methode weitergeben. Wir können mehrere Ausnahmen in der throws-Klausel angeben, und es kann auch mit der main()-Methode verwendet werden.
  3. try-catch – Wir verwenden den try-catch-Block zur Behandlung von Ausnahmen in unserem Code. try markiert den Beginn des Blocks und catch befindet sich am Ende des try-Blocks, um die Ausnahmen zu behandeln. Wir können mehrere catch-Blöcke mit einem try-Block haben. Der try-catch-Block kann auch verschachtelt sein. Der catch-Block erfordert einen Parameter, der vom Typ Exception sein sollte.
  4. Endlich – der finally-Block ist optional und kann nur mit einem try-catch-Block verwendet werden. Da eine Ausnahme den Ausführungsvorgang stoppt, können einige Ressourcen geöffnet bleiben, die nicht geschlossen werden. Daher können wir den finally-Block verwenden. Der finally-Block wird immer ausgeführt, unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht.

Ein Beispiel für die Fehlerbehandlung

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");
		}
	}
}
  • Die Methode testException() wirft Ausnahmen mit dem throw-Schlüsselwort. Die Methode signiert mit dem throws-Schlüsselwort, um den Aufrufer über die Arten von Ausnahmen zu informieren, die auftreten können.
  • In der Methode main() behandele ich Ausnahmen mit dem try-catch-Block in der Methode main(). Wenn ich sie nicht behandele, leite ich sie mit der throws-Klausel in der Methode main() weiter.
  • Die Methode testException(-10) wird aufgrund der Ausnahme niemals ausgeführt, und dann wird der finally-Block ausgeführt.

Die Methode printStackTrace() ist eine der nützlichen Methoden in der Exception-Klasse für Debugging-Zwecke.

Dieser Code gibt Folgendes aus:

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)

Einige wichtige Punkte, die zu beachten sind:

  • Wir können keine catch– oder finally-Klausel ohne eine try-Anweisung haben.
  • A try statement should have either catch block or finally block, it can have both blocks.
  • Wir können keinen Code zwischen try-catch-finally-Blöcken schreiben.
  • Wir können mehrere catch-Blöcke mit einem einzigen try-Statement haben.
  • try-catch-Blöcke können ähnlich wie if-else-Anweisungen verschachtelt werden.
  • Wir können nur einen finally-Block mit einem try-catch-Statement haben.

Java-Ausnahme-Hierarchie

Wie bereits erwähnt, wird beim Auftreten einer Ausnahme ein Ausnahmeobjekt erstellt. Java-Ausnahmen sind hierarchisch strukturiert, und Vererbung wird verwendet, um verschiedene Arten von Ausnahmen zu kategorisieren. Throwable ist die Elternklasse der Java-Ausnahme-Hierarchie und hat zwei Kindobjekte – Error und Exception. Exceptions sind weiter unterteilt in überprüfte Exceptions und Laufzeit-Exceptions.

  1. Fehler: Errors sind außergewöhnliche Szenarien, die außerhalb des Anwendungsbereichs liegen und nicht vorhersehbar und nicht wiederherstellbar sind. Beispielsweise Hardwarefehler, Absturz der Java Virtual Machine (JVM) oder Out-of-Memory-Fehler. Deshalb haben wir eine separate Hierarchie von Errors und sollten versuchen, diese Situationen nicht zu behandeln. Einige der häufigsten Errors sind OutOfMemoryError und StackOverflowError.
  2. Überprüfte Ausnahmen: Überprüfte Exceptions sind außergewöhnliche Szenarien, mit denen wir in einem Programm rechnen können und versuchen, uns davon zu erholen. Zum Beispiel FileNotFoundException. Wir sollten diese Ausnahme abfangen und dem Benutzer eine nützliche Nachricht bereitstellen und sie ordnungsgemäß für Debugging-Zwecke protokollieren. Die Exception ist die Oberklasse aller überprüften Exceptions. Wenn wir eine überprüfte Exception werfen, müssen wir sie im selben Methodenblock abfangen oder sie mit dem Schlüsselwort throws an den Aufrufer weiterleiten.
  3. Laufzeit-Ausnahme: Laufzeit-Exceptions werden durch schlechtes Programmieren verursacht. Zum Beispiel der Versuch, ein Element aus einem Array abzurufen. Wir sollten zuerst die Länge des Arrays überprüfen, bevor wir versuchen, das Element abzurufen, da sonst möglicherweise zur Laufzeit eine ArrayIndexOutOfBoundException ausgelöst wird. RuntimeException ist die Oberklasse aller Laufzeit-Exceptions. Wenn wir in einer Methode eine Laufzeit-Exception werfen, ist es nicht erforderlich, sie im Methodensignatur throws-Klausel anzugeben. Laufzeit-Ausnahmen können durch besseres Programmieren vermieden werden.

Einige nützliche Methoden von Exception-Klassen

Java Exception und alle seine Unterklassen bieten keine spezifischen Methoden an, alle Methoden sind in der Basisklasse Throwable definiert. Die Exception-Klassen werden erstellt, um verschiedene Arten von Exception-Szenarien anzugeben, damit wir die Ursache leicht identifizieren und die Exception entsprechend ihres Typs behandeln können. Die Klasse Throwable implementiert das Interface Serializable für die Interoperabilität.

Einige der nützlichen Methoden der Klasse Throwable sind:

  1. public String getMessage() – Diese Methode gibt die Nachricht String von Throwable zurück, und die Nachricht kann beim Erstellen der Ausnahme über ihren Konstruktor angegeben werden.
  2. public String getLocalizedMessage() – Diese Methode wird bereitgestellt, damit Unterklassen sie überschreiben können, um eine sprachspezifische Nachricht für das aufrufende Programm bereitzustellen. Die Throwable-Klassenimplementierung dieser Methode verwendet die Methode getMessage(), um die Ausnahmemeldung zurückzugeben.
  3. public synchronized Throwable getCause() – Diese Methode gibt die Ursache der Ausnahme zurück oder null, wenn die Ursache unbekannt ist.
  4. public String toString() – Diese Methode gibt die Informationen über Throwable im String-Format zurück, der zurückgegebene String enthält den Namen der Throwable-Klasse und die lokalisierte Nachricht.
  5. public void printStackTrace() – Diese Methode gibt die Stack-Trace-Informationen auf dem Standardfehlerstrom aus. Diese Methode ist überladen, und wir können PrintStream oder PrintWriter als Argument übergeben, um die Stack-Trace-Informationen in die Datei oder den Strom zu schreiben.

Java 7 Automatisches Ressourcenmanagement und Verbesserungen im Catch-Block

Wenn Sie viele Ausnahmen in einem einzelnen try-Block abfangen, werden Sie feststellen, dass der catch-Blockcode größtenteils aus redundantem Code besteht, um den Fehler zu protokollieren. In Java 7 war eines der Features ein verbesserter catch-Block, in dem wir mehrere Ausnahmen in einem einzelnen catch-Block abfangen können. Hier ist ein Beispiel für den catch-Block mit dieser Funktion:

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

Es gibt einige Einschränkungen, wie z.B. dass das Ausnahmeobjekt final ist und wir es nicht im catch-Block ändern können. Lesen Sie die vollständige Analyse unter Java 7 Catch Block Improvements.

Die meiste Zeit verwenden wir den finally-Block nur, um die Ressourcen zu schließen. Manchmal vergessen wir, sie zu schließen, und erhalten Laufzeit-Ausnahmen, wenn die Ressourcen erschöpft sind. Diese Ausnahmen sind schwer zu debuggen, und wir müssen möglicherweise jeden Ort überprüfen, an dem wir diese Ressource verwenden, um sicherzustellen, dass wir sie schließen. In Java 7 war eine der Verbesserungen try-with-resources, bei der wir eine Ressource im try-Statement erstellen können und sie innerhalb des try-catch-Blocks verwenden. Wenn die Ausführung den try-catch-Block verlässt, schließt die Laufzeitumgebung diese Ressourcen automatisch. Hier ist ein Beispiel für den try-catch-Block mit dieser Verbesserung:

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 stellt viele Ausnahmeklassen für uns bereit, aber manchmal müssen wir unsere eigenen benutzerdefinierten Ausnahmeklassen erstellen. Zum Beispiel, um den Aufrufer über einen bestimmten Typ von Ausnahme mit der entsprechenden Meldung zu informieren. Wir können benutzerdefinierte Felder für die Verfolgung haben, wie beispielsweise Fehlercodes. Angenommen, wir schreiben eine Methode, die nur Textdateien verarbeiten soll, dann können wir dem Aufrufer den entsprechenden Fehlercode mitteilen, wenn eine andere Art von Datei als Eingabe gesendet wird.

Zuerst erstellen Sie 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;
	}
}

Dann erstellen Sie ein 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");
			}
		}
	}
}

Wir können eine separate Methode haben, um verschiedene Arten von Fehlercodes zu verarbeiten, die wir von verschiedenen Methoden erhalten. Einige von ihnen werden verbraucht, weil wir den Benutzer möglicherweise nicht darüber informieren möchten, oder einige davon werfen wir zurück, um den Benutzer über das Problem zu informieren.

Hier erweitere ich die Exception, damit immer dann, wenn diese Ausnahme auftritt, sie in der Methode behandelt oder an das Aufrufprogramm zurückgegeben werden muss. Wenn wir RuntimeException erweitern, ist es nicht erforderlich, dies im throws-Klausel anzugeben.

Dies war eine Designentscheidung. Die Verwendung von überprüften Exceptions hat den Vorteil, Entwicklern dabei zu helfen, zu verstehen, welche Ausnahmen sie erwarten können, und angemessene Maßnahmen zu ergreifen, um sie zu behandeln.

Best Practices für die Behandlung von Ausnahmen in Java

  • Verwenden Sie spezifische Ausnahmen – Basisklassen der Ausnahme-Hierarchie bieten keine nützlichen Informationen, deshalb hat Java so viele Ausnahme-Klassen, wie z.B. IOException mit weiteren Unterklassen wie FileNotFoundException, EOFException usw. Wir sollten immer spezifische Ausnahmeklassen throw und catch verwenden, damit der Aufrufer die Ursache der Ausnahme leicht erkennen und verarbeiten kann. Dies erleichtert das Debuggen und hilft Clientanwendungen, Ausnahmen angemessen zu behandeln.
  • Frühzeitig werfen oder Fail-Fast – Wir sollten versuchen, Ausnahmen so früh wie möglich zu werfen. Betrachten wir die obige processFile()-Methode, wenn wir das null-Argument an diese Methode übergeben, erhalten wir die folgende Ausnahme:
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)

Während des Debuggens müssen wir den Stapelrückverfolgungsbericht sorgfältig überprüfen, um den tatsächlichen Ort der Ausnahme zu identifizieren. Wenn wir unsere Implementationslogik ändern, um frühzeitig auf diese Ausnahmen zu überprüfen, wie unten dargestellt:

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

	// ... weitere Verarbeitung
}

Dann wird der Ausnahme-Stapelrückverfolgungsbericht anzeigen, wo die Ausnahme mit einer klaren Meldung aufgetreten ist:

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)
  • Spät Fangen – Da Java darauf besteht, die überprüfte Ausnahme entweder zu behandeln oder sie in der Methodensignatur zu deklarieren, neigen Entwickler manchmal dazu, die Ausnahme zu „fangen“ und den Fehler zu protokollieren. Diese Praxis ist jedoch schädlich, da das aufrufende Programm keine Benachrichtigung über die Ausnahme erhält. Wir sollten Ausnahmen nur „fangen“, wenn wir sie angemessen behandeln können. Zum Beispiel werfe ich in der obigen Methode Ausnahmen zurück an die aufrufende Methode, um sie dort zu behandeln. Die gleiche Methode könnte von anderen Anwendungen verwendet werden, die die Ausnahme auf eine andere Weise verarbeiten möchten. Bei der Implementierung einer Funktion sollten wir immer Ausnahmen an den Aufrufer zurückgeben und ihnen die Entscheidung überlassen, wie sie damit umgehen sollen.
  • Ressourcen Schließen – Da Ausnahmen die Verarbeitung des Programms stoppen, sollten wir alle Ressourcen im finally-Block schließen oder die Java 7 try-with-resources-Verbesserung verwenden, um Java die Schließung für Sie durchführen zu lassen.
  • Protokollierung von Ausnahmen – Wir sollten immer Ausnahmemeldungen protokollieren und beim throwen von Ausnahmen eine klare Meldung bereitstellen, damit der Aufrufer leicht erkennen kann, warum die Ausnahme aufgetreten ist. Wir sollten immer einen leeren catch-Block vermeiden, der einfach die Ausnahme aufnimmt und keine sinnvollen Details zur Ausnahme für die Fehlersuche bereitstellt.
  • Einzelner catch-Block für mehrere Ausnahmen – In den meisten Fällen protokollieren wir Ausnahmedetails und geben eine Meldung an den Benutzer aus. In diesem Fall sollten wir die Java-7-Funktion verwenden, um mehrere Ausnahmen in einem einzigen catch-Block zu behandeln. Dieser Ansatz reduziert die Codegröße und sieht auch sauberer aus.
  • Verwendung von benutzerdefinierten Ausnahmen – Es ist immer besser, eine Ausnahmebehandlungsstrategie zur Entwurfszeit zu definieren und anstatt mehrere Ausnahmen zu throwen und zu catchen, können wir eine benutzerdefinierte Ausnahme mit einem Fehlercode erstellen, und das Aufruferprogramm kann diese Fehlercodes behandeln. Es ist auch eine gute Idee, eine Hilfsmethode zu erstellen, um verschiedene Fehlercodes zu verarbeiten und zu verwenden.
  • Namenskonventionen und Verpackung – Wenn Sie Ihre benutzerdefinierte Ausnahme erstellen, stellen Sie sicher, dass sie mit Exception endet, damit aus dem Namen selbst klar wird, dass es sich um eine Ausnahme handelt. Stellen Sie auch sicher, dass sie wie im Java Development Kit (JDK) verpackt sind. Zum Beispiel ist IOException die Basisklasse für alle IO-Operationen.
  • Verwenden Sie Ausnahmen mit Bedacht – Ausnahmen sind kostspielig, und manchmal ist es überhaupt nicht erforderlich, Ausnahmen zu werfen, und wir können stattdessen eine boolesche Variable an das Aufrufprogramm zurückgeben, um anzuzeigen, ob eine Operation erfolgreich war oder nicht. Dies ist hilfreich, wenn die Operation optional ist und Sie nicht möchten, dass Ihr Programm stecken bleibt, weil es fehlschlägt. Zum Beispiel möchten wir beim Aktualisieren der Aktienkurse in der Datenbank aus einem Webdienst eines Drittanbieters das Werfen von Ausnahmen vermeiden, wenn die Verbindung fehlschlägt.
  • Dokumentieren Sie die geworfenen Ausnahmen – Verwenden Sie Javadoc @throws, um die von der Methode geworfenen Ausnahmen klar zu spezifizieren. Dies ist sehr hilfreich, wenn Sie eine Schnittstelle für andere Anwendungen bereitstellen.

Fazit

In diesem Artikel haben Sie etwas über die Ausnahmebehandlung in Java gelernt. Sie haben über throw und throws gelernt. Sie haben auch über try (und try-with-resources), catch und finally-Blöcke gelernt.

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