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

Tengo una clase 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é s no se anula en el bloque finally , pero controla la salida impresa?

    El bloque try se completa con la ejecución de la instrucción return y el valor de s en el momento en que se ejecuta la instrucción return es el valor devuelto por el método. El hecho de que la cláusula finally cambie más tarde el valor de s (después de que se complete la statement de return ) no cambia (en ese punto) el valor de retorno.

    Tenga en cuenta que lo anterior se refiere a los cambios en el valor de s en el bloque finally , no en el objeto al que hace referencia. Si s era una referencia a un objeto mutable (qué String no es) y el contenido del objeto se cambió en el bloque finally , entonces esos cambios se verían en el valor devuelto.

    Las reglas detalladas de 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 statement de return cuenta como una terminación abrupta del bloque try (la sección que comienza ” Si la ejecución del bloque try se completa abruptamente por cualquier otra razón R … ” se aplica). Consulte la Sección 14.17 del JLS para saber por qué una statement de return es una terminación abrupta de un bloque.

    A modo de más detalle: si tanto el bloque try como el bloque finally de una instrucción try-finally finalizan abruptamente debido a declaraciones return , entonces se aplican las siguientes reglas de §14.20.2:

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

    • Si el bloque finally se completa normalmente, la instrucción try completa abruptamente por el motivo R.
    • Si el bloque final se completa abruptamente por la razón S, entonces la instrucción try completa abruptamente por la razón S (y la razón R se descarta).

    El resultado es que la statement return en el bloque finally determina el valor de retorno de la sentencia try-finally , y el valor devuelto del bloque try se descarta. Algo similar ocurre en una statement try-catch-finally si el bloque try arroja una excepción, es capturado por un bloque catch , y tanto el catch block como el finally block tienen return statements.

    Porque el valor de retorno se pone en la stack antes de la llamada para finalmente.

    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 preservado “dev” cadena de ser cambiado antes de regresar. De hecho, aquí no hay un locking definitivo.

    Hay 2 cosas dignas de mención aquí:

    • Las cadenas son inmutables. Cuando establece s para “anular la variable s”, establece s para hacer referencia 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 stack para regresar al código de llamada. Después (cuando se ejecuta el bloque finally), alterar la referencia no debería hacer nada para el valor de retorno que ya está en la stack.

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

    Como se puede ver en la salida s se cambia, pero después de la devolución.

     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 

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

    Es una decisión de diseño dudosa que probablemente fue un error en retrospectiva (al igual que las referencias que pueden ser anulables / mutables por defecto y, según algunos, las excepciones marcadas). En muchos sentidos, este comportamiento es exactamente coherente con la comprensión coloquial de lo que finally significa: “no importa lo que ocurra de antemano en el bloque try , siempre ejecute este código”. Por lo tanto, si devuelve true desde un bloque finally , el efecto general siempre debe ser return s , ¿no?

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

    Intente esto: si desea imprimir el valor de sobrescritura de s.

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