¿Por qué solo las variables finales son accesibles en la clase anónima?

355
  1. asolo puede ser final aquí. ¿Por qué? ¿Cómo puedo volver a asignar aen el onClick()método sin manteniéndola como miembro privado?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
  2. ¿Cómo puedo devolver el 5 * acuando hizo clic? Quiero decir,

    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
fuente
1
No creo que las clases de Java anónimos proporcionan el tipo de cierre lambda que es de esperar, pero por favor alguien me corrija si me equivoco ...
user541686
44
¿Qué estás intentando lograr? El controlador de clics podría ejecutarse cuando "f" haya finalizado.
Ivan Dubrov
@Lambert si desea utilizar un método onClick en el que tiene que ser definitiva @Ivan ¿Cómo puede f () se comporta como método onClick () int retorno del método cuando se hace clic
2
A eso me refiero: no admite el cierre completo porque no permite el acceso a variables no finales.
user541686
44
Nota: a partir de Java 8, su variable solo necesita ser efectivamente final
Peter Lawrey

Respuestas:

489

Como se señaló en los comentarios, algo de esto se vuelve irrelevante en Java 8, donde finalpuede estar implícito. Sin embargo, solo se puede usar una variable final efectiva en una clase interna anónima o una expresión lambda.


Básicamente se debe a la forma en que Java gestiona los cierres .

Cuando crea una instancia de una clase interna anónima, todas las variables que se utilizan dentro de esa clase tienen sus valores copiados a través del constructor autogenerado. Esto evita que el compilador tenga que generar automáticamente varios tipos adicionales para mantener el estado lógico de las "variables locales", como por ejemplo el compilador de C # lo hace ... (Cuando C # captura una variable en una función anónima, realmente captura la variable: el El cierre puede actualizar la variable de una manera que es vista por el cuerpo principal del método, y viceversa).

Como el valor se ha copiado en la instancia de la clase interna anónima, sería extraño que la variable pudiera modificarse por el resto del método; podría tener un código que parece estar funcionando con una variable desactualizada ( porque eso es efectivamente lo que sucedería ... estarías trabajando con una copia tomada en un momento diferente). Del mismo modo, si pudiera realizar cambios dentro de la clase interna anónima, los desarrolladores podrían esperar que esos cambios sean visibles dentro del cuerpo del método de inclusión.

Hacer que la variable final elimine todas estas posibilidades, ya que el valor no se puede cambiar en absoluto, no necesita preocuparse por si dichos cambios serán visibles. Las únicas formas de permitir que el método y la clase interna anónima vean los cambios de cada uno es usar un tipo mutable de alguna descripción. Esta podría ser la clase que la encierra, una matriz, un tipo de envoltorio mutable ... algo así. Básicamente es un poco como comunicarse entre un método y otro: los cambios realizados en los parámetros de un método no son vistos por quien llama, pero sí los cambios realizados en los objetos a los que se refieren los parámetros.

Si está interesado en una comparación más detallada entre los cierres de Java y C #, tengo un artículo que profundiza más. Quería centrarme en el lado de Java en esta respuesta :)

Jon Skeet
fuente
44
@Ivan: Al igual que C #, básicamente. Sin embargo, viene con un alto grado de complejidad, si desea el mismo tipo de funcionalidad que C #, donde las variables de diferentes ámbitos pueden "instanciarse" diferentes números de veces.
Jon Skeet
11
Todo esto fue cierto para Java 7, tenga en cuenta que con Java 8, se han introducido cierres y ahora es posible acceder a un campo no final de una clase desde su clase interna.
Mathias Bader
22
@MathiasBader: ¿En serio? Pensé que todavía era esencialmente el mismo mecanismo, el compilador ahora es lo suficientemente inteligente como para inferir final(pero aún debe ser efectivamente final).
Thilo
3
@Mathias Bader: siempre se podía acceder a campos no finales , que no deben confundirse con variables locales , que tenían que ser finales y aún así deben ser efectivamente finales, para que Java 8 no cambie la semántica.
Holger
41

Hay un truco que permite a la clase anónima actualizar datos en el ámbito 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?
}

Sin embargo, este truco no es muy bueno debido a los problemas de sincronización. Si el controlador se invoca más tarde, debe 1) sincronizar el acceso a res si el controlador se invocó desde el hilo diferente 2) debe tener algún tipo de indicador o indicación de que se actualizó res

Sin embargo, este truco funciona bien si la clase anónima se invoca en el mismo hilo inmediatamente. Me gusta:

