Por que alterar a variável retornada em um bloco final não altera o valor de retorno?

146

Eu tenho uma classe Java simples, como mostrado abaixo:

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

E a saída desse código é esta:

Entry in finally Block
dev  

Por que snão é substituído no finallybloco, ainda controla a saída impressa?

Dev
fonte
11
você deve colocar a instrução de retorno na repetição bloco finally, comigo, finalmente bloco é sempre executada
Juan Antonio Gomez Moriano
1
no bloco try-lo retornar s
Devendra
6
A ordem das declarações é importante. Você está retornando santes de alterar seu valor.
precisa saber é o seguinte
6
Mas, você pode retornar o novo valor no finallybloco , ao contrário de C # (que você não pode)
Alvin Wong
3
Relacionados: não retorno "acontecer depois" finalmente
Alvin Wong

Respostas:

167

O trybloco é concluído com a execução da returninstrução e o valor de sno momento em que a returninstrução é executada é o valor retornado pelo método. O fato de a finallycláusula alterar posteriormente o valor de s(após a conclusão da returninstrução) não muda (nesse ponto) o valor de retorno.

Observe que o item acima lida com alterações no valor de ssi mesmo no finallybloco, não no objeto que faz sreferência. Se sfosse uma referência a um objeto mutável (o que Stringnão é) e o conteúdo do objeto fosse alterado no finallybloco, essas alterações seriam vistas no valor retornado.

As regras detalhadas de como tudo isso funciona podem ser encontradas na Seção 14.20.2 da Especificação da linguagem Java . Observe que a execução de uma returninstrução conta como um término abrupto do trybloco (a seção que inicia " Se a execução do bloco try for concluída abruptamente por qualquer outro motivo, R .... " se aplica). Consulte a Seção 14.17 do JLS para saber por que uma returninstrução é um encerramento abrupto de um bloco.

Por meio de mais detalhes: se o trybloco e o finallybloco de uma try-finallyinstrução terminam abruptamente devido a returninstruções, as seguintes regras do §14.20.2 se aplicam:

Se a execução do trybloco for concluída abruptamente por qualquer outro motivo R [além de gerar uma exceção], o finallybloco será executado e haverá uma opção:

  • Se o finallybloco for concluído normalmente, a tryinstrução será concluída abruptamente pelo motivo R.
  • Se o finallybloco for concluído abruptamente pelo motivo S, a tryinstrução será concluída abruptamente pelo motivo S (e o motivo R será descartado).

O resultado é que a returninstrução no finallybloco determina o valor de retorno de toda a try-finallyinstrução e o valor retornado do trybloco é descartado. O mesmo ocorre em uma try-catch-finallyinstrução se o trybloco lança uma exceção, é capturado por um catchbloco e o catchbloco e o finallybloco têm returninstruções.

Ted Hopp
fonte
Se eu usar a classe StringBuilder em vez de String do que acrescentar algum valor no bloco finalmente, isso altera o valor de retorno.por que?
Devendra
6
@ev - eu discuto isso no segundo parágrafo da minha resposta. Na situação que você descreve, o finallybloco não altera o objeto retornado (the StringBuilder), mas pode alterar as partes internas do objeto. O finallybloco é executado antes que o método realmente retorne (mesmo que a returninstrução tenha sido concluída); portanto, essas alterações ocorrem antes que o código de chamada veja o valor retornado.
Ted Hopp
tente com List, você obtém o mesmo comportamento que o StringBuilder.
Yogesh Prajapati
1
@yogeshprajapati - Yep. O mesmo acontece com qualquer valor de retorno mutável ( StringBuilder, List, Set, ad nauseum): se você alterar o conteúdo do finallybloco, em seguida, essas mudanças são vistos no código de chamada quando o método finalmente sai.
Ted Hopp
Isso diz o que acontece, não diz o porquê (seria particularmente útil se apontasse para onde isso foi coberto no JLS).
TJ Crowder
65

Porque o valor de retorno é colocado na pilha antes da chamada finalmente.

