¿Por qué cambiar la variable devuelta en un bloque finalmente no cambia el valor de retorno?

146

Tengo una clase de Java simple como se muestra a continuación:

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());
    }
}

Y el resultado de este código es este:

Entry in finally Block
dev  

¿Por qué sno se anula en el finallybloque y controla la salida impresa?

Dev
fuente
11
deberías poner la declaración de devolución en el bloque finalmente, repetir conmigo, finalmente el bloque siempre se ejecuta
Juan Antonio Gomez Moriano
1
en try block it return s
Devendra
66
El orden de las declaraciones es importante. Regresará santes de cambiar su valor.
Peter Lawrey
66
Sin embargo, se puede devolver el valor nuevo en el finallybloque , al contrario que en C # (que no se puede)
Alvin Wong
3
Relacionado: finalmente vuelve "suceder después"
Alvin Wong

Respuestas:

167

El trybloque se completa con la ejecución de la returndeclaración y el valor de sen el momento en returnque se ejecuta la declaración es el valor devuelto por el método. El hecho de que la finallycláusula luego cambie el valor de s(después de que la returndeclaración se complete) no (en ese punto) cambia el valor de retorno.

Tenga en cuenta que lo anterior trata con cambios en el valor de ssí mismo en el finallybloque, no en el objeto al que hace sreferencia. Si sfue una referencia a un objeto mutable (que Stringno lo es) y el contenido del objeto se modificó en el finallybloque, esos cambios se verían en el valor devuelto.

Las reglas detalladas sobre cómo funciona todo esto se pueden encontrar en la Sección 14.20.2 de la Especificación del lenguaje Java . Tenga en cuenta que la ejecución de una returndeclaración cuenta como una terminación abrupta del trybloque (se aplica la sección que comienza " Si la ejecución del bloque try finaliza abruptamente por cualquier otro motivo R ... "). Ver Sección 14.17 del JLS para ver por qué una returndeclaración es una terminación abrupta de un bloque.

A modo de más detalles: si tanto el trybloque como el finallybloque de una try-finallydeclaración terminan abruptamente debido a returndeclaraciones, entonces se aplican las siguientes reglas del §14.20.2:

Si la ejecución del trybloque se completa abruptamente por cualquier otra razón R [además de lanzar una excepción], entonces el finallybloque se ejecuta y luego hay una opción:

  • Si el finallybloque se completa normalmente, la trydeclaración se completa abruptamente por la razón R.
  • Si el finallybloque se completa abruptamente por la razón S, entonces la trydeclaración se completa abruptamente por la razón S (y la razón R se descarta).

El resultado es que la returndeclaración en el finallybloque determina el valor de retorno de toda la try-finallydeclaración, y el valor devuelto del trybloque se descarta. Algo similar ocurre en una try-catch-finallydeclaración si el trybloque arroja una excepción, es atrapado por un catchbloque y tanto el catchbloque como el finallybloque tienen returndeclaraciones.

Ted Hopp
fuente
Si uso la clase StringBuilder en lugar de String que agrega algún valor en el bloque finalmente, cambia el valor de retorno. ¿Por qué?
Devendra
66
@dev - Discuto eso en el segundo párrafo de mi respuesta. En la situación que describe, el finallybloque no cambia qué objeto se devuelve (el StringBuilder) pero puede cambiar las partes internas del objeto. El finallybloque se ejecuta antes de que el método realmente regrese (aunque la returndeclaración haya finalizado), por lo que esos cambios ocurren antes de que el código de llamada vea el valor devuelto.
Ted Hopp
intente con List, obtendrá el mismo comportamiento que StringBuilder.
Yogesh Prajapati
1
@yogeshprajapati - Sí. Lo mismo es cierto con cualquier valor de retorno mutable ( StringBuilder, List, Set, hasta la saciedad): si cambia el contenido en el finallybloque, a continuación, se observan los cambios en el código de llamada cuando el método finalmente salidas.
Ted Hopp
Esto dice lo que sucede, no dice por qué (sería particularmente útil si señalara dónde se cubrió esto en el JLS).
TJ Crowder
65

Porque el valor de retorno se coloca en la pila antes de la llamada a finalmente.

