Warum ändert das Ändern der zurückgegebenen Variablen in einem finally-Block nicht den Rückgabewert?

146

Ich habe eine einfache Java-Klasse wie unten gezeigt:

public class Test {

    private String s;

    public String foo() {
        try {
            s = "dev";
            return s;
        } 
        finally {
            s = "override variable s";
            System.out.println("Entry in finally Block");  
        }
    }

    public static void main(String[] xyz) {
        Test obj = new Test();
        System.out.println(obj.foo());
    }
}

Und die Ausgabe dieses Codes ist folgende:

Entry in finally Block
dev  

Warum wird sim finallyBlock nicht überschrieben , aber die gedruckte Ausgabe gesteuert?

Dev
quelle
11
Sie sollten die return-Anweisung auf den finally-Block setzen, mit mir wiederholen, finally-Block wird immer ausgeführt
Juan Antonio Gomez Moriano
1
in try block gibt es s zurück
Devendra
6
Die Reihenfolge der Aussagen ist wichtig. Sie kehren zurück, sbevor Sie den Wert ändern.
Peter Lawrey
6
Aber Sie können den neuen Wert im finallyBlock zurückgeben , im Gegensatz zu C # (was Sie nicht können)
Alvin Wong
3
Verwandte: kehrt endlich "passiert nach" zurück
Alvin Wong

Antworten:

167

Der tryBlock wird mit der Ausführung der returnAnweisung abgeschlossen, und der Wert szum Zeitpunkt der returnAusführung der Anweisung ist der von der Methode zurückgegebene Wert. Die Tatsache, dass die finallyKlausel später den Wert von s(nach Abschluss der returnAnweisung) ändert, ändert (zu diesem Zeitpunkt) den Rückgabewert nicht.

Beachten Sie, dass sich das Obige auf Änderungen des Werts von sich sselbst im finallyBlock sbezieht , nicht auf das Objekt, auf das verwiesen wird. Wenn sein Verweis auf ein veränderbares Objekt (was Stringnicht der Fall ist) und der Inhalt des Objekts im finallyBlock geändert wurden , werden diese Änderungen im zurückgegebenen Wert angezeigt.

Die detaillierten Regeln für die Funktionsweise finden Sie in Abschnitt 14.20.2 der Java-Sprachspezifikation . Beachten Sie, dass die Ausführung einer returnAnweisung als abrupte Beendigung des tryBlocks gilt (es gilt der Abschnitt " Wenn die Ausführung des try-Blocks aus einem anderen Grund abrupt abgeschlossen wird ... "). In Abschnitt 14.17 des JLS erfahren Sie, warum eine returnAnweisung eine abrupte Beendigung eines Blocks darstellt.

Zur weiteren Detaillierung: Wenn sowohl der tryBlock als auch der finallyBlock einer try-finallyAnweisung aufgrund von returnAnweisungen abrupt beendet werden , gelten die folgenden Regeln aus §14.20.2:

Wenn die Ausführung des tryBlocks aus einem anderen Grund abrupt beendet wird [außer eine Ausnahme auszulösen], wird der finallyBlock ausgeführt, und dann gibt es eine Auswahl:

  • Wenn der finallyBlock normal abgeschlossen wird, wird die tryAnweisung aus Grund R abrupt abgeschlossen.
  • Wenn der finallyBlock aus Grund S abrupt abgeschlossen wird, wird die tryAnweisung aus Grund S abrupt abgeschlossen (und Grund R wird verworfen).

Das Ergebnis ist, dass die returnAnweisung im finallyBlock den Rückgabewert der gesamten try-finallyAnweisung bestimmt und der vom tryBlock zurückgegebene Wert verworfen wird. Ähnliches gilt für eine try-catch-finallyAnweisung, wenn der tryBlock eine Ausnahme auslöst, von einem catchBlock abgefangen wird und sowohl der catchBlock als auch der finallyBlock returnAnweisungen haben.

