Por que apenas variáveis ​​finais estão acessíveis na classe anônima?

355
  1. asó pode ser final aqui. Por quê? Como posso transferir ano onClick()método sem mantê-lo como membro privado?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
    
  2. Como posso devolver 5 * aquando clicou? Quero dizer,

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                 int b = a*5;
                 return b; // but return type is void 
            }
        });
    }
    
Andrii Abramov
fonte
11
Eu não acho que as classes Java anônimos fornecer o tipo de fechamento lambda que seria de esperar, mas alguém por favor me corrijam se eu estiver errado ...
user541686
4
O que você está tentando alcançar? O manipulador de cliques pode ser executado quando "f" for concluído.
Ivan Dubrov
@Lambert se você quiser usar um no método onClick que tem que ser @Ivan final, como lata f () se comporta como método onClick () int método de retorno quando clicado
2
É isso o que quero dizer - ele não suporta fechamento total porque não permite acesso a variáveis ​​não finais.
precisa saber é o seguinte
4
Nota: a partir do Java 8, sua variável precisa ser efetivamente final #
Peter Lawrey 24/02/16

Respostas:

489

Conforme observado nos comentários, parte disso se torna irrelevante no Java 8, onde finalpode estar implícito. Apenas uma variável efetivamente final pode ser usada em uma classe interna anônima ou expressão lambda.


Isso se deve basicamente à maneira como o Java gerencia os fechamentos .

Quando você cria uma instância de uma classe interna anônima, todas as variáveis ​​usadas nessa classe têm seus valores copiados por meio do construtor gerado automaticamente. Isso evita que o compilador precise gerar automaticamente vários tipos extras para manter o estado lógico das "variáveis ​​locais", como por exemplo o compilador C # ... (Quando o C # captura uma variável em uma função anônima, ele realmente captura a variável - o O encerramento pode atualizar a variável de uma maneira que é vista pelo corpo principal do método e vice-versa.)

Como o valor foi copiado para a instância da classe interna anônima, seria estranho se a variável pudesse ser modificada pelo restante do método - você poderia ter um código que parecia estar funcionando com uma variável desatualizada ( porque é efetivamente o que estaria acontecendo ... você estaria trabalhando com uma cópia tirada em outro momento). Da mesma forma, se você puder fazer alterações na classe interna anônima, os desenvolvedores podem esperar que essas alterações sejam visíveis no corpo do método que o envolve.

Tornar a variável final remove todas essas possibilidades - como o valor não pode ser alterado, você não precisa se preocupar se essas alterações serão visíveis. As únicas maneiras de permitir que o método e a classe interna anônima vejam as mudanças um do outro é usar um tipo mutável de alguma descrição. Pode ser a própria classe envolvente, uma matriz, um tipo de invólucro mutável ... qualquer coisa assim. Basicamente, é um pouco como se comunicar entre um método e outro: as alterações feitas nos parâmetros de um método não são vistas pelo responsável pela chamada, mas as alterações feitas nos objetos referidos pelos parâmetros são vistas.

Se você estiver interessado em uma comparação mais detalhada entre os fechamentos Java e C #, eu tenho um artigo que vai além. Eu queria me concentrar no lado Java nesta resposta :)

Jon Skeet
fonte
4
@ Ivan: Como C #, basicamente. No entanto, ele vem com um grau razoável de complexidade, se você deseja o mesmo tipo de funcionalidade que o C #, em que variáveis ​​de diferentes escopos podem ser "instanciadas" diferentes números de vezes.
perfil completo de Jon Skeet
11
Isso tudo foi verdade para o Java 7, lembre-se de que, com o Java 8, foram introduzidos encerramentos e agora é realmente possível acessar um campo não final de uma classe a partir de sua classe interna.
Mathias Bader
22
@MathiasBader: Sério? Eu pensei que ainda é essencialmente o mesmo mecanismo, o compilador agora é inteligente o suficiente para inferir final(mas ainda precisa ser efetivamente final).
Thilo
3
@ Matias Bader: você sempre pode acessar campos não finais , que não devem ser confundidos com variáveis locais , que precisavam ser finais e ainda precisam ser efetivamente finais, para que o Java 8 não mude a semântica.
Holger
41