Tordek
fuente
3
Eso es cierto, pero no aborda la pregunta del OP sobre por qué la cadena devuelta no cambió. Eso tiene que ver con la inmutabilidad de la cadena y las referencias frente a objetos más que empujar el valor de retorno en la pila.
templatetypedef
Ambas cuestiones están relacionadas.
Tordek
2
@templatetypedef incluso si String fuera mutable, el uso =no lo mutaría.
Owen
1
@Saul: el punto de Owen (que es correcto) fue que la inmutabilidad no tiene nada que ver con por qué el finallybloqueo de OP no afectó el valor de retorno. Creo que a lo que templatetypedef podría haber estado llegando (aunque esto no está claro) es que debido a que el valor devuelto es una referencia a un objeto inmutable, incluso cambiar el código en el finallybloque (aparte de usar otra returndeclaración) no podría afectar el valor devuelto por el método
Ted Hopp
1
@templatetypedef No, no lo hace. Nada que ver con la inmutabilidad de String en absoluto. Lo mismo sucedería con cualquier otro tipo. Tiene que ver con evaluar la expresión de retorno antes de ingresar el bloque finalmente, en otras palabras, exactamente 'empujar el valor de retorno en la pila'.
Marqués de Lorne
32

Si miramos dentro de bytecode, notaremos que JDK ha realizado una optimización significativa, y el método foo () se ve así:

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

Y 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 preservó la cadena "dev" de ser cambiada antes de regresar. De hecho, aquí no hay finalmente un bloqueo en absoluto.

Mikhail
fuente
Esto no es una optimización. Esto es simplemente una implementación de la semántica requerida.
Marqués de Lorne
22

Hay 2 cosas notables aquí:

  • Las cuerdas son inmutables. Cuando configuras s para "anular la variable s", configuras s para referirse a la cadena en línea, sin alterar el búfer de caracteres inherente del objeto s para cambiar a "anular la variable s".
  • Pones una referencia a la s en la pila para volver al código de llamada. Después (cuando se ejecuta finalmente el bloque), alterar la referencia no debería hacer nada por el valor de retorno que ya está en la pila.
0xCAFEBABE
fuente
1
Entonces, si tomo stringbuffer, ¿se anulará la picadura?
Devendra
10
@dev: si tuviera que cambiar el contenido del búfer en la finallycláusula, eso se vería en el código de llamada. Sin embargo, si asignó un nuevo buffer de cadenas a s, entonces el comportamiento sería el mismo que ahora.
Ted Hopp
Sí, si agrego en el búfer de cadena en finalmente los cambios de bloque se reflejan en la salida.
Devendra
@ 0xCAFEBABE también da muy buena respuesta y conceptos, le agradezco mucho.
Devendra
13

Cambio un poco tu código para probar el punto de Ted.

Como puede ver en la salida, de shecho, cambia pero después del regreso.

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);
}
}

Salida:

Entry in finally Block 
dev 
override variable s
Franco
fuente
por qué las cadenas anuladas no regresan.
Devendra
2
Como Ted y Tordek ya dijeron "el valor de retorno se pone en la pila antes de que finalmente se ejecute"
Frank
1
Si bien esta es una buena información adicional, soy reacio a votarla porque no responde (por sí sola) a la pregunta.
Joachim Sauer
5

Técnicamente hablando, el returnbloque en el try no se ignorará si finallyse define un bloque, solo si ese bloque finalmente también incluye a return.

Es una decisión de diseño dudosa que probablemente fue un error en retrospectiva (al igual que las referencias son anulables / mutables de forma predeterminada y, según algunos, excepciones marcadas). En muchos sentidos, este comportamiento es exactamente consistente con la comprensión coloquial de lo que finallysignifica: "no importa lo que ocurra de antemano en el trybloque, siempre ejecute este código". Por lo tanto, si devuelve verdadero de un finallybloque, el efecto general siempre debe ser return s, ¿no?

En general, esto rara vez es una buena expresión idiomática, y debe usar finallybloques generosamente para limpiar / cerrar recursos, pero rara vez devuelve un valor de ellos.

Kiran Jujare
fuente
Es dudoso por qué? Es congruente con el orden de evaluación en todos los demás contextos.
Marqués de Lorne
0

Pruebe esto: si desea imprimir el valor de anulación de s.

finally {
    s = "override variable s";    
    System.out.println("Entry in finally Block");
    return s;
}
Achintya Jha
fuente
1
da advertencia .. finalmente el bloque no se completa normalmente
Devendra