Exceptions
1. Grundlagen
1.1. Programm-Ausführung, Stack, Heap
Ein Java-Programm startet die Ausführung (leicht vereinfacht) mit der Ausführung der Methode
public static void main(String[] args)
der beim Start anzugebenden Klasse (auf der Kommandozeile z.B. mit
java my.cool.pkg.MyMainClass
oder in der Eclipse-Run-Configuration).
Davor werden noch statische Variable initialisiert
(z.B. private static LocalTime currTime = LocalTime.now();
)
und statische Initializer ausgeführt
(z.B. static { System.out.println("Static Initializer running …");}
).
In der Main-Methode werden optional weitere statische Methoden aufgerufen, Objekte erzeugt und daran Instanz-Methoden aufgerufen. Dieser Prozess setzt sich fort und führt zu einer mitunter sehr tiefen Verschachtelung gerade laufender Methoden.
Die Aufrufparameter, lokale Variable (primitive und Referenzen auf Objekte) und weitere Informationen dieser gerade aktiven Methoden werden auf dem sogenannten Stack abgelegt. Im Gegensatz dazu liegen alle erzeugten Objekte auf dem sogenannten Heap (Details hierzu z.B. unter Stack-Speicher und Heap-Speicher in Java).
Bei Abschluss einer Methode wird diese vom Stack entfernt und die Ausführung läuft bei ihrer aufrufenden Methode weiter (der Rückgabewert ist dann für diese verfügbar). Nun könnte eine weitere Methode aufgerufen werden, die wiederum auf dem Stack landet.
So wächst und schwindet die Höhe des Stacks dynamisch bis zum Programmende, wo die Ausführung wieder in der Main-Methode landet, sofern nicht davor ein System.exit(0)
(oder eine andere Zahl, die eine grobe, vom Entwickler festzulegende Problemkategorisierung darstellen kann) aufgerufen worden ist.
1.2. Ergänzung mit Exception-Konzept
Da Java (wie die meisten Programmiersprachen) nur einen einzigen Rückgabewert erlaubt (der allerdings eine Referenz auf ein beliebig komplexes Objekt sein kann), ist auf dieser Basis eine angemessene Reaktionsmöglichkeit auf Fehler sehr beschränkt – oder zumindest nur mit großem Aufwand erreichbar.
Um das Auftreten von Fehlern sinnvoll handhaben zu können, gibt es daher ein eigenes, sehr elegantes Konzept – einen standardisierten, mächtigen "Fluchtweg" aus einer Fehlersituation – die Exceptions.
2. Praktische Umsetzung
2.1. Auslösen (Werfen) von Exceptions
Diese Exceptions werden quasi als mächtiger Ersatz für Fehlermeldungen im Code "geworfen". Dabei wird ein Exception-Objekt erzeugt und dieses erhält als Konstruktor-Parameter üblicherweise auch (neben optional weiteren Infos) einen Fehlertext (Message).
public class ExceptionUsageDemo {
public int calcSomething (int value) {
if (value == 1) {
throw new IllegalArgumentException("unzulässiger Wert 1 übergeben");
}
return 5/(value - 1); // würde bei value=1 unendlich
}
}
Eine Exception beendet die prozedurale Abfolge der Statements und verlässt die betroffene Methode (nach der Möglichkeit zu Aufräumarbeiten) unmittelbar und gibt ein neu erzeugtes Exception-Objekt mit Fehler-Infos an das aufrufende Statement (die Problemstelle in der aufrufenden Methode) zurück.
2.2. Checked und Unchecked Exceptions
Es gibt im wesentlichen 2 praxisrelevante Exception-Basisklassen: eine hat den Namen Exception (genannt "Checked Exception"), die andere ist eine Subklasse der anderen mit dem Namen RuntimeException (genannt "Unchecked Exception").
Die Klassenhierarchie der Exceptions ist: Throwable > Exception > RuntimeException
Eigene Exceptions sind wie oben erwähnt entweder von Exception (checked) oder RuntimeException (unchecked) abzuleiten.
Diese beiden unterscheiden sich – wie schon aus den "Spitznamen" erahnbar – in einer wichtigen Hinsicht fundamental voneinander: Sobald in einer Methode eine Checked Exception (Exception oder Subklassen davon – außer der RuntimeException-Subklasse!) geworfen werden kann, MUSS dies in der Methodensignatur vermerkt sein. Der Compiler erzwingt dann eine sinnvolle Weiterbehandlungsmöglichkeit im aufrufenden Code (alternativ ein Try-Catch der eigenen Exceptions ist nur in seltenen Ausnahmefällen sinnvoll).
import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
public class UsageCheckedExceptionDemo {
public void readFile() throws FileNotFoundException { // siehe 'throws ...'
File file = new File("NichtExistenteDatei.txt");
FileReader fr = new FileReader(file);
}
}
Unter der Annahme, dass die Datei NichtExistenteDatei.txt
tatsächlich nicht gefunden wird, wird die angekündigte FileNotFoundException
ausgelöst und ohne weitere Maßnahmen wird das Programm beendet und folgender Stacktrace auf STDERR ausgegeben:
java.io.FileNotFoundException: NichtExistenteDatei.txt (Datei oder Verzeichnis nicht gefunden) at java.base/java.io.FileInputStream.open0(Native Method) at java.base/java.io.FileInputStream.open(FileInputStream.java:212) at java.base/java.io.FileInputStream.<init>(FileInputStream.java:154) at java.base/java.io.FileReader.<init>(FileReader.java:75) at UsageCheckedExceptionDemo.readFile(UsageCheckedExceptionDemo.java:8)
Falls der Programmierer eine Unchecked Exception (RuntimeException oder Subklassen davon) verwendet, mus diese NICHT in der Signatur vermerkt werden. Damit kann eine solche Exception zur Laufzeit ohne sinnvolle Behandlung auftreten, falls sie nicht dennoch explizit behandelt wird.
public class UncheckedExceptionUsageDemo {
public int calcSomething (int value) {
if (value == 1) {
throw new IllegalArgumentException("unzulässiger Wert 1 übergeben");
}
return 5/(value - 1); // würde bei value=1 unendlich
}
}
Es wird mit new IllegalArgumentException(…)
ein Exception-Objekt erzeugt und dann mit throw
das "Werfen" ausgelöst.
Hier wird bei Aufruf der Methode mit calcSomething(1);
wie oben ohne weitere Maßnahmen das Programm beendet und folgender Stacktrace auf STDERR ausgegeben:
java.lang.IllegalArgumentException: unzulässiger Wert 1 übergeben at UncheckedExceptionUsageDemo.calcSomething(UncheckedExceptionUsageDemo.java:4)
Siehe z.B. java.lang.IllegalArgumentException (Java SE 17 & JDK 17) für Details.
2.3. Umgang mit auftretenden Exceptions
Das aufrufende Code-Abschnitt hat nun 2 Möglichkeiten, mit diesem Ereignis und dem damit verbundenen Exception-Objekt umzugehen:
-
es fühlt sich befähigt, die Fehlerursache sinnvoll zu behandeln – dann wird die Exception "gefangen" und im Falle des Auftretens entsprechender Code ausgeführt
-
es hat nicht ausreichend geeigneten Kontext um sinnvoll zu reagieren – dann wird die Exception an die in der Aufruf-Hierarchie nächsthöhere Methode weitergereicht (hat die gleiche Wirkung wie eine selbst geworfene Exception).
Beispielsweise kann eine fehlerhafte Benutzer-Eingabe nur an einer Stelle behandelt werden, wo die Möglichkeit zur Fehlermeldung und Anbieten einer Eingabekorrektur durch den Benutzer gegeben ist.
Als "Faustformel" kann dienen, dass der Code, in dem der den Fehler seine Ursache hat (falsche Eingabe, etc.), auch der praktikabelste Platz zur Fehlerbehandlung ist.
Beispiel:
import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
public class ExceptionHandlingDemo {
public void readFile() { // nun kein 'throws ...', da Exception selbst behandelt
File file = new File("MichGibtsNicht.txt");
//File file = new File("ExceptionHandlingDemo.class"); // die gibt es bei Ausführen in BlueJ
try {
FileReader fr = new FileReader(file);
System.out.println("Datei '" + file.getName() + "' gelesen");
} catch (FileNotFoundException ex) {
System.out.println("Uuups - " + ex.getMessage());
} finally { // optional - wird in JEDEM Fall anschließend (zum "Aufräumen" etc.) ausgeführt
System.out.println("alles unter Kontrolle");
}
}
}
Wenn Datei MichGibtsNicht.txt
verwendet wird, entsteht folgende Ausgabe komplett in SDTOUT:
Uuups - MichGibtsNicht.txt (Datei oder Verzeichnis nicht gefunden) alles unter Kontrolle
Wenn Datei ExceptionHandlingDemo.class
aktiviert wird (bei Ausführung z.B. in BlueJ existiert diese im aktuellen Verzeichnis), entsteht folgende Ausgabe komplett in STDOUT.
Datei 'ExceptionHandlingDemo.class' gelesen alles unter Kontrolle
Offensichtlich wird alles unter Kontrolle
in beiden Fällen ausgegeben. Der finally
-Block kann entfallen, wenn nicht benötigt (wird meistens weggelassen):
2.4. Erzeugen eigener Exceptions
2.4.1. Wozu eigene Exceptions
Der Sinn besteht darin, dass man damit gezielt nur auf diejenigen Exceptions reagieren kann, für die man "zuständig" ist.
2.4.2. Praktische Durchführung
Erfolgt einfach durch Erben: … extends Exception
. Alle besseren IDEs bieten die Möglichkeit, Konstruktoren von der Superklasse "abzuleiten", d.h. eigene mit selben Parameterlisten zu erzeugen.
So ist mit wenigen Klicks eine gültige und meist ausreichende eigene Exception zu erstellen.
Sehr oft löscht man den Großteil der automatisch erstellten Konstruktoren und behält nur die nachstehend gezeigten.
package my.demo.exceptions;
public class MyBadParamException extends Exception {
public MyBadParamException(String message) { // meist nur diese 2 Konstruktoren nötig
super(message);
// Enthält zwingend den Aufruf des passenden Superklassen-Konstruktors, sonst nichts
}
public MyBadParamException(String message, Throwable cause) {
super(message, cause); // 'cause': Exc-Obj. d. "tieferen Ursache"
}
}
2.4.3. Komplexere eigene Exceptions
In manchen Situationen ist es sinnvoll, mehr Kontext-Information mit dem Exception-Objekt "mitzuschicken", sodass der Reagierende alle nötigen Fakten für eine sinnvolle Reaktion verfügbar hat.
Man kann einfach beliebige Instanzvariablen und auch Methoden (v.a. Getter) definieren und die Konstruktoren erweitern, sodass die Werte bei der Erzeugung mitgegeben werden.
Auch Setter und weitere Methoden sind denkbar, um erst das Exception-Objekt mit
MySmartException ex = new MySmartException(…);
zu erzeugen (dabei passiert noch nichts!),
dann alle gewünschten Methoden auszuführen und es erst danach dem
throw ex;
die Exception auszulösen.
2.5. Stacktrace — Bedeutung und Nutzung
Bei unbehandelten Exceptions wird vom Laufzeitsystem vor der Beendigung des Programms eine Behandlung durchgeführt, und zwar in Form eines Stacktrace. Dieser gibt neben der Exception Message wichtige Einsichten in die Entstehung.
Wir sehen uns nun nochmals einen oben erwähnten Stacktrace (mit Package my.demo
für die Klasse) an:
java.io.FileNotFoundException: NichtExistenteDatei.txt (Datei oder Verzeichnis nicht gefunden) (1) at java.base/java.io.FileInputStream.open0(Native Method) (2) at java.base/java.io.FileInputStream.open(FileInputStream.java:212) (3) at java.base/java.io.FileInputStream.<init>(FileInputStream.java:154) (4) at java.base/java.io.FileReader.<init>(FileReader.java:75) (5) at my.demo.UsageCheckedExceptionDemo.readFile(UsageCheckedExceptionDemo.java:8) (6)
1 | Angabe, welche Exception, dazu die Message (hier Dateiname + Problemart) |
2 | Aufruf der Betriebssystemfunktion zum Datei-Öffnen (Native Method, mit der C-Funktionen des Betriebssystems angesprochen werden können) |
3 | die "tiefste" echte Java-Methode - hier sieht man den Namen der Source-Datei und sogar die Zeilennummer des JDK-Sourcecode. In Eclipse sind die Dateien als Link bereitgestellt und die JDK-Source-Datei kann bei korrekter Eclipse-Konfiguration direkt an der richtigen Stelle geöffnet werden! |
4 | mit .<init> ist jeweils ein Konstruktor gemeint (FileInputStream ist die Basis) |
5 | FileReader bietet elegantere Möglichkeiten zum text-orientierten Dateizugriff |
6 | hier die erste (und hier auch finale) "eigene" Klasse und Methode, wo in Eclipse der Link unbedingt angeklickt werden sollte, um direkt zur selbstverursachten Problemstelle zu gelangen. Bei vernünftiger Package-Verwendung findet man die eigenen Klassen sehr rasch, da immer der voll qualifizierte Klassenname angezeigt wird (hier my.demo.UsageCheckedExceptionDemo , dahinter die Methode '.readFile', dahinter in Klammer die Source-Datei samt Zeilennummer)! |
Als Faustformel ergibt sich also, im Stacktrace die erste eigene Klasse (von oben nach unten) zu suchen. Dort ist das Problem fast immer lokalisiert.
3. Links
https://www.baeldung.com/java-get-current-stack-trace [Get the Current Stack Trace in Java | Baeldung] … 2023-03-05
Unchecked Exceptions — The Controversy - Dev.java … 2023-11-28
Don’t Use Checked Exceptions … 2024-02-22
Functional error handling with Java 17 … 2024-02-22