Tordek
fonte
3
Isso é verdade, mas não aborda a pergunta do OP sobre por que a string retornada não mudou. Isso tem a ver com imutabilidade e referências de string versus objetos do que empurrar o valor de retorno na pilha.
templatetypedef
Ambos os problemas estão relacionados.
Tordek
2
@templatetypedef, mesmo que String fosse mutável, o uso =não a modificaria.
Owen
1
@Saul - O ponto de Owen (correto) é que a imutabilidade não tem nada a ver com o motivo pelo qual o finallybloqueio do OP não afetou o valor de retorno. Eu acho que o que templatetypedef poderia estar chegando (embora isso não esteja claro) é que, porque o valor retornado é uma referência a um objeto imutável, mesmo alterar o código no finallybloco (exceto usar outra returninstrução) não poderia afetar o valor retornado do método
Ted Hopp
1
@templatetypedef Não, não. Nada a ver com a imutabilidade de String. O mesmo aconteceria com qualquer outro tipo. Tem a ver com avaliar a expressão de retorno antes de inserir o bloco final, ou seja, exacfly 'empurrando o valor de retorno na pilha'.
Marquês de Lorne
32

Se olharmos dentro do bytecode, perceberemos que o JDK fez uma otimização significativa e o método foo () se parece com:

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

E 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 preservou a string "dev" antes de retornar. De fato, aqui não há finalmente um bloqueio.

Mikhail
fonte
Isso não é uma otimização. Isso é simplesmente uma implementação da semântica necessária.
Marquês de Lorne
22

Há duas coisas dignas de nota aqui:

  • Strings são imutáveis. Quando você define s para "substituir a variável s", define s para se referir à String incorporada, não alterando o buffer de caracteres inerente do objeto s para alterar para "substituir a variável s".
  • Você coloca uma referência aos s na pilha para retornar ao código de chamada. Depois (quando o bloco finalmente é executado), a alteração da referência não deve fazer nada pelo valor de retorno já existente na pilha.
0xCAFEBABE
fonte
1
por isso, se eu pegar o stringbuffer do que o ferrão será substituído?
Devendra
10
@ dev - Se você alterasse o conteúdo do buffer na finallycláusula, isso seria visto no código de chamada. No entanto, se você atribuiu um novo buffer de seqüência de caracteres a s, então o comportamento seria o mesmo de agora.
achou
Sim, se eu acrescentar um buffer de string em finalmente as alterações do bloco refletem na saída.
Devendra
@ 0xCAFEBABE você também dá muito boas respostas e conceitos, eu agradeço muito a você.
Devendra
13

Mudo um pouco o seu código para provar o ponto de vista de Ted.

Como você pode ver na saída sé realmente alterado, mas após o retorno.

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

Resultado:

Entry in finally Block 
dev 
override variable s
Frank
fonte
por que a string substituída não retorna.
Devendra
2
Como Ted e Tordek já disse "o valor de retorno é colocado na pilha antes do finalmente é executada"
Frank
1
Embora essa seja uma boa informação adicional, estou relutante em votar de novo, porque ela não responde por si só.
Joachim Sauer
5

Tecnicamente falando, o returnbloco try não será ignorado se um finallybloco for definido, apenas se esse bloco finalmente também incluir a return.

É uma decisão de projeto duvidosa que provavelmente foi um erro em retrospecto (bem como referências sendo anuláveis ​​/ mutáveis ​​por padrão e, de acordo com algumas, exceções verificadas). De muitas maneiras, esse comportamento é exatamente consistente com o entendimento coloquial do que finallysignifica - "não importa o que aconteça de antemão no trybloco, sempre execute esse código". Portanto, se você retornar verdadeiro de um finallybloco, o efeito geral sempre deve ser return s, não?

Em geral, esse raramente é um bom idioma, e você deve usar finallyblocos liberalmente para limpar / fechar recursos, mas raramente, se é que alguma vez, retorna um valor a partir deles.

Kiran Jujare
fonte
É duvidoso por quê? É congruente com a ordem de avaliação em todos os outros contextos.
Marquês de Lorne
0

Tente isto: Se você deseja imprimir o valor de substituição de s.

finally {
    s = "override variable s";    
    System.out.println("Entry in finally Block");
    return s;
}
Achintya Jha
fonte
1
dá aviso .. finalmente bloco não for concluída normalmente
Devendra