No se puede hacer referencia a una variable no final dentro de una clase interna definida en un método diferente


247

Editado: necesito cambiar los valores de varias variables, ya que se ejecutan varias veces a través de un temporizador. Necesito seguir actualizando los valores con cada iteración a través del temporizador. No puedo establecer los valores como finales ya que eso me impedirá actualizar los valores, sin embargo, recibo el error que describo en la pregunta inicial a continuación:

Anteriormente había escrito lo que está debajo:

Recibo el error "no puede referirse a una variable no final dentro de una clase interna definida en un método diferente".

Esto está sucediendo para el precio llamado doble y el precio llamado precioObjeto. ¿Sabes por qué me sale este problema? No entiendo por qué necesito tener una declaración final. Además, si puede ver qué es lo que estoy tratando de hacer, qué debo hacer para solucionar este problema.

public static void main(String args[]) {

    int period = 2000;
    int delay = 2000;

    double lastPrice = 0;
    Price priceObject = new Price();
    double price = 0;

    Timer timer = new Timer();

    timer.scheduleAtFixedRate(new TimerTask() {
        public void run() {
            price = priceObject.getNextPrice(lastPrice);
            System.out.println();
            lastPrice = price;
        }
    }, delay, period);
}

Lo que pregunto es cómo puedo obtener una variable en un temporizador que pueda actualizar continuamente.
Ankur

1
@Ankur: la respuesta simple es "No". Pero puede lograr el efecto deseado utilizando una clase interna; ver la respuesta de @ petercardona.
Stephen C

Respuestas:


197

Java no admite cierres verdaderos , aunque usar una clase anónima como la que está usando aquí ( new TimerTask() { ... }) parece una especie de cierre.

editar - Vea los comentarios a continuación - la siguiente no es una explicación correcta, como señala KeeperOfTheSoul.

Por eso no funciona:

Las variables lastPricey el precio son variables locales en el método main (). El objeto que cree con la clase anónima puede durar hasta main()que regrese el método.

Cuando el main()método regrese, las variables locales (como lastPricey price) se limpiarán de la pila, por lo que ya no existirán después de los main()retornos.

Pero el objeto de clase anónimo hace referencia a estas variables. Las cosas saldrían terriblemente mal si el objeto de clase anónimo intenta acceder a las variables después de que se hayan limpiado.

Al hacer lastPricey price final, ya no son realmente variables, sino constantes. El compilador simplemente puede reemplazar el uso de lastPricey priceen la clase anónima con los valores de las constantes (en tiempo de compilación, por supuesto), y ya no tendrá el problema de acceder a variables inexistentes.

Otros lenguajes de programación que admiten cierres lo hacen tratando especialmente esas variables, asegurándose de que no se destruyan cuando finalice el método, de modo que el cierre aún pueda acceder a las variables.

@Ankur: Puedes hacer esto:

public static void main(String args[]) {
    int period = 2000;
    int delay = 2000;

    Timer timer = new Timer();

    timer.scheduleAtFixedRate(new TimerTask() {
        // Variables as member variables instead of local variables in main()
        private double lastPrice = 0;
        private Price priceObject = new Price();
        private double price = 0;

        public void run() {
            price = priceObject.getNextPrice(lastPrice);
            System.out.println();
            lastPrice = price;
        }
    }, delay, period);      
}

34
No es del todo cierto, Java genera capturas para las variables en cuestión para capturar sus valores de tiempo de ejecución, es solo que querían evitar un extraño efecto secundario que es posible en .Net donde al capturar el valor en el delegado, cambie el valor en el método externo, y ahora el delegado ve el nuevo valor, consulte stackoverflow.com/questions/271440/c-captured-variable-in-loop para ver el ejemplo de C # de este comportamiento que Java pretende evitar.
Chris Chilvers

14
Ese no es un "efecto secundario extraño", es el comportamiento normal que la gente esperaría, y que Java no puede entregar porque no genera capturas. Como solución alternativa, las variables locales utilizadas en una clase anónima deben ser finales.
Michael Borgwardt

12
Jesper, probablemente deberías editar las partes incorrectas de tus respuestas en lugar de solo tener un mensaje diciendo que lo anterior es incorrecto.
James McMahon

