Java: The try-return-finally thing

Facebooktwittergoogle_pluspinterestlinkedinmailFacebooktwittergoogle_pluspinterestlinkedinmail

Es gibt in der Software-Entwicklung ungeschriebene, weil unschöne, Regeln. Dazu zählt unter anderem die Verwendung von return Statements innerhalb eines try-Blockes bei Java. Wieso dies so ist und was bei der Benutzung verwirrt zeigt dieser kleine Beitrag.

try-catch in der Benutzung

Normalerweise lassen sich mittels try/catch problemlos Exceptions abfangen und darauf reagieren. Eine normale Behandlung sieht wie folgt aus. Folgende Zeilen Code versuchen aus einer Datei zu lesen und werfen im Fehlerfall (Datei nicht gefunden, keine Leserechte etc) einen Hinweis. Die Systemausgabe im finally wird nach dem try/catch-Versuch ausgeführt.

InputStream input = new FileInputStream("unknownFilename.txt");

try {
   int data = input.read();
   // doing some with the data
}
catch(IOException e) {
   System.out.println("Error occured with reading file:" + e.getMessage());
}
finally {
   System.out.println("This will be done after the trying the stuff.");
}

Bei der Frage wann genau der Inhalt des <em>finally</em>-Blockes ausgegeben wird muss jedoch ins Detail gegangen werden.
<h2>Ausgaben vor dem return</h2>
Sichtbar wird das Problem bei folgenden Zeilen Code (ausführbar: <a href="https://ideone.com/LiOHOs" target="_blank">Ideone.com</a>)

import java.util.*;
import java.lang.*;
import java.io.*;
 
class someCreepyThings
{
	public static void main (String[] args) throws java.lang.Exception
	{
		System.out.println(test());
	}
 
	public static String test() {
 
		try {
			return "try";
		} finally {
			System.out.println("finally");
		}
	}
}

Was gibt dieser Code-Schnipsel aus? Zuerst das Wörtchen try und dann finally? Laut der Java-Dokumentation soll folgendes passieren:

The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs.

Damit vermutet man folgende Ausgabe:

try
finally

Jedoch ist es genau andersrum: Bevor ein return-Statement innerhalb eines try-Blockes ausgeführt wird führt Java den finally-Bereich aus. Dies wird auch in der zugehörigen Java-Doku erwähnt:

But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return,continue, or break.

Um nicht ausversehen den try-Block zu verlassen ohne aufgeräumt zu haben verhält sich Java genauso, und damit leider für einige Entwickler unerwartet:

finally
try

Jetzt wirds interessant

Wie verhält sich Java bei ähnlichen Konstrukten und Ausgaben wie eine zwischengelagerte Funktion in der Ausgabe (ausführbar: Ideone.com)

import java.util.*;
import java.lang.*;
import java.io.*;
 
class Ideone
{
	public static void main (String[] args) throws java.lang.Exception
	{
		System.out.println(test());
	}
 
	public static String test() {
 
		try {
			return printX();
		}
		finally {
			System.out.println("finally");
		}
	}
 
	public static String printX() {
 
		System.out.println("X");
		return "otherThings";
	}
}

Folgende Ausgabe wird angezeigt:

X
finally
otherThings

Diese Ausgabe wird nicht sofort erkannt.

Wie verhält es sich mit einfachen Variablen

Nachdem das Verhalten bei Ausgabe auf System.out nachvollziehbar, jedoch ungewohnt, ist verhält sich das Setzen und Ändern von Attributen anders.

import java.util.*;
import java.lang.*;
import java.io.*;
 
class someCreepyThing
{
	public static void main(final String[] args) {
	    System.out.println(test());
	}
 
	public static int test() {
		int i;
 
	    try {
	    	i = 2;
	        return i;
	    } finally {
	        i = 12;
	        System.out.println("finally trumps return.");
	    }
	}
}

Was wird ausgegeben?

Diese Frage haben wir ja bereits vorhin beantwortet: finally trumps return. wird zuerst ausgegeben, danach der Wert, der in i gespeichert ist. Doch:

Welchen Wert hat i?

Zuerst wird eine lokale Variable i instantiert, welche im try-Block auf 2 gesetzt wird. Bevor i zurück- und ausgegeben wird beendet Java unsere try-Abfrage mit dem finally-Block und setzt auf 12. So der normale Gedankengang. Jedoch:

finally trumps return
2

Wie lässt sich dieses Verhalten erklären? Der zurückzugebende Wert wird vor der Ausführung vom finally-Block zwischengespeichert und nicht mehr verändert.

Was passiert bei Objekten?

Vielleicht lässt sich der Rückgabewert verändern wenn Objekte benutzt werden. Erster Schritt ist ein Integer, welcher auf gleiche Art und Weise wie das einfache int benutzt wird (ausführbar: Ideone.com).

import java.util.*;
import java.lang.*;
import java.io.*;
 
class someCreepyThing
{
	public static void main(final String[] args) {
	    System.out.println(test());
	}
 
	public static Integer test() {
 
 	    Integer i = 0;
 
	    try {
	    	i = 2;
	        return i;
	    } finally {
	        i = 12;
	        System.out.println("finally trumps return.");
	    }
	}
}

Doch leider ändert sich nichts:

finally trumps return.
2

Unser eigenes Objekt

Jetzt wollen wir es wissen: Wir schreiben unser eigenes ganzzahliges Objekt (ausführbar: Ideone.com).

import java.util.*;
import java.lang.*;
import java.io.*;
 
class someCreepyThing
{
	public static void main(final String[] args) {
	    System.out.println(test());
	}
 
	public static int test() {
	    SomeObject i = new SomeObject();
	    try {
	        i.setI(2);
	        return i.stringMe();
	    } finally {
	        i.setI(12);
	        System.out.println("finally trumps return.");
	    }
	}
 
	static class SomeObject {
 
		static int i;
 
		public static void setI(int set) {
			i = set;
		}
 
		public static int stringMe() {
			return i;
		}
 
	}
 
}

Wir initialisieren das Objekt, setzen den internen int auf 2 und führen vor dem return des try-Blockes setI(12) aus. Da unsere return jedoch bereits die statische Methode stringMe() aufruft (und der Kompiler mir den Rückgabewert ausrechnet bevor finally ausgeführt wird) sehen wir dieselbe Ausgabe:

finally trumps return.
2

Nun Ändern wir unsere return-Zeile einwenig und geben das Objekt zurück (und nicht die bereits fertig berechnete Ausgabe) (ausführbar: Ideone.com).

import java.util.*;
import java.lang.*;
import java.io.*;
 
class someCreepyThing
{
	public static void main(final String[] args) {
	    System.out.println(test().stringMe());
	}
 
	public static SomeObject test() {
	    SomeObject i = new SomeObject();
	    try {
	        i.setI(2);
	        return i;
	    } finally {
	        i.setI(12);
	        System.out.println("finally trumps return.");
	    }
	}
 
	static class SomeObject {
 
		static int i;
 
		public static void setI(int set) {
			i = set;
		}
 
		public static int stringMe() {
			return i;
		}
 
	}
 
}

Und schwups: das Rückgabeobjekt sowie der passende Wert wird manipuliert:

finally trumps return
12

Wenn finally den try manipulieren

Ein try-Block funktioniert demnach genau, wie man es erwartet: zuerst wird deren Inhalt ausgeführt, danach folgen finally-Anweisungen. Mit einer Ausnahme: return-Anweisungen werden erst nach den finally-Befehlen zurückgegeben. Dadurch lassen sich Rückgabeobjekte manipulieren. Sogar so, dass sich eine geworfene Exception umgangen wird. Ein erstes Beispiel zeigt folgendes Snippet:

import java.util.*;
import java.lang.*;
import java.io.*;
 
class Ideone
{
	public static void main (String[] args) throws java.lang.Exception
	{
		System.out.print(test().getValue());
	}
 
	public static SomeObject test() {
 
		SomeObject object = new SomeObject();
 
		try {
			return object;
		} finally {
			object.throwNextTime();
		}
	}
 
	static class SomeObject {
 
		static boolean throwException = false;
 
		static String returnValue = "value";
 
		static String getValue() {
 
			if (throwException) {
				throw new RuntimeException();
			}
 
			return returnValue;
		}
 
		static void throwNextTime() {
			throwException = true;
		}
 
	}
}

Der finally-Block ändert das Objekt dahingehend, dass es einen Exception wirft.

Fazit

Nach soviel Verwirrung lässt sich nur eins Festlegen:

Benutz niemals return-Werte in try-Blöcken!

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.