// ...

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
fuente
2
gracias por tu respuesta. Sé todo esto y mi solución es mejor que esto. mi pregunta es "¿por qué solo final"?
66
La respuesta es que así es como se implementan :)
Ivan Dubrov
1
Gracias. Había usado el truco anterior solo. No estaba seguro de si es una buena idea. Si Java no lo permite, puede haber una buena razón. Su respuesta aclara que mi List.forEachcódigo es seguro.
RuntimeException
Lea stackoverflow.com/q/12830611/2073130 para una buena discusión sobre la razón detrás de "por qué solo final".
lcn
Hay varias soluciones alternativas. El mío es: final int resf = res; Inicialmente utilicé el enfoque de matriz, pero creo que tiene una sintaxis demasiado engorrosa. La referencia atómica es quizás un poco más lenta (asigna un objeto).
zakmck
17

Una clase anónima es una clase interna y la regla estricta se aplica a las clases internas (JLS 8.1.3) :

Cualquier variable local, parámetro de método formal o parámetro de controlador de excepción utilizado pero no declarado en una clase interna debe declararse final . Cualquier variable local, utilizada pero no declarada en una clase interna debe asignarse definitivamente antes del cuerpo de la clase interna .

Todavía no he encontrado una razón o una explicación sobre jls o jvms, pero sí sabemos que el compilador crea un archivo de clase separado para cada clase interna y tiene que asegurarse de que los métodos declarados en este archivo de clase ( en el nivel de código de byte) al menos tener acceso a los valores de las variables locales.

( Jon tiene la respuesta completa : mantengo esta sin borrar porque uno podría estar interesado en la regla JLS)

Andreas Dolk
fuente
11

Puede crear una variable de nivel de clase para obtener el valor devuelto. quiero decir

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

ahora puedes obtener el valor de K y usarlo donde quieras.

La respuesta de tu por qué es:

Una instancia de clase interna local está vinculada a la clase Main y puede acceder a las variables locales finales de su método contenedor. Cuando la instancia usa un local final de su método de contención, la variable retiene el valor que tenía en el momento de la creación de la instancia, incluso si la variable ha salido del alcance (esta es efectivamente la versión cruda y limitada de los cierres de Java).

Como una clase interna local no es miembro de una clase o paquete, no se declara con un nivel de acceso. (Sea claro, sin embargo, que sus propios miembros tienen niveles de acceso como en una clase normal).

Ashfak Balooch
fuente
Mencioné que "sin mantenerlo como miembro privado"
6

Bueno, en Java, una variable puede ser final no solo como un parámetro, sino como un campo de nivel de clase, como

public class Test
{
 public final int a = 3;

o como una variable local, como

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

Si desea acceder y modificar una variable desde una clase anónima, puede hacer que la variable sea una variable de nivel de clase en la clase adjunta .

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

No puede tener una variable como final y darle un nuevo valor. finalsignifica solo eso: el valor es inmutable y final.

Y como es final, Java puede copiarlo de forma segura en clases anónimas locales. No está obteniendo alguna referencia a int (especialmente porque no puede tener referencias a primitivas como int en Java, solo referencias a objetos ).

Simplemente copia el valor de a en un int implícito llamado a en su clase anónima.

Zach L
fuente
3
Asociado "variable de nivel de clase" con static. Quizás sea más claro si usa "variable de instancia" en su lugar.
eljenso
1
bueno, usé el nivel de clase porque la técnica funcionaría con variables tanto de instancia como estáticas.
Zach L
Ya sabemos que las finales son accesibles pero queremos saber por qué. ¿Puedes agregar alguna explicación más sobre por qué lado?
Saurabh Oza
6

La razón por la que el acceso se ha restringido solo a las variables finales locales es que si todas las variables locales se hicieran accesibles, primero tendrían que copiarse en una sección separada donde las clases internas pueden tener acceso a ellas y mantener múltiples copias de Las variables locales mutables pueden conducir a datos inconsistentes. Mientras que las variables finales son inmutables y, por lo tanto, cualquier cantidad de copias no tendrá ningún impacto en la consistencia de los datos.

Swapnil Gangrade
fuente
No es así como se implementa en lenguajes como C # que admiten esta función. De hecho, el compilador cambia la variable de una variable local a una variable de instancia, o crea una estructura de datos adicional para estas variables que puede superar el alcance de la clase externa. Sin embargo, no hay "copias múltiples de variables locales"
Mike76
Mike76 No he echado un vistazo a la implementación de C #, pero Scala hace lo segundo que mencionó, creo: si Intse reasigna un dentro de un cierre, cambie esa variable a una instancia de IntRef(esencialmente un Integercontenedor mutable ). Cada acceso variable se reescribe en consecuencia.
Adowrath
3

Para comprender la justificación de esta restricción, considere el siguiente 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();
    }
}

