- Ein Blick auf finally und Exception Handling, insbesondere das richtige Freigeben von Resourcen.
Es gibt leider ein wirklich ärgerliches Idiom/Pattern/Snippet, das man immer wieder in Java Code sieht:
SomeResult readSomething () {
InputStream in = null;
SomeResult result = null;
try {
in = new InputStream(....);
// read from in in some way or other,
// build a result
result = new SomeResult(...);
} catch (IOException e) {
// log e in some way
result = null;
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
// (log e in some way)
}
}
return result;
}
(Analog mit allen möglichen anderen Arten von Resourcen, OutputStream
, Connection
, ResultSet
, Reader
, Writer
etc.)
Der obige Code ist noch nicht falsch per se, aber problematisch aus einer Reihe von Gründen:
- Der Code ist unnötig verbose
- Im Fehlerfall wird einfach null zurückgeben (i.d.R. wird bei solchem Code der Caller auch nicht auf null prüfen: hat ja beim Testen immer geklappt)
- Der Code ist anfällig im Falle von Änderungen und Erweiterungen
- Kein einziger Wert ist final
Richtig gefährlich wird es, wenn man noch eine zweite Resource hinzufügt, die geclosed werden muss, denn nur zu oft führt es zu Code wie diesem:
InputStream in = null;
// we just assume there is data from
// a 2nd stream, but it could also be
// a combination of ResultSets,
// Connections, input and output streams...
InputStream in2 = null;
SomeResult result = null;
try {
in = new InputStream(....);
in2 = new InputStream(....);
// read from streams in some way or other,
// build a result
result = new SomeResult(...);
} catch (IOException e) {
// (log e in some way)
// DERP!
result = null;
} finally {
try {
if (in != null) {
in.close();
}
// notice what's wrong here?
if (in2 != null) {
in2.close();
}
} catch (IOException e) {
//log e in some way
}
}
return result;
}
Wenn es ein Problem mit dem ersten Stream gibt, ist es sehr gut möglich, dass im Fehlerfall in.close()
fehl schlägt. Nun wird in2
nicht mehr geschlossen und wir haben ein Resource-Leak.
Wird readSomething
häufiger aufgerufen und das Problem besteht weiter, kann dies zum Crash der App und schlimmstenfalls zu einer Instabilität des gesamten Systems führen. Bei Netzwerkverbindungen kann es z.B. so dazu kommen, dass das System keine weiteren Netzwerkverbindungen mehr aufbauen kann, weil die maximale Anzahl an Verbindungen geöffnet ist und nicht sauber geschlossen wurde.
Nicht immer kann man sich darauf verlassen, dass ein Beenden/Crash der Applikation wirklich alle Resourcen freigibt – und selbst wenn: eine crashende App oder eine App die keine Sockets mehr öffnen kann ist in den meisten Fällen schlimm genug.
finally richtig eingesetzt
Ändert man den Code nur ein wenig ab, ist er nicht nur besser lesbar, es kann auch nichts mehr schief gehen:
SomeResult readSomething () throws IOException {
final InputStream in = new InputStream(...);
try {
final InputStream in2 = new InputStream(...);
try { //read from streams in some way or other,
// build & immediately return a result
return new SomeResult(...);
} finally {
in2.close();
}
} finally {
in.close();
}
}
Wir erzeugen den InputStream
sofort bei der Deklaration. Deshalb kann er nicht null
sein, also entfallen die lästigen null
-Checks beim schließen (und NullPointerExceptions
, wenn man den Check weg lässt).
Schlägt das Erzeugen des Streams bei der Erzeugung fehl, gibt es nichts zu closen
; (schlägt ein Konstruktor fehl, haben wir in Java nur null
als Resultat, nicht ein „halb erzeugtes“ Objekt wie in C++), der finally
-Block wird aber gar nicht erreicht, weil der zugehörige try
-Block gar nicht erst betreten wird.
Jede Resource wird, falls sie geöffnet wird, in einem eigenen finally
-Block garantiert geschlossen – es kommt also auch nicht zu Resource Leaks, alles wird sauber & korrekt freigegeben. Wir kümmern uns nicht ums Exception Handling. Der Caller muss entscheiden was passiert, wenn die Aktion schief geht. Der Code sorgt nur dafür, dass im Fehlerfall alle Resourcen sauber freigegeben werden.
Eine Alternative für den Fall, dass es einen sinnvollen default Wert gibt, folgt weiter unten. Wir geben das Resultat auch nicht erst am Ende der Funktion zurück. Wer returnen sofort, wenn wir unser Ergebnis haben. Die finally
-Blocks werden trotzdem garantiert ausgeführt. Wir tun quasi im innersten Block so, als könnte nichts schiefgehen und es gäbe gar kein Resource-Handing und haben so wesentlich leichteren zu lesenden und schreibenden Code.
Ein wenig Whitespace-Ketzerei
Für den Spezialfall „Ressourcen mit finally
freigeben“ kann man sogar so etwas ketzerisches machen wie nicht nach einem try
einzurücken. Für diesen ganz speziellen Fall kann es die Lesbarkeit IMHO deutlich erhöhen:
SomeResult readSomething () throws IOException {
//CREATE RESOURCES
final InputStream in = new InputStream(...);
try {
final InputStream in2 = new InputStream(...);
try { //read from streams in some way or other,
// build a result
return new SomeResult(...);
// CLOSE RESOURCES
} finally {
in2.close();
}
} finally {
in.close();
}
}
Variante: Exceptions nicht weitergeben
Möchte man das Error Handling doch in der Funktion durchführen, z.B. weil man weiß, dass es einen sinnvollen default Wert gibt, kann man den Code wesentlich angenehmer so aufbauen:
SomeResult readSomething () {
try {
//CREATE RESOURCES
final InputStream in = new InputStream(...);
try {
final InputStream in2 = new InputStream(...);
try {
//read from streams in some way or other,
// build a result
return new SomeResult(...);
//CLOSE RESOURCES
} finally {
in2.close();
}
} finally {
in.close();
}
} catch (final IOException e) {
//log in some way, perform other
// error handling return
SomeResult.DEFAULT;
}
}
Man trennt also Error-Handling (try - catch
) vom Resource Handling (try - finally
).
Überlegungen beim Error Handling:
Wenn irgendwo Exceptions fliegen können, insbesondere IOException
s oder JDBCExceptions
s als Resultat eines Versuchs irgendwo Daten her zu bekommen, sollte man sich immer bewusst Gedanken darüber machen, wie man diese Exceptions behandeln will. Im Grunde hat man nur zwei Möglichkeiten:
- Sauberen Defaultwert zurückgeben, evtl. noch loggen
- Einfach die Exception weiterreichen, ggfs. gewrappt
Die Frage ist: Was heißt es im Kontext der Software, wenn die Exception fliegt? Heißt es das etwas gewaltig im Argen ist und es keinen Sinn macht weiter zu arbeiten (z.B.: Datenbankfehler auf der Serverseite: wenn die DB weg ist oder das Statement einen Fehler hat, können wir gleich dicht machen)? Dann wrappt man die Exception am besten in eine RuntimeException
(oder besser: in eine Subklasse davon) und tut (fast) überall so, als gebe es den Fehler nicht. Man muss nun nur dafür sorgen, dass es weit oben in der Call-Hierarchy einen Exception-Handler für die RuntimeException
gibt, der dafür sorgt, dass sich das System elegant und mit Würde (z.B. zumindest ein Hinweis an den User & logging) verabschiedet („graceful“).
Oder wissen wir es nicht? Vielleicht weil der Code in verschiedenen Kontexten aufgerufen wird, die wir ggfs. gar nicht kennen? In diesem Fall sollte man auch einfach die Exception werfen und dem Caller die Entscheidung überlassen. Hier sollte man eher nicht wrappen, um dem Caller die volle Kontrolle darüber zu lassen, wie er mit der Exception umgehen möchte.
Ansonsten sollte man sich gute Gedanken machen, welchen default man zurück gibt. null
sollte hier die absolut letzte Wahl sein.
Beispiele in einer Android-App:
- Daten vom Server laden: geht oft schief, sollte in den normalen Workflow eingearbeitet werden. Vorgehen: Exception werfen aber direkt fangen, Message an den User, Abbruch, weiter im normalen Workflow.
- Manuell raw assets über einen Stream laden: Die Assets müssen korrekt im APK vorliegen und sind für den normalen Betrieb der App unbedingt notwendig. Man kann außerdem davon ausgehen, dass lokale Resourcen lesbar sind. Also: Let it Crash! Einfach die Exception in eine
RuntimeException
wrappen und werfen, am besten mit einer eindeutigen Fehlermessage. Dann einen globalen Error-Handler benutzen, der noch eine Nachricht an den User ausgibt (damit man sinnvolles Feedback bekommt). - Eine Configuration soll eingelesen werden (aus welcher Quelle auch immer) und ist nicht vorhanden: Einfach eine Default-Config zurück geben.
Und was ist mit Java 7?
In Java 7 sollte man natürlich nicht mehr finally
nutzen, sondern die neue „Try-With-Resources“ Syntax (die aber praktisch nur Syntactic Sugar für das obige Vorgehen ist):
SomeResult readSomething () throws IOException {
try (final InputStream in = new InputStream(...);
final InputStream in2 = new InputStream(...)){
//read from streams in some way or other,
// build a result
return new SomeResult(...);
}
}
Allerdings wird es noch eine ganze Weile dauern, bis wir Java 7 unter Android nutzen können (von einigen Hacks einmal abgesehen), so dass man als Android Entwickler noch eine ganze Weile auf finally
angewiesen sein wird.