Ted Hopp
quelle
Wenn ich die StringBuilder-Klasse anstelle von String verwende, als einen Wert im finally-Block anzuhängen, ändert sich der Rückgabewert. Warum?
Devendra
6
@dev - Ich diskutiere das im zweiten Absatz meiner Antwort. In der von Ihnen beschriebenen Situation finallyändert der Block nicht, welches Objekt zurückgegeben wird, StringBuildersondern die Interna des Objekts. Der finallyBlock wird ausgeführt, bevor die Methode tatsächlich zurückgegeben wird (obwohl die returnAnweisung beendet wurde), sodass diese Änderungen auftreten, bevor der aufrufende Code den zurückgegebenen Wert sieht.
Ted Hopp
Wenn Sie es mit List versuchen, erhalten Sie dasselbe Verhalten wie mit StringBuilder.
Yogesh Prajapati
1
@yogeshprajapati - Ja. Das gleiche gilt bei jedem wandelbar Rückgabewert ( StringBuilder, List, Set, bis zum Überdruss): Wenn Sie den Inhalt im ändern finallyBlock, dann werden diese Änderungen in dem Aufruf - Code zu sehen , wenn das Verfahren schließlich beendet.
Ted Hopp
Dies sagt, was passiert, es sagt nicht warum (wäre besonders nützlich, wenn darauf hingewiesen würde, wo dies in der JLS behandelt wurde).
TJ Crowder
65

Weil der Rückgabewert vor dem Aufruf von finally auf den Stack gelegt wird.

Tordek
quelle
3
Das stimmt, aber es geht nicht auf die Frage des OP ein, warum sich die zurückgegebene Zeichenfolge nicht geändert hat. Dies hat mehr mit der Unveränderlichkeit von Zeichenfolgen und Referenzen als mit Objekten zu tun, als den Rückgabewert auf dem Stapel zu verschieben.
Templatetypedef
Beide Probleme hängen zusammen.
Tordek
2
@templatetypedef selbst wenn String veränderbar wäre, =würde using ihn nicht mutieren.
Owen
1
@Saul - Owens Punkt (der richtig ist) war, dass Unveränderlichkeit nichts damit zu tun hat, warum der finallyBlock von OP den Rückgabewert nicht beeinflusst hat. Ich denke, was templatetypedef möglicherweise erreicht hat (obwohl dies nicht klar ist), ist, dass selbst das Ändern des Codes im finallyBlock (außer die Verwendung einer anderen returnAnweisung) keine Auswirkungen auf das hat , da der zurückgegebene Wert eine Referenz auf ein unveränderliches Objekt ist Von der Methode zurückgegebener Wert.
Ted Hopp
1
@templatetypedef Nein, tut es nicht. Nichts mit der Unveränderlichkeit von Strings zu tun. Gleiches würde bei jedem anderen Typ passieren. Es hat mit der Auswertung des Rückgabeausdrucks vor dem Eingeben des finally-Blocks zu tun, mit anderen Worten exacfly 'den Rückgabewert auf den Stapel schieben'.
Marquis von Lorne
32

Wenn wir uns den Bytecode ansehen, werden wir feststellen, dass JDK eine signifikante Optimierung vorgenommen hat und die foo () -Methode wie folgt aussieht:

String tmp = null;
try {
    s = "dev"
    tmp = s;
    s = "override variable s";
    return tmp;
} catch (RuntimeException e){
    s = "override variable s";
    throw e;
}

Und Bytecode:

0:  ldc #7;         //loading String "dev"
2:  putstatic   #8; //storing it to a static variable
5:  getstatic   #8; //loading "dev" from a static variable
8:  astore_0        //storing "dev" to a temp variable
9:  ldc #9;         //loading String "override variable s"
11: putstatic   #8; //setting a static variable
14: aload_0         //loading a temp avariable
15: areturn         //returning it
16: astore_1
17: ldc #9;         //loading String "override variable s"
19: putstatic   #8; //setting a static variable
22: aload_1
23: athrow

Java hat verhindert, dass der "dev" -String vor der Rückkehr geändert wird. Tatsächlich gibt es hier überhaupt keinen endgültigen Block.