19
Java, de hecho, no admite cierres. Los lenguajes que admiten cierres lo hacen almacenando todo el entorno local (es decir, el conjunto de variables locales definidas en el marco de pila actual) como un objeto de montón. Java no tiene soporte para esto (los diseñadores del lenguaje querían implementarlo pero se les acabó el tiempo), por lo que, como solución alternativa, cada vez que se crea una instancia de una clase local, los valores de las variables locales a las que hace referencia se copian en el montón . Sin embargo, la JVM no puede mantener los valores sincronizados con las variables locales, por lo que deben ser finales.
Taymon

64
Esta respuesta es completamente confusa ahora que no hay nadie llamado "KeeperOfTheSoul" que le haya comentado. La respuesta debe ser revisada.
Adam Parkin

32

Para evitar efectos secundarios extraños con los cierres en las variables de Java a las que hace referencia un delegado anónimo, debe marcarse como final, por lo tanto, para referirse a ellos lastPricey al precio dentro de la tarea del temporizador, deben marcarse como finales.

Obviamente, esto no funcionará para usted porque desea cambiarlos, en este caso debe considerar encapsularlos dentro de una clase.

public class Foo {
    private PriceObject priceObject;
    private double lastPrice;
    private double price;

    public Foo(PriceObject priceObject) {
        this.priceObject = priceObject;
    }

    public void tick() {
        price = priceObject.getNextPrice(lastPrice);
        lastPrice = price;
    }
}

ahora solo crea un nuevo Foo como final y llama .tick desde el temporizador.

public static void main(String args[]){
    int period = 2000;
    int delay = 2000;

    Price priceObject = new Price();
    final Foo foo = new Foo(priceObject);

    Timer timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
        public void run() {
            foo.tick();
        }
    }, delay, period);
}

1
o simplemente puede llevar a Foo a implementar Runnable ...?
vidstige

18

Solo puede acceder a las variables finales de la clase que lo contiene cuando usa una clase anónima. Por lo tanto, debe declarar las variables que se usan como finales (lo cual no es una opción para usted ya que está cambiando lastPrice and price ), o no use una clase anónima.

Por lo tanto, sus opciones son crear una clase interna real, en la que pueda pasar las variables y usarlas de manera normal

o:

Hay un truco rápido (y en mi opinión feo) para su último precio y variable de precio que es declararlo así

final double lastPrice[1];
final double price[1];

y en tu clase anónima puedes establecer el valor así

price[0] = priceObject.getNextPrice(lastPrice[0]);
System.out.println();
lastPrice[0] = price[0];

14

Buenas explicaciones de por qué no puede hacer lo que está tratando de hacer ya proporcionado. Como solución, tal vez considere:

public class foo
{
    static class priceInfo
    {
        public double lastPrice = 0;
        public double price = 0;
        public Price priceObject = new Price ();
    }

    public static void main ( String args[] )
    {

        int period = 2000;
        int delay = 2000;

        final priceInfo pi = new priceInfo ();
        Timer timer = new Timer ();

        timer.scheduleAtFixedRate ( new TimerTask ()
        {
            public void run ()
            {
                pi.price = pi.priceObject.getNextPrice ( pi.lastPrice );
                System.out.println ();
                pi.lastPrice = pi.price;

            }
        }, delay, period );
    }
}

Parece que probablemente podría hacer un mejor diseño que eso, pero la idea es que podría agrupar las variables actualizadas dentro de una referencia de clase que no cambia.


11

Con las clases anónimas, en realidad estás declarando una clase anidada "sin nombre". Para las clases anidadas, el compilador genera una nueva clase pública independiente con un constructor que tomará todas las variables que usa como argumentos (para las clases anidadas "con nombre", esta siempre es una instancia de la clase original / adjunta). Esto se hace porque el entorno de tiempo de ejecución no tiene noción de clases anidadas, por lo que debe haber una conversión (automática) de una clase anidada a una independiente.

Tome este código por ejemplo:

public class EnclosingClass {
    public void someMethod() {
        String shared = "hello"; 
        new Thread() {
            public void run() {
                // this is not valid, won't compile
                System.out.println(shared); // this instance expects shared to point to the reference where the String object "hello" lives in heap
            }
        }.start();

        // change the reference 'shared' points to, with a new value
        shared = "other hello"; 
        System.out.println(shared);
    }
}