Existe um truque que permite que a classe anônima atualize dados no escopo externo.

private void f(Button b, final int a) {
    final int[] res = new int[1];
    b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            res[0] = a * 5;
        }
    });

    // But at this point handler is most likely not executed yet!
    // How should we now res[0] is ready?
}

No entanto, esse truque não é muito bom devido aos problemas de sincronização. Se o manipulador for chamado posteriormente, você precisará 1) sincronizar o acesso ao res se o manipulador for chamado a partir do encadeamento diferente 2) precisar de algum tipo de sinalizador ou indicação de que o res foi atualizado

Esse truque funciona bem, no entanto, se a classe anônima for invocada no mesmo encadeamento imediatamente. Gostar:

// ...

final int[] res = new int[1];
Runnable r = new Runnable() { public void run() { res[0] = 123; } };
r.run();
System.out.println(res[0]);

// ...
Ivan Dubrov
fonte
2
obrigado pela sua resposta. Eu sei tudo isso e minha solução é melhor que isso. minha pergunta é "por que apenas final"?
6
A resposta é, então, que é como eles são implementados :)
Ivan Dubrov
11
Obrigado. Eu tinha usado o truque acima sozinho. Eu não tinha certeza se é uma boa ideia. Se o Java não permitir, pode haver uma boa razão. Sua resposta esclarece que meu List.forEachcódigo é seguro.
RuntimeException
Leia stackoverflow.com/q/12830611/2073130 para uma boa discussão sobre a lógica por trás de "por que apenas final".
Lcn
Existem várias soluções alternativas. A minha é: final int resf = res; Inicialmente, usei a abordagem de matriz, mas acho que ela tem uma sintaxe muito complicada. AtomicReference é talvez um pouco mais lento (aloca um objeto).
Zakmck
17

Uma classe anônima é uma classe interna e a regra estrita se aplica a classes internas (JLS 8.1.3) :

Qualquer variável local, parâmetro formal de método ou parâmetro de manipulador de exceção usado, mas não declarado em uma classe interna, deve ser declarado final . Qualquer variável local, usada mas não declarada em uma classe interna, deve ser definitivamente atribuída antes do corpo da classe interna .

Ainda não encontrei uma razão ou explicação sobre as jls ou jvms, mas sabemos que o compilador cria um arquivo de classe separado para cada classe interna e deve garantir que os métodos declarados nesse arquivo de classe ( no nível do código de bytes) pelo menos tenha acesso aos valores das variáveis ​​locais.

( Jon tem a resposta completa - eu mantenho esta desmarcada porque alguém pode estar interessado na regra JLS)

Andreas Dolk
fonte
11

Você pode criar uma variável no nível da classe para obter o valor retornado. Quero dizer

class A {
    int k = 0;
    private void f(Button b, int a){
        b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            k = a * 5;
        }
    });
}

agora você pode obter o valor de K e usá-lo onde quiser.

Resposta do seu porque é:

Uma instância local da classe interna está vinculada à classe Main e pode acessar as variáveis ​​locais finais do método que a contém. Quando a instância usa um local final do método que a contém, a variável retém o valor que ela mantinha no momento da criação da instância, mesmo que a variável tenha saído do escopo (essa é efetivamente a versão bruta e limitada de fechamentos de Java).

Como uma classe interna local não é membro de uma classe ou pacote, ela não é declarada com um nível de acesso. (Seja claro, no entanto, que seus próprios membros têm níveis de acesso como em uma classe normal.)

Ashfak Balooch
fonte
Eu mencionei que "sem mantê-lo como membro privado"
6

Bem, em Java, uma variável pode ser final não apenas como parâmetro, mas como um campo no nível da classe, como

public class Test
{
 public final int a = 3;

ou como uma variável local, como

public static void main(String[] args)
{
 final int a = 3;

Se você deseja acessar e modificar uma variável de uma classe anônima, convém tornar a variável uma variável de nível de classe na classe envolvente .

public class Test
{
 public int a;
 public void doSomething()
 {
  Runnable runnable =
   new Runnable()
   {
    public void run()
    {
     System.out.println(a);
     a = a+1;
    }
   };
 }
}

Você não pode ter uma variável como final e atribuir um novo valor. finalsignifica exatamente isso: o valor é imutável e final.

E, como é final, o Java pode copiá- lo com segurança para classes anônimas locais. Você não está obtendo alguma referência ao int (especialmente porque você não pode ter referências a primitivas como int em Java, apenas referências a objetos ).

Ele apenas copia o valor de a em um int implícito chamado a na sua classe anônima.

Zach L
fonte
3
Associo "variável de nível de classe" a static. Talvez seja mais claro se você usar "variável de instância".
eljenso
11
bem, usei o nível de classe porque a técnica funcionaria com variáveis ​​de instância e estáticas.
Zach L
já sabemos que a final é acessível, mas queremos saber por quê? você pode adicionar mais explicações sobre por que lado?
Saurabh Oza
6

A razão pela qual o acesso foi restrito apenas às variáveis ​​finais locais é que, se todas as variáveis ​​locais fossem tornadas acessíveis, elas primeiro precisariam ser copiadas para uma seção separada onde as classes internas possam ter acesso a elas e manter várias cópias de variáveis ​​locais mutáveis ​​podem levar a dados inconsistentes. Considerando que as variáveis ​​finais são imutáveis ​​e, portanto, qualquer número de cópias para elas não terá impacto na consistência dos dados.

Swapnil Gangrade
fonte
Não é assim que é implementado em idiomas como C # que suportam esse recurso. De fato, o compilador altera a variável de uma variável local para uma variável de instância ou cria uma estrutura de dados extra para essas variáveis ​​que podem sobreviver ao escopo da classe externa. No entanto, não há "várias cópias de variáveis locais"
Mike76
Mike76 Não observei a implementação do C #, mas o Scala faz a segunda coisa que você mencionou: Se um Intestá sendo transferido para dentro de um fechamento, altere essa variável para uma instância de IntRef(essencialmente um Integerinvólucro mutável ). Todo acesso à variável é reescrito de acordo.
Adowrath 15/09
3

Para entender a lógica dessa restrição, considere o seguinte programa:

public class Program {

    interface Interface {
        public void printInteger();
    }
    static Interface interfaceInstance = null;

    static void initialize(int val) {
        class Impl implements Interface {
            @Override
            public void printInteger() {
                System.out.println(val);
            }
        }
        interfaceInstance = new Impl();
    }

    public static void main(String[] args) {
        initialize(12345);
        interfaceInstance.printInteger();
    }
}

O interfaceInstance permanece na memória após o retorno do método initialize , mas o parâmetro val não. A JVM não pode acessar uma variável local fora de seu escopo; portanto, Java faz a chamada subseqüente para printInteger funcionar, copiando o valor de val para um campo implícito com o mesmo nome em interfaceInstance . O interfaceInstance se diz ter capturado o valor do parâmetro local. Se o parâmetro não fosse final (ou efetivamente final), seu valor poderia mudar, ficando fora de sincronia com o valor capturado, potencialmente causando um comportamento não intuitivo.

Benjamin Curtis Drake
fonte
2

Os métodos dentro de uma classe interna anônima podem ser invocados bem após o término do encadeamento que o gerou. No seu exemplo, a classe interna será chamada no segmento de despacho de eventos e não no mesmo segmento que o que o criou. Portanto, o escopo das variáveis ​​será diferente. Portanto, para proteger esses problemas de escopo de atribuição de variáveis, declare-os finais.

S73417H
fonte
2

Quando uma classe interna anônima é definida no corpo de um método, todas as variáveis ​​declaradas finais no escopo desse método são acessíveis de dentro da classe interna. Para valores escalares, uma vez atribuídos, o valor da variável final não pode ser alterado. Para valores de objeto, a referência não pode ser alterada. Isso permite que o compilador Java "capture" o valor da variável no tempo de execução e armazene uma cópia como um campo na classe interna. Depois que o método externo termina e o quadro da pilha é removido, a variável original desaparece, mas a cópia privada da classe interna persiste na memória da própria classe.

( http://en.wikipedia.org/wiki/Final_%28Java%29 )

ola_star
fonte
1
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

        @Override
        public void onClick(ClickEvent event) {
            a[0] = a[0] * 5;

        }
    });
}
Bruce Zu
fonte
0

Como Jon tem os detalhes da implementação, uma outra resposta possível seria que a JVM não deseja manipular a gravação em registro que encerrou sua ativação.

Considere o caso de uso em que suas lambdas, em vez de serem aplicadas, são armazenadas em algum lugar e executadas posteriormente.

Lembro-me de que em Smalltalk você obteria uma loja ilegal levantada ao fazer essa modificação.

mathk
fonte
0

Experimente este código,

Crie uma lista de matrizes e coloque valor dentro dela e retorne-o:

private ArrayList f(Button b, final int a)
{
    final ArrayList al = new ArrayList();
    b.addClickHandler(new ClickHandler() {

         @Override
        public void onClick(ClickEvent event) {
             int b = a*5;
             al.add(b);
        }
    });
    return al;
}
Wasim
fonte
O OP está perguntando por que motivo é necessário algo. Portanto, você deve apontar como o seu código está endereçando-o.
NitinSingh
0

A classe anônima Java é muito semelhante ao fechamento do Javascript, mas o Java implementa isso de maneira diferente. (verifique a resposta de Andersen)

Portanto, para não confundir o Java Developer com o comportamento estranho que pode ocorrer para quem vem do fundo do Javascript. Eu acho que é por isso que eles nos forçam a usar final, essa não é a limitação da JVM.

Vejamos o exemplo de Javascript abaixo:

var add = (function () {
  var counter = 0;

  var func = function () {
    console.log("counter now = " + counter);
    counter += 1; 
  };

  counter = 100; // line 1, this one need to be final in Java

  return func;

})();


add(); // this will print out 100 in Javascript but 0 in Java

Em Javascript, o countervalor será 100, porque existe apenas umcounter variável do começo ao fim.

Mas em Java, se não houver final, ele será impresso 0, porque enquanto o objeto interno está sendo criado, o 0valor é copiado para as propriedades ocultas do objeto da classe interna. (existem duas variáveis ​​inteiras aqui, uma no método local e outra nas propriedades ocultas da classe interna)

Portanto, qualquer alteração após a criação do objeto interno (como a linha 1), não afetará o objeto interno. Portanto, criará confusão entre dois resultados e comportamentos diferentes (entre Java e Javascript).

Acredito que é por isso que o Java decide forçá-lo a ser final, para que os dados sejam 'consistentes' do começo ao fim.

GMsoF
fonte
0

Variável final Java dentro de uma classe interna

classe interna pode usar apenas

  1. referência da classe externa
  2. variáveis ​​locais finais fora do escopo, que são um tipo de referência (por exemplo Object...)
  3. O valor (primitivo) (por exemplo int...) pode ser agrupado por um tipo de referência final. IntelliJ IDEApode ajudá-lo a encobri-lo em uma matriz de elemento

Quando a non static nested( inner class) [About] é gerado pelo compilador - uma nova classe - <OuterClass>$<InnerClass>.classé criada e os parâmetros vinculados são passados ​​para o construtor [variável local na pilha] . É semelhante ao fechamento

variável final é uma variável que não pode ser reatribuída. A variável de referência final ainda pode ser alterada modificando um estado

Era possível que fosse estranho, porque como programador você poderia fazer assim

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

            myClass.something(); //<- what is the address ?
            int b = a; //<- 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
        }
    });

    myClass = new MyClass(); //address 2
    int a = 10;
}
yoAlex5
fonte
-2

Talvez esse truque te dê uma idéia

Boolean var= new anonymousClass(){
    private String myVar; //String for example
    @Overriden public Boolean method(int i){
          //use myVar and i
    }
    public String setVar(String var){myVar=var; return this;} //Returns self instane
}.setVar("Hello").method(3);
Whimusical
fonte