Mikhail
quelle
Dies ist keine Optimierung. Dies ist lediglich eine Implementierung der erforderlichen Semantik.
Marquis von Lorne
22

Hier sind zwei Dinge bemerkenswert:

  • Saiten sind unveränderlich. Wenn Sie s auf "Variable s überschreiben" setzen, setzen Sie s so, dass es sich auf den inline-String bezieht, ohne den inhärenten Zeichenpuffer des s-Objekts zu ändern, um in "Variable s überschreiben" zu ändern.
  • Sie setzen einen Verweis auf das s auf dem Stapel, um zum aufrufenden Code zurückzukehren. Danach (wenn der finally-Block ausgeführt wird) sollte das Ändern der Referenz nichts für den Rückgabewert tun, der sich bereits auf dem Stapel befindet.
0xCAFEBABE
quelle
1
Also, wenn ich Stringbuffer nehme, wird Sting überschrieben?
Devendra
10
@dev - Wenn Sie den Inhalt des Puffers in der finallyKlausel ändern würden, würde dies im aufrufenden Code angezeigt . Wenn Sie jedoch einen neuen Zeichenfolgenpuffer zugewiesen haben s, ist das Verhalten das gleiche wie jetzt.
Ted Hopp
Ja, wenn ich im Zeichenfolgenpuffer in schließlich Blockänderungen anhänge, spiegeln sie die Ausgabe wider.
Devendra
@ 0xCAFEBABE Sie geben auch sehr gute Antworten und Konzepte, ich danke Ihnen voll und ganz.
Devendra
13

Ich ändere deinen Code ein wenig, um den Sinn von Ted zu beweisen.

Wie Sie in der Ausgabe sehen können, swird zwar geändert, aber nach der Rückkehr.

public class Test {

public String s;

public String foo() {

    try {
        s = "dev";
        return s;
    } finally {
        s = "override variable s";
        System.out.println("Entry in finally Block");

    }
}

public static void main(String[] xyz) {
    Test obj = new Test();
    System.out.println(obj.foo());
    System.out.println(obj.s);
}
}

Ausgabe:

Entry in finally Block 
dev 
override variable s
Frank
quelle
warum überschriebene Zeichenfolgen nicht zurückgegeben werden.
Devendra
2
Wie Ted und Tordek bereits sagten "der Rückgabewert wird auf den Stapel gelegt, bevor der endgültige ausgeführt wird"
Frank
1
Dies sind zwar gute zusätzliche Informationen, ich zögere jedoch, sie zu bewerten, da sie die Frage (allein) nicht beantworten.
Joachim Sauer
5

Technisch gesehen wird der returnBlock im try-Block nicht ignoriert, wenn ein finallyBlock definiert ist, sondern nur, wenn dieser endgültige Block auch a enthält return.

Es ist eine zweifelhafte Entwurfsentscheidung, die im Nachhinein wahrscheinlich ein Fehler war (ähnlich wie Referenzen, die standardmäßig nullbar / veränderbar sind, und nach einigen überprüften Ausnahmen). In vielerlei Hinsicht stimmt dieses Verhalten genau mit dem umgangssprachlichen Verständnis dessen überein, was finallybedeutet: "Egal, was vorher im tryBlock passiert , führen Sie diesen Code immer aus." Wenn Sie also von einem finallyBlock true zurückgeben , muss der Gesamteffekt immer zu sein return s, nein?

Im Allgemeinen ist dies selten eine gute Redewendung, und Sie sollten finallyBlöcke großzügig zum Bereinigen / Schließen von Ressourcen verwenden, aber selten oder nie einen Wert von ihnen zurückgeben.

Kiran Jujare
quelle
Es ist zweifelhaft warum? Es stimmt mit der Bewertungsreihenfolge in allen anderen Kontexten überein.
Marquis von Lorne
0

Versuchen Sie Folgendes: Wenn Sie den Überschreibungswert von s drucken möchten.

finally {
    s = "override variable s";    
    System.out.println("Entry in finally Block");
    return s;
}
Achintya Jha
quelle
1
es gibt Warnung .. schließlich Block blockiert nicht normal
Devendra