El interfaceInstance permanece en la memoria después de los initialize método devuelve, pero el parámetro val no lo hace. La JVM no puede acceder a una variable local fuera de su alcance, por lo que Java hace que la llamada posterior a printInteger funcione copiando el valor de val en un campo implícito del mismo nombre dentro de interfaceInstance . Se dice que interfaceInstance ha capturado el valor del parámetro local. Si el parámetro no fuera definitivo (o efectivamente final), su valor podría cambiar, quedando fuera de sincronía con el valor capturado, lo que podría causar un comportamiento poco intuitivo.

Benjamin Curtis Drake
fuente
2

Los métodos dentro de una clase interna anónima pueden invocarse mucho después de que el hilo que engendró haya terminado. En su ejemplo, la clase interna se invocará en el hilo de envío de eventos y no en el mismo hilo que el que la creó. Por lo tanto, el alcance de las variables será diferente. Por lo tanto, para proteger estos problemas de alcance de asignación variable, debe declararlos definitivos.

S73417H
fuente
2

Cuando se define una clase interna anónima dentro del cuerpo de un método, todas las variables declaradas finales en el alcance de ese método son accesibles desde la clase interna. Para valores escalares, una vez que se ha asignado, el valor de la variable final no puede cambiar. Para valores de objeto, la referencia no puede cambiar. Esto permite que el compilador de Java "capture" el valor de la variable en tiempo de ejecución y almacene una copia como un campo en la clase interna. Una vez que el método externo ha finalizado y su marco de pila se ha eliminado, la variable original desaparece pero la copia privada de la clase interna persiste en la memoria de la clase.

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

ola_star
fuente
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
fuente
0

Como Jon responde a los detalles de implementación, otra posible respuesta sería que la JVM no quiere manejar el registro escrito que ha finalizado su activación.

Considere el caso de uso donde sus lambdas en lugar de aplicarse, se almacenan en algún lugar y se ejecutan más tarde.

Recuerdo que en Smalltalk obtendrías una tienda ilegal cuando hagas tal modificación.

mathk
fuente
0

Prueba este código,

Cree la Lista de matrices y ponga valor dentro de eso y devuélvalo

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
fuente
OP pregunta por qué se requiere algo. Por lo tanto, debe señalar cómo lo aborda su código
NitinSingh
0

La clase anónima de Java es muy similar al cierre de Javascript, pero Java lo implementa de manera diferente. (verifique la respuesta de Andersen)

Entonces, para no confundir al Desarrollador Java con el comportamiento extraño que podría ocurrir para aquellos que provienen de un fondo Javascript. Supongo que es por eso que nos obligan a usar final, esta no es la limitación de JVM.

Veamos el ejemplo de Javascript a continuación:

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

En Javascript, el countervalor será 100, porque solo hay unocounter variable de principio a fin.

Pero en Java, si no hay final, se imprimirá 0, porque mientras se crea el objeto interno, el 0valor se copia en las propiedades ocultas del objeto de clase interna. (aquí hay dos variables enteras, una en el método local y otra en las propiedades ocultas de la clase interna)

Entonces, cualquier cambio después de la creación del objeto interno (como la línea 1), no afectará al objeto interno. Por lo tanto, creará confusión entre dos resultados y comportamientos diferentes (entre Java y Javascript).

Creo que es por eso que Java decide obligarlo a ser definitivo, por lo que los datos son 'consistentes' desde el principio hasta el final.

GMsoF
fuente
0

Variable final de Java dentro de una clase interna

la clase interna solo puede usar

  1. referencia de clase externa
  2. variables locales finales fuera de alcance que son un tipo de referencia (p. ej. Object ...)
  3. valor (por ejemplo (primitiva) int...) tipo puede ser envuelto por un tipo de referencia final. IntelliJ IDEApuede ayudarlo a convertirlo en una matriz de elementos

Cuando el compilador genera un non static nested( inner class) [Acerca de] - una nueva clase - <OuterClass>$<InnerClass>.classse crea y los parámetros enlazados se pasan al constructor [Variable local en la pila] . Es similar al cierre

La variable final es una variable que no se puede reasignar. La variable de referencia final todavía se puede cambiar modificando un estado

Era posible que fuera extraño porque como programador podrías hacer así

//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
fuente
-2

Quizás este truco te dé una idea

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);
Caprichoso
fuente