Eso no funcionará, porque esto es lo que hace el compilador debajo del capó:

public void someMethod() {
    String shared = "hello"; 
    new EnclosingClass$1(shared).start();

    // change the reference 'shared' points to, with a new value
    shared = "other hello"; 
    System.out.println(shared);
}

La clase anónima original se reemplaza por una clase independiente que genera el compilador (el código no es exacto, pero debería darle una buena idea):

public class EnclosingClass$1 extends Thread {
    String shared;
    public EnclosingClass$1(String shared) {
        this.shared = shared;
    }

    public void run() {
        System.out.println(shared);
    }
}

Como puede ver, la clase independiente tiene una referencia al objeto compartido, recuerde que todo en Java es paso por valor, por lo que incluso si la variable de referencia 'compartida' en EnclosingClass cambia, la instancia a la que apunta no se modifica , y todas las demás variables de referencia que lo apuntan (como la de la clase anónima: Incluyendo $ 1), no se darán cuenta de esto. Esta es la razón principal por la que el compilador lo obliga a declarar estas variables 'compartidas' como finales, para que este tipo de comportamiento no se convierta en su código ya en ejecución.

Ahora, esto es lo que sucede cuando usa una variable de instancia dentro de una clase anónima (esto es lo que debe hacer para resolver su problema, mover su lógica a un método de "instancia" o un constructor de una clase):

public class EnclosingClass {
    String shared = "hello";
    public void someMethod() {
        new Thread() {
            public void run() {
                System.out.println(shared); // this is perfectly valid
            }
        }.start();

        // change the reference 'shared' points to, with a new value
        shared = "other hello"; 
        System.out.println(shared);
    }
}

Esto compila bien, porque el compilador modificará el código, de modo que la nueva clase generada Enclosing $ 1 tendrá una referencia a la instancia de EnclosingClass donde se instancia (esto es solo una representación, pero debería ayudarlo):

public void someMethod() {
    new EnclosingClass$1(this).start();

    // change the reference 'shared' points to, with a new value
    shared = "other hello"; 
    System.out.println(shared);
}

public class EnclosingClass$1 extends Thread {
    EnclosingClass enclosing;
    public EnclosingClass$1(EnclosingClass enclosing) {
        this.enclosing = enclosing;
    }

    public void run() {
        System.out.println(enclosing.shared);
    }
}

De esta manera, cuando la variable de referencia 'compartida' en EnclosingClass se reasigna, y esto sucede antes de la llamada a Thread # run (), verá "otro hola" impreso dos veces, porque ahora la variable de encierro EnclosingClass $ 1 # mantendrá una referencia al objeto de la clase donde se declaró, por lo que los cambios en cualquier atributo de ese objeto serán visibles para las instancias de EnclosingClass $ 1.

Para obtener más información sobre el tema, puede ver esta excelente publicación de blog (no escrita por mí): http://kevinboone.net/java_inner.html


¿Qué pasa si la variable local 'compartida' es un objeto mutable? Según su explicación, declarar 'final' tampoco ayudará, ¿verdad?
Sactiw

Declarar "compartido" como final le permitirá modificar el estado del objeto al que hace referencia la variable final, pero para este ejemplo en particular eso no funcionará porque no podrá cambiar el valor de la variable "compartida" (que es lo que quería el OP), podrá usarlo dentro de clases anónimas, pero su valor no cambiará (porque se declara final). Es importante notar la diferencia entre las variables y los valores reales que contienen (que pueden ser primitivas o referencias a objetos en el montón).
emerino

>>> pero su valor no cambiará . Supongo que le falta el punto, es decir, si la variable de referencia final apunta a un objeto mutable, aún puede actualizarse, sin embargo, la clase anónima crea la copia superficial, por lo que los cambios se reflejan en el anónimo. clase. En otras palabras, el estado está sincronizado, que es lo que se desea aquí. Aquí, OP necesita capacidad para modificar la variable compartida (el tipo primitivo) y para lograr que OP necesitará ajustar el valor debajo de un objeto mutable y compartir ese objeto mutable.
sactiw

1
Por supuesto, el OP puede ajustar el valor necesario bajo un objeto mutable, declarar la variable como final y usarla en su lugar. Sin embargo, puede evitar el uso de un objeto adicional declarando la variable como un atributo de la clase actual (como se señala y explica en la respuesta). Forzar objetos mutables (como usar matrices solo para poder modificar el valor de una variable compartida) no es una buena idea.
emerino

7

Cuando me encuentro con este problema, simplemente paso los objetos a la clase interna a través del constructor. Si necesito pasar primitivas u objetos inmutables (como en este caso), se necesita una clase contenedora.

Editar: En realidad, no uso una clase anónima en absoluto, sino una subclase adecuada:

public class PriceData {
        private double lastPrice = 0;
        private double price = 0;

        public void setlastPrice(double lastPrice) {
            this.lastPrice = lastPrice;
        }

        public double getLastPrice() {
            return lastPrice;
        }

        public void setPrice(double price) {
            this.price = price;
        }

        public double getPrice() {
            return price;
        }
    }

    public class PriceTimerTask extends TimerTask {
        private PriceData priceData;
        private Price priceObject;

        public PriceTimerTask(PriceData priceData, Price priceObject) {
            this.priceData = priceData;
            this.priceObject = priceObject;
        }

        public void run() {
            priceData.setPrice(priceObject.getNextPrice(lastPrice));
            System.out.println();
            priceData.setLastPrice(priceData.getPrice());

        }
    }

    public static void main(String args[]) {

        int period = 2000;
        int delay = 2000;

        PriceData priceData = new PriceData();
        Price priceObject = new Price();

        Timer timer = new Timer();

        timer.scheduleAtFixedRate(new PriceTimerTask(priceData, priceObject), delay, period);
    }

2

No puede hacer referencia a variables no finales porque la especificación del lenguaje Java lo dice. De 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". Párrafo entero.
Solo puedo ver una parte de su código; según yo, la modificación de la programación de variables locales es una idea extraña. Las variables locales dejan de existir cuando abandonas la función. ¿Quizás los campos estáticos de una clase serían mejores?


2

Acabo de escribir algo para manejar algo según la intención de los autores . Encontré que lo mejor que podía hacer era dejar que el constructor tomara todos los objetos y luego, en su método implementado, usara esos objetos constructores.

Sin embargo, si está escribiendo una clase de interfaz genérica, debe pasar un Objeto, o mejor, una lista de Objetos. Esto podría ser hecho por Object [] o incluso mejor, Object ... porque es más fácil de llamar.

Vea mi pieza de ejemplo justo debajo.

List<String> lst = new ArrayList<String>();
lst.add("1");
lst.add("2");        

SomeAbstractClass p = new SomeAbstractClass (lst, "another parameter", 20, true) {            

    public void perform( ) {                           
        ArrayList<String> lst = (ArrayList<String>)getArgs()[0];                        
    }

};

public abstract class SomeAbstractClass{    
    private Object[] args;

    public SomeAbstractClass(Object ... args) {
        this.args = args;           
    }      

    public abstract void perform();        

    public Object[] getArgs() {
        return args;
    }

}

Consulte esta publicación sobre cierres de Java que admite esto de forma inmediata: http://mseifed.blogspot.se/2012/09/closure-implementation-for-java-5-6-and.html

La versión 1 admite la aprobación de cierres no finales con transmisión automática:
https://github.com/MSeifeddo/Closure-implementation-for-Java-5-6-and-7/blob/master/org/mo/closure/v1/ Closure.java

    SortedSet<String> sortedNames = new TreeSet<String>();
    // NOTE! Instead of enforcing final, we pass it through the constructor
    eachLine(randomFile0, new V1<String>(sortedNames) {
        public void call(String line) {
            SortedSet<String> sortedNames = castFirst();  // Read contructor arg zero, and auto cast it
            sortedNames.add(extractName(line));
        }
    });

2

Si desea cambiar un valor en una llamada de método dentro de una clase anónima, ese "valor" es en realidad un Future. Entonces, si usas guayaba, puedes escribir

...
final SettableFuture<Integer> myvalue = SettableFuture<Integer>.create();
...
someclass.run(new Runnable(){

    public void run(){
        ...
        myvalue.set(value);
        ...
    }
 }

 return myvalue.get();

2

Una solución que he notado no se menciona (a menos que me la haya perdido, si lo hice, corríjame), es el uso de una variable de clase. Encontré este problema al intentar ejecutar un nuevo hilo dentro de un método:new Thread(){ Do Something } .

Llamar doSomething()desde lo siguiente funcionará. No necesariamente tiene que declararlo final, solo necesita cambiar el alcance de la variable para que no se recopile antes de la clase interna. Esto es a menos, por supuesto, que su proceso sea enorme y que cambiar el alcance pueda crear algún tipo de conflicto. No quería hacer mi variable final ya que de ninguna manera era una final / constante.

public class Test
{

    protected String var1;
    protected String var2;

    public void doSomething()
    {
        new Thread()
        {
            public void run()
            {
                System.out.println("In Thread variable 1: " + var1);
                System.out.println("In Thread variable 2: " + var2);
            }
        }.start();
    }

}

1

Si la variable requiere ser final, no puede serlo, entonces puede asignar el valor de la variable a otra variable y hacer ESO final para que pueda usarla en su lugar.



1

simplemente puede declarar la variable fuera de la clase externa. Después de esto, podrá editar la variable desde dentro de la clase interna. A veces me enfrento a problemas similares al codificar en Android, por lo que declaro la variable como global y funciona para mí.


Esto realmente no responde la pregunta ... Es por eso que te votan negativamente.
Stuart Siegler

0

¿Puedes hacer lastPrice, priceObjecty pricecampos de la clase interna anónima?


0

La principal preocupación es si una variable dentro de la instancia de clase anónima se puede resolver en tiempo de ejecución. No es imprescindible hacer que una variable sea final siempre que se garantice que la variable está dentro del alcance del tiempo de ejecución. Por ejemplo, vea las dos variables _statusMessage y _statusTextView dentro del método updateStatus ().

public class WorkerService extends Service {

Worker _worker;
ExecutorService _executorService;
ScheduledExecutorService _scheduledStopService;

TextView _statusTextView;


@Override
public void onCreate() {
    _worker = new Worker(this);
    _worker.monitorGpsInBackground();

    // To get a thread pool service containing merely one thread
    _executorService = Executors.newSingleThreadExecutor();

    // schedule something to run in the future
    _scheduledStopService = Executors.newSingleThreadScheduledExecutor();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {

    ServiceRunnable runnable = new ServiceRunnable(this, startId);
    _executorService.execute(runnable);

    // the return value tells what the OS should
    // do if this service is killed for resource reasons
    // 1. START_STICKY: the OS restarts the service when resources become
    // available by passing a null intent to onStartCommand
    // 2. START_REDELIVER_INTENT: the OS restarts the service when resources
    // become available by passing the last intent that was passed to the
    // service before it was killed to onStartCommand
    // 3. START_NOT_STICKY: just wait for next call to startService, no
    // auto-restart
    return Service.START_NOT_STICKY;
}

@Override
public void onDestroy() {
    _worker.stopGpsMonitoring();
}

@Override
public IBinder onBind(Intent intent) {
    return null;
}

class ServiceRunnable implements Runnable {

    WorkerService _theService;
    int _startId;
    String _statusMessage;

    public ServiceRunnable(WorkerService theService, int startId) {
        _theService = theService;
        _startId = startId;
    }

    @Override
    public void run() {

        _statusTextView = MyActivity.getActivityStatusView();

        // get most recently available location as a latitude /
        // longtitude
        Location location = _worker.getLocation();
        updateStatus("Starting");

        // convert lat/lng to a human-readable address
        String address = _worker.reverseGeocode(location);
        updateStatus("Reverse geocoding");

        // Write the location and address out to a file
        _worker.save(location, address, "ResponsiveUx.out");
        updateStatus("Done");

        DelayedStopRequest stopRequest = new DelayedStopRequest(_theService, _startId);

        // schedule a stopRequest after 10 seconds
        _theService._scheduledStopService.schedule(stopRequest, 10, TimeUnit.SECONDS);
    }

    void updateStatus(String message) {
        _statusMessage = message;

        if (_statusTextView != null) {
            _statusTextView.post(new Runnable() {

                @Override
                public void run() {
                    _statusTextView.setText(_statusMessage);

                }

            });
        }
    }

}

0

lo que funcionó para mí es solo definir la variable fuera de esta función tuya.

Justo antes de declarar la función principal, es decir

Double price;
public static void main(String []args(){
--------
--------
}

Eso no funcionará, está declarando una variable de instancia, para usarla, deberá crear una instancia dentro de su método principal. Debería ser más específico o simplemente agregar el modificador estático a la variable 'precio'.
emerino

0

Declare la variable como estática y haga referencia en el método requerido usando className.variable


Non-static parameter cannot be referenced from a static context
Stephen

@Shweta Las variables locales y los parámetros del método no pueden declararse 'estáticos', además, se trata de la forma en que se ha implementado para permitir que las clases dentro de los métodos (clases anónimas locales) continúen el acceso a las variables locales y los parámetros del método incluso después del método. ha regresado, es decir, hace sus copias 'finales' y las usa como variables de instancia.
Sactiw

0

Solo otra explicación. Considere este ejemplo a continuación

public class Outer{
     public static void main(String[] args){
         Outer o = new Outer();
         o.m1();        
         o=null;
     }
     public void m1(){
         //int x = 10;
         class Inner{
             Thread t = new Thread(new Runnable(){
                 public void run(){
                     for(int i=0;i<10;i++){
                         try{
                             Thread.sleep(2000);                            
                         }catch(InterruptedException e){
                             //handle InterruptedException e
                         }
                         System.out.println("Thread t running");                             
                     }
                 }
             });
         }
         new Inner().t.start();
         System.out.println("m1 Completes");
    }
}

Aquí la salida será

m1 completa

Hilo t corriendo

Hilo t corriendo

Hilo t corriendo

................

Ahora el método m1 () se completa y asignamos la variable de referencia o a nulo, Now Outer Class Object es elegible para GC pero Inner Class Object todavía existe quien tiene una relación (Has-A) con el objeto Thread que se está ejecutando. Sin el objeto de clase externa existente no hay posibilidad de que exista el método m1 () y sin el método de m1 () existente no hay posibilidad de que exista su variable local, pero si el objeto de clase interna usa la variable local del método m1 () entonces todo se explica por sí mismo .

Para resolver esto, tenemos que crear una copia de la variable local y luego copiar en el montón con el objeto de clase Inner, lo que Java hace solo para la variable final porque en realidad no son variables, son como constantes (todo sucede solo en tiempo de compilación no en tiempo de ejecución).


-1

Para resolver el problema anterior, diferentes idiomas toman decisiones diferentes.

para Java, la solución es la que vemos en este artículo.

para C #, la solución es permitir efectos secundarios y la captura por referencia es la única opción.

para C ++ 11, la solución es permitir que el programador tome la decisión. Pueden elegir capturar por valor o por referencia. Si se captura por valor, no se producirán efectos secundarios porque la variable referenciada es realmente diferente. Si se captura por referencia, pueden ocurrir efectos secundarios, pero el programador debe darse cuenta.


-2

Porque es confuso si la variable no es final, ya que los cambios no se recogerán en la clase anónima.

Simplemente haga que las variables 'precio' y 'lastPrice' sean finales.

- Editar

Vaya, y también necesitarás no asignarles, obviamente, en tu función. Necesitarás nuevas variables locales. De todos modos, sospecho que alguien ya te ha dado una mejor respuesta.


2
no solo es confuso, es francamente incorrecto, por lo tanto, el compilador no lo permite.
Chii

Pero entonces, ¿cómo cambio los valores cuando lo necesito?
Ankur

No solo porque es confuso; Esto se debe a que Java no admite cierres. Vea mi respuesta a continuación. @Ankur: Puede hacer que las variables sean miembros variables del objeto de clase anónimo en lugar de variables locales en main ().
Jesper

Él los está modificando, por lo que no pueden ser definitivos.
Robin

Si price y lastPrice fueran finales, las asignaciones a ellos no se compilarían.
Greg Mattes
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.