Ich denke, Sie haben wahrscheinlich schon einmal eine Situation erlebt, in der Sie Code ausführen und am Ende so etwas wie eine NullPointerException , eine ClassCastException oder Schlimmeres erhalten ... Darauf folgt ein langer Prozess des Debuggens, Analysierens, Googelns usw. Ausnahmen sind so wie sie sind wunderbar: Sie zeigen die Art des Problems und den Ort an, an dem es aufgetreten ist. Wenn Sie Ihr Gedächtnis auffrischen und etwas mehr erfahren möchten, schauen Sie sich diesen Artikel an: Ausnahmen: aktiviert, deaktiviert und benutzerdefiniert .
Allerdings kann es Situationen geben, in denen Sie eine eigene Ausnahme erstellen müssen. Angenommen, Ihr Code muss Informationen von einem Remotedienst anfordern, der aus irgendeinem Grund nicht verfügbar ist. Oder nehmen wir an, jemand füllt einen Antrag für eine Bankkarte aus und gibt eine Telefonnummer an, die – ob zufällig oder nicht – bereits mit einem anderen Benutzer im System verknüpft ist.
Natürlich hängt das richtige Verhalten hier immer noch von den Anforderungen des Kunden und der Architektur des Systems ab, aber gehen wir davon aus, dass Sie damit beauftragt wurden, zu prüfen, ob die Telefonnummer bereits verwendet wird, und gegebenenfalls eine Ausnahme auszulösen.
Erstellen wir eine Ausnahme:
public class PhoneNumberAlreadyExistsException extends Exception {
public PhoneNumberAlreadyExistsException(String message) {
super(message);
}
}
Als nächstes verwenden wir es, wenn wir unsere Prüfung durchführen:
public class PhoneNumberRegisterService {
List<String> registeredPhoneNumbers = Arrays.asList("+1-111-111-11-11", "+1-111-111-11-12", "+1-111-111-11-13", "+1-111-111-11-14");
public void validatePhone(String phoneNumber) throws PhoneNumberAlreadyExistsException {
if (registeredPhoneNumbers.contains(phoneNumber)) {
throw new PhoneNumberAlreadyExistsException("The specified phone number is already in use by another customer!");
}
}
}
Um unser Beispiel zu vereinfachen, verwenden wir mehrere fest codierte Telefonnummern zur Darstellung einer Datenbank. Und zum Schluss versuchen wir, unsere Ausnahme zu nutzen:
public class CreditCardIssue {
public static void main(String[] args) {
PhoneNumberRegisterService service = new PhoneNumberRegisterService();
try {
service.validatePhone("+1-111-111-11-14");
} catch (PhoneNumberAlreadyExistsException e) {
// Here we can write to logs or display the call stack
e.printStackTrace();
}
}
}
Und jetzt ist es an der Zeit, Umschalt+F10 zu drücken (wenn Sie IDEA verwenden), also das Projekt auszuführen. Folgendes sehen Sie in der Konsole:
Ausnahme.PhoneNumberAlreadyExistsException: Die angegebene Telefonnummer wird bereits von einem anderen Kunden verwendet!
bei Ausnahme.PhoneNumberRegisterService.validatePhone(PhoneNumberRegisterService.java:11)
Sieh dich an! Sie haben Ihre eigene Ausnahme erstellt und sie sogar ein wenig getestet. Herzlichen Glückwunsch zu diesem Erfolg! Ich empfehle, ein wenig mit dem Code zu experimentieren, um besser zu verstehen, wie er funktioniert.
Fügen Sie eine weitere Prüfung hinzu – prüfen Sie beispielsweise, ob die Telefonnummer Buchstaben enthält. Wie Sie wahrscheinlich wissen, werden in den USA häufig Buchstaben verwendet, um Telefonnummern leichter zu merken, z. B. 1-800-MY-APPLE. Durch Ihre Prüfung könnte sichergestellt werden, dass die Telefonnummer nur Ziffern enthält.
Okay, wir haben eine geprüfte Ausnahme erstellt. Alles wäre schön und gut, aber...
Die Programmiergemeinschaft ist in zwei Lager gespalten – diejenigen, die kontrollierte Ausnahmen befürworten, und diejenigen, die sie ablehnen. Beide Seiten bringen starke Argumente vor. Zu beiden gehören erstklassige Entwickler: Bruce Eckel kritisiert geprüfte Ausnahmen, während James Gosling sie verteidigt. Es sieht so aus, als ob diese Angelegenheit nie dauerhaft geklärt werden wird. Schauen wir uns jedoch die Hauptnachteile der Verwendung geprüfter Ausnahmen an.
Der Hauptnachteil geprüfter Ausnahmen besteht darin, dass sie behandelt werden müssen. Und hier haben wir zwei Möglichkeiten: Entweder an Ort und Stelle mit einem Try-Catch umgehen oder, wenn wir dieselbe Ausnahme an vielen Stellen verwenden, Throws verwenden , um die Ausnahmen auszulösen und sie in Klassen der obersten Ebene zu verarbeiten.
Außerdem kann es sein, dass wir am Ende einen „Boilerplate“-Code haben, also Code, der viel Platz einnimmt, aber nicht viel Arbeit leistet.
In relativ großen Anwendungen treten Probleme auf, wenn viele Ausnahmen behandelt werden: Die Throws- Liste einer Methode der obersten Ebene kann leicht auf ein Dutzend Ausnahmen anwachsen.
Entwicklern gefällt das normalerweise nicht und sie entscheiden sich stattdessen für einen Trick: Sie sorgen dafür, dass alle ihre geprüften Ausnahmen einen gemeinsamen Vorfahren erben – ApplicationNameException . Jetzt müssen sie auch diese ( geprüfte !) Ausnahme in einem Handler abfangen:
catch (FirstException e) {
// TODO
}
catch (SecondException e) {
// TODO
}
catch (ThirdException e) {
// TODO
}
catch (ApplicationNameException e) {
// TODO
}
Hier stehen wir vor einem weiteren Problem: Was sollen wir im letzten Catch- Block tun? Oben haben wir bereits alle erwarteten Situationen verarbeitet, daher bedeutet ApplicationNameException an dieser Stelle für uns nichts anderes als „ Ausnahme : Es ist ein unverständlicher Fehler aufgetreten“. So gehen wir damit um:
catch (ApplicationNameException e) {
LOGGER.error("Unknown error", e.getMessage());
}
Und am Ende wissen wir nicht, was passiert ist.
Aber könnten wir nicht jede Ausnahme auf einmal auslösen, so?
public void ourCoolMethod() throws Exception {
// Do some work
}
Ja wir könnten. Aber was sagt uns „wirft eine Ausnahme“? Dass etwas kaputt ist. Sie müssen alles von oben bis unten untersuchen und sich lange mit dem Debugger vertraut machen, um den Grund zu verstehen.
Möglicherweise stoßen Sie auch auf ein Konstrukt, das manchmal als „Ausnahmeschlucken“ bezeichnet wird:
try {
// Some code
} catch(Exception e) {
throw new ApplicationNameException("Error");
}
Zur Erklärung gibt es hier nicht viel hinzuzufügen – der Code macht alles klar, oder besser gesagt, er macht alles unklar.
Natürlich können Sie sagen, dass Sie dies im echten Code nicht sehen werden. Schauen wir uns nun die Eingeweide (den Code) der URL- Klasse aus dem java.net- Paket an. Folge mir, wenn du es wissen willst!
Hier ist eines der Konstrukte in der URL- Klasse:
public URL(String spec) throws MalformedURLException {
this(null, spec);
}
Wie Sie sehen, haben wir eine interessante geprüfte Ausnahme – MalformedURLException . Hier kann es ausgelöst werden (und ich zitiere):
„Wenn kein Protokoll angegeben ist oder ein unbekanntes Protokoll gefunden wird oder die Spezifikation null ist oder die analysierte URL nicht der spezifischen Syntax des zugehörigen Protokolls entspricht.“
Das ist:
- Wenn kein Protokoll angegeben ist.
- Es wurde ein unbekanntes Protokoll gefunden.
- Die Spezifikation ist null .
- Die URL entspricht nicht der spezifischen Syntax des zugehörigen Protokolls.
Erstellen wir eine Methode, die ein URL- Objekt erstellt:
public URL createURL() {
URL url = new URL("https://codegym.cc");
return url;
}
Sobald Sie diese Zeilen in der IDE schreiben (ich programmiere in IDEA, aber das funktioniert sogar in Eclipse und NetBeans), werden Sie Folgendes sehen:
Das bedeutet, dass wir entweder eine Ausnahme auslösen oder den Code in einen Try-Catch- Block einschließen müssen. Im Moment schlage ich vor, die zweite Option zu wählen, um zu visualisieren, was passiert:
public static URL createURL() {
URL url = null;
try {
url = new URL("https://codegym.cc");
} catch(MalformedURLException e) {
e.printStackTrace();
}
return url;
}
Wie Sie sehen, ist der Code bereits ziemlich ausführlich. Und darauf haben wir oben bereits hingewiesen. Dies ist einer der offensichtlichsten Gründe für die Verwendung ungeprüfter Ausnahmen.
Wir können eine ungeprüfte Ausnahme erstellen, indem wir RuntimeException in Java erweitern.
Ungeprüfte Ausnahmen werden von der Error- Klasse oder der RuntimeException- Klasse geerbt . Viele Programmierer sind der Meinung, dass diese Ausnahmen in unseren Programmen nicht behandelt werden können, da sie Fehler darstellen, von denen wir nicht erwarten können, dass sie behoben werden, während das Programm ausgeführt wird.
Wenn eine ungeprüfte Ausnahme auftritt, wird sie normalerweise dadurch verursacht, dass der Code falsch verwendet wird und ein Argument übergeben wird, das null oder anderweitig ungültig ist.
Nun, schreiben wir den Code:
public class OurCoolUncheckedException extends RuntimeException {
public OurCoolUncheckedException(String message) {
super(message);
}
public OurCoolUncheckedException(Throwable cause) {
super(cause);
}
public OurCoolUncheckedException(String message, Throwable throwable) {
super(message, throwable);
}
}
Beachten Sie, dass wir mehrere Konstruktoren für unterschiedliche Zwecke erstellt haben. Dadurch können wir unserer Ausnahme mehr Fähigkeiten verleihen. Beispielsweise können wir dafür sorgen, dass eine Ausnahme uns einen Fehlercode liefert. Lassen Sie uns zunächst eine Aufzählung erstellen , um unsere Fehlercodes darzustellen:
public enum ErrorCodes {
FIRST_ERROR(1),
SECOND_ERROR(2),
THIRD_ERROR(3);
private int code;
ErrorCodes(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
Nun fügen wir unserer Ausnahmeklasse einen weiteren Konstruktor hinzu:
public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
super(message, cause);
this.errorCode = errorCode.getCode();
}
Und vergessen wir nicht, ein Feld hinzuzufügen (wir hätten es fast vergessen):
private Integer errorCode;
Und natürlich eine Methode, um diesen Code zu erhalten:
public Integer getErrorCode() {
return errorCode;
}
Schauen wir uns die gesamte Klasse an, damit wir sie überprüfen und vergleichen können:
public class OurCoolUncheckedException extends RuntimeException {
private Integer errorCode;
public OurCoolUncheckedException(String message) {
super(message);
}
public OurCoolUncheckedException(Throwable cause) {
super(cause);
}
public OurCoolUncheckedException(String message, Throwable throwable) {
super(message, throwable);
}
public OurCoolUncheckedException(String message, Throwable cause, ErrorCodes errorCode) {
super(message, cause);
this.errorCode = errorCode.getCode();
}
public Integer getErrorCode() {
return errorCode;
}
}
Ta-da! Unsere Ausnahme ist erledigt! Wie Sie sehen, gibt es hier nichts besonders Kompliziertes. Schauen wir es uns in Aktion an:
public static void main(String[] args) {
getException();
}
public static void getException() {
throw new OurCoolUncheckedException("Our cool exception!");
}
Wenn wir unsere kleine Anwendung ausführen, sehen wir in der Konsole etwa Folgendes:
Nutzen wir nun die zusätzlichen Funktionen, die wir hinzugefügt haben. Wir werden dem vorherigen Code etwas hinzufügen:
public static void main(String[] args) throws Exception {
OurCoolUncheckedException exception = getException(3);
System.out.println("getException().getErrorCode() = " + exception.getErrorCode());
throw exception;
}
public static OurCoolUncheckedException getException(int errorCode) {
return switch (errorCode) {
case 1:
return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.FIRST_ERROR.getCode(), new Throwable(), ErrorCodes.FIRST_ERROR);
case 2:
return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.SECOND_ERROR.getCode(), new Throwable(), ErrorCodes.SECOND_ERROR);
default: // Since this is the default action, here we catch the third and any other codes that we have not yet added. You can learn more by reading Java switch statement
return new OurCoolUncheckedException("Our cool exception! An error occurred: " + ErrorCodes.THIRD_ERROR.getCode(), new Throwable(), ErrorCodes.THIRD_ERROR);
}
}
Sie können mit Ausnahmen auf die gleiche Weise arbeiten wie mit Objekten. Natürlich wissen Sie sicher bereits, dass in Java alles ein Objekt ist.
Und schauen Sie sich an, was wir getan haben. Zuerst haben wir die Methode geändert, die nun nicht mehr auslöst, sondern einfach eine Ausnahme erzeugt, abhängig vom Eingabeparameter. Als nächstes generieren wir mithilfe einer switch-case -Anweisung eine Ausnahme mit dem gewünschten Fehlercode und der gewünschten Fehlermeldung. Und in der Hauptmethode rufen wir die erstellte Ausnahme ab, rufen den Fehlercode ab und lösen ihn aus.
Lassen Sie uns Folgendes ausführen und sehen, was wir auf der Konsole erhalten:
Schauen Sie – wir haben den Fehlercode gedruckt, den wir von der Ausnahme erhalten haben, und dann die Ausnahme selbst ausgelöst. Darüber hinaus können wir sogar genau verfolgen, wo die Ausnahme ausgelöst wurde. Bei Bedarf können Sie der Meldung alle relevanten Informationen hinzufügen, zusätzliche Fehlercodes erstellen und Ihren Ausnahmen neue Funktionen hinzufügen.
Na, was haltet Ihr davon? Ich hoffe, dass bei dir alles geklappt hat!
Im Allgemeinen handelt es sich bei Ausnahmen um ein recht umfangreiches und nicht eindeutig zu klärendes Thema. Es wird noch viele weitere Streitigkeiten darüber geben. Beispielsweise hat nur Java geprüfte Ausnahmen. Unter den beliebtesten Sprachen habe ich keine gesehen, die sie verwendet.
Bruce Eckel hat in Kapitel 12 seines Buches „Thinking in Java“ sehr gut über Ausnahmen geschrieben – ich empfehle Ihnen, es zu lesen! Schauen Sie sich auch den ersten Band von Horstmanns „Core Java“ an – auch Kapitel 7 enthält viel Interessantes.
Eine kleine Zusammenfassung
-
Schreiben Sie alles in ein Protokoll! Protokollieren Sie Nachrichten in ausgelösten Ausnahmen. Dies hilft normalerweise beim Debuggen sehr und ermöglicht es Ihnen zu verstehen, was passiert ist. Lassen Sie einen Catch- Block nicht leer, da er sonst die Ausnahme einfach „verschluckt“ und Sie keine Informationen haben, die Ihnen beim Aufspüren von Problemen helfen könnten.
-
Wenn es um Ausnahmen geht, ist es eine schlechte Praxis, sie alle auf einmal abzufangen (wie ein Kollege von mir sagte: „Es ist kein Pokémon, es ist Java“), also vermeiden Sie das Abfangen (Ausnahme e) oder, schlimmer noch, das Abfangen ( Throwable t ) .
-
Lösen Sie Ausnahmen so früh wie möglich aus. Dies ist eine gute Java-Programmierpraxis. Wenn Sie Frameworks wie Spring studieren, werden Sie feststellen, dass sie dem „Fail-Fast“-Prinzip folgen. Das heißt, sie „versagen“ so früh wie möglich, um eine schnelle Fehlersuche zu ermöglichen. Dies bringt natürlich gewisse Unannehmlichkeiten mit sich. Dieser Ansatz trägt jedoch dazu bei, robusteren Code zu erstellen.
-
Wenn Sie andere Teile des Codes aufrufen, ist es am besten, bestimmte Ausnahmen abzufangen. Wenn der aufgerufene Code mehrere Ausnahmen auslöst, ist es schlechte Programmierpraxis, nur die übergeordnete Klasse dieser Ausnahmen abzufangen. Angenommen, Sie rufen Code auf, der FileNotFoundException und IOException auslöst . In Ihrem Code, der dieses Modul aufruft, ist es besser, zwei Catch-Blöcke zu schreiben, um jede der Ausnahmen abzufangen, als einen einzelnen Catch , um Exception abzufangen .
-
Fangen Sie Ausnahmen nur dann ab, wenn Sie sie für Benutzer und zum Debuggen effektiv behandeln können.
-
Zögern Sie nicht, Ihre eigenen Ausnahmen zu schreiben. Natürlich gibt es in Java viele vorgefertigte, für jeden Anlass etwas, aber manchmal muss man trotzdem sein eigenes „Rad“ erfinden. Sie sollten jedoch genau verstehen, warum Sie dies tun, und sicherstellen, dass der Standardsatz an Ausnahmen nicht bereits das enthält, was Sie benötigen.
-
Achten Sie beim Erstellen eigener Ausnahmeklassen auf die Benennung! Sie wissen wahrscheinlich bereits, dass es äußerst wichtig ist, Klassen, Variablen, Methoden und Pakete richtig zu benennen. Ausnahmen sind keine Ausnahmen! :) Beenden Sie immer mit dem Wort Exception , und der Name der Ausnahme sollte die Art des Fehlers, den sie darstellt, klar zum Ausdruck bringen. Zum Beispiel FileNotFoundException .
-
Dokumentieren Sie Ihre Ausnahmen. Wir empfehlen, für Ausnahmen ein @throws-Javadoc-Tag zu schreiben. Dies ist besonders nützlich, wenn Ihr Code Schnittstellen jeglicher Art bereitstellt. Und es wird Ihnen später auch leichter fallen, Ihren eigenen Code zu verstehen. Was denken Sie, wie können Sie feststellen, worum es bei MalformedURLException geht? Von Javadoc! Ja, der Gedanke, Dokumentation zu schreiben, ist nicht sehr verlockend, aber glauben Sie mir, Sie werden es sich danken, wenn Sie sechs Monate später zu Ihrem eigenen Code zurückkehren.
-
Geben Sie Ressourcen frei und vernachlässigen Sie nicht das Try-with-Resources- Konstrukt.
-
Hier ist die Gesamtzusammenfassung: Ausnahmen mit Bedacht nutzen. Das Auslösen einer Ausnahme ist im Hinblick auf die Ressourcen ein ziemlich „teurer“ Vorgang. In vielen Fällen kann es einfacher sein, das Auslösen von Ausnahmen zu vermeiden und stattdessen beispielsweise eine boolesche Variable zurückzugeben, die angibt, ob die Operation erfolgreich war, indem man ein einfaches und „kostengünstigeres“ if-else verwendet .
Es kann auch verlockend sein, Anwendungslogik an Ausnahmen zu binden, was Sie eindeutig nicht tun sollten. Wie wir am Anfang des Artikels sagten, gelten Ausnahmen für Ausnahmesituationen, die nicht erwartet werden, und es gibt verschiedene Tools, um sie zu verhindern. Insbesondere gibt es Optional , um eine NullPointerException zu verhindern , oder Scanner.hasNext und dergleichen, um eine IOException zu verhindern , die die Methode read() auslösen kann.
GO TO FULL VERSION