Невозможно обратиться к неконечной переменной внутри внутреннего класса, определенного в другом методе.


247

Отредактировано: мне нужно изменить значения нескольких переменных, так как они запускаются несколько раз через таймер. Мне нужно постоянно обновлять значения с каждой итерацией через таймер. Я не могу установить значения final, поскольку это не позволит мне обновить значения, однако я получаю ошибку, которую я описал в начальном вопросе ниже:

Я ранее написал, что ниже:

Я получаю сообщение об ошибке "не может ссылаться на не финальную переменную внутри внутреннего класса, определенного в другом методе".

Это происходит для двойной цены, а цена называется ценой объекта. Ты знаешь, почему у меня такая проблема. Я не понимаю, почему мне нужно иметь окончательную декларацию. Также, если вы видите, что я пытаюсь сделать, что мне нужно сделать, чтобы обойти эту проблему.

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

Я спрашиваю, как получить переменную в таймере, которую я могу постоянно обновлять.
Анкур

1
@ Анкур: простой ответ - «Нет». Но вы можете достичь желаемого эффекта, используя внутренний класс; смотрите ответ @ petercardona.
Стивен С

Ответы:


197

Java не поддерживает истинные замыкания , хотя использование анонимного класса, как вы используете здесь ( new TimerTask() { ... }), выглядит как своего рода замыкание.

изменить - см. комментарии ниже - следующее не является правильным объяснением, как указывает KeeperOfTheSoul.

Вот почему это не работает:

Переменные lastPriceи цена являются локальными переменными в методе main (). Объект, который вы создаете с помощью анонимного класса, может длиться до тех пор, пока main()метод не вернется.

Когда main()метод вернется, локальные переменные (такие как lastPriceи price) будут удалены из стека, поэтому они больше не будут существовать после main()возврата.

Но анонимный объект класса ссылается на эти переменные. Все пошло бы ужасно неправильно, если бы объект анонимного класса попытался получить доступ к переменным после того, как они были очищены.

Делая lastPriceи price final, они уже не переменные, а константы. Затем компилятор может просто заменить использование lastPriceи priceв анонимном классе значениями констант (конечно, во время компиляции), и у вас больше не будет проблем с доступом к несуществующим переменным.

Другие языки программирования, которые поддерживают замыкания, делают это путем специальной обработки этих переменных - следя за тем, чтобы они не уничтожались при завершении метода, чтобы замыкание все еще могло обращаться к переменным.

@ Анкур: Вы могли бы сделать это:

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
Не совсем верно, Java генерирует захваты для рассматриваемых переменных для захвата их значений времени выполнения, просто они хотели избежать странного побочного эффекта, который возможен в .Net, где путем захвата значения в делегате измените значение во внешнем методе, и теперь делегат видит новое значение, см. stackoverflow.com/questions/271440/c-captured-variable-in-loop для примера C # этого поведения, которого Java стремится избежать.
Крис Чилверс

14
Это не «странный побочный эффект», это нормальное поведение, которое люди ожидают, и которое Java не может предоставить, потому что не генерирует захваты. В качестве обходного пути локальные переменные, используемые в анонимном классе, должны быть окончательными.
Майкл Боргвардт

12
Джеспер, тебе, вероятно, следует отредактировать неправильные части своих ответов, а не просто получить сообщение о том, что вышеупомянутое неверно.
Джеймс МакМэхон

19
На самом деле Java не поддерживает замыкания. Языки, которые поддерживают замыкания, делают это, сохраняя всю локальную среду (то есть набор локальных переменных, определенных в текущем кадре стека) как объект кучи. Java не имеет поддержки для этого (разработчики языка хотели реализовать его, но у него не хватило времени), поэтому в качестве обходного пути, когда создается экземпляр локального класса, значения любых локальных переменных, на которые он ссылается, копируются в кучу. , Однако в этом случае JVM не может синхронизировать значения с локальными переменными, поэтому они должны быть окончательными.
Таймон

64
Этот ответ совершенно сбивает с толку, так как никто по имени KeeperOfTheSoul не прокомментировал это. Ответ должен быть пересмотрен.
Адам Паркин

32

Чтобы избежать странных побочных эффектов с замыканиями в java-переменных, на которые ссылается анонимный делегат, они должны быть помечены как окончательные, поэтому для ссылки lastPriceи цены в задаче таймера их необходимо пометить как окончательные.

Это, очевидно, не будет работать для вас, потому что вы хотите изменить их, в этом случае вы должны смотреть на их инкапсуляцию в классе.

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

Теперь просто создайте новый Foo как final и вызовите .tick из таймера.

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
или вы можете просто привести Foo реализовать Runnable ..?
Виджет

18

Вы можете получить доступ только к конечным переменным из содержащего класса при использовании анонимного класса. Поэтому вам нужно объявить используемые переменные final (что не подходит для вас, так как вы меняете lastPrice и цену ), или не использовать анонимный класс.

Таким образом, вы можете создать реальный внутренний класс, в котором вы можете передавать переменные и использовать их обычным способом.

или:

Существует быстрый (и, на мой взгляд, некрасивый) хак для вашей переменной lastPrice и цены, который должен объявить это так

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

и в своем анонимном классе вы можете установить значение, как это

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

14

Хорошие объяснения того, почему вы не можете делать то, что вы пытаетесь сделать, уже предоставлены. В качестве решения, возможно, рассмотрим:

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

Похоже, что вы могли бы сделать лучший дизайн, чем это, но идея в том, что вы можете сгруппировать обновленные переменные внутри ссылки на класс, который не изменяется.


11

С анонимными классами вы фактически объявляете «безымянный» вложенный класс. Для вложенных классов компилятор генерирует новый автономный открытый класс с конструктором, который будет принимать все переменные, которые он использует в качестве аргументов (для «именованных» вложенных классов это всегда экземпляр исходного / включающего класса). Это сделано потому, что среда выполнения не имеет понятия о вложенных классах, поэтому необходимо выполнить (автоматическое) преобразование из вложенного в автономный класс.

Возьмите этот код для примера:

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

Это не сработает, потому что это то, что компилятор делает изнутри:

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

Исходный анонимный класс заменяется некоторым автономным классом, который генерирует компилятор (код не точный, но должен дать вам хорошее представление):

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

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

Как вы можете видеть, автономный класс содержит ссылку на общий объект, помните, что все в java передаются по значению, поэтому даже если переменная ссылки 'shared' в EnclosingClass изменяется, экземпляр, на который он указывает, не изменяется и все другие ссылочные переменные, указывающие на него (как, например, в анонимном классе: с вложением $ 1), не будут знать об этом. Это основная причина, по которой компилятор заставляет вас объявлять эти «общие» переменные как окончательные, чтобы этот тип поведения не превращался в уже запущенный код.

Вот что происходит, когда вы используете переменную экземпляра внутри анонимного класса (это то, что вы должны сделать, чтобы решить свою проблему, переместить свою логику в метод «экземпляра» или конструктор класса):

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

Это прекрасно скомпилируется, потому что компилятор изменит код, так что новый сгенерированный класс Enclosing $ 1 будет содержать ссылку на экземпляр EnclosingClass, где он был создан (это только представление, но оно должно помочь вам):

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

Подобным образом, когда ссылочная переменная 'shared' в EnclosingClass переназначается, и это происходит до вызова Thread # run (), вы увидите, что "другой привет" напечатан дважды, потому что теперь вложенная переменная EnclosingClass $ 1 # будет сохранять ссылку объекту класса, в котором он был объявлен, поэтому изменения любого атрибута этого объекта будут видны экземплярам EnclosingClass $ 1.

Для получения дополнительной информации по этому вопросу, вы можете увидеть это отличное сообщение в блоге (не написано мной): http://kevinboone.net/java_inner.html


Что, если локальная переменная 'shared' является изменяемым объектом? Согласно вашему объяснению, объявление 'final' тоже не поможет, верно?
Саквояж

Объявление «shared» как final позволит вам изменить состояние объекта, на которое ссылается финальная переменная, но для этого конкретного примера это не сработает, потому что вы не сможете изменить значение «shared» переменной (которая это то, что хотел OP), вы сможете использовать его внутри анонимных классов, но его значение не изменится (потому что оно объявлено как final). Важно заметить разницу между переменными и фактическими значениями, которые они содержат (которые могут быть примитивами или ссылками на объекты в куче).
Emerino

>>> но это значение не изменится, я думаю, вы упускаете точку, т. е. если конечная ссылочная переменная указывает на изменяемый объект, она все равно может быть обновлена, однако анонимный класс создает поверхностную копию, поэтому изменения отражаются в анонимном класс. Другими словами, состояние синхронизировано, что здесь и нужно. Здесь OP требуется возможность изменять разделяемую переменную (тип примитива), и для достижения этого OP потребуется обернуть значение в изменяемый объект и совместно использовать этот изменяемый объект.
Sactiw

1
Конечно, OP может обернуть необходимое значение в изменяемый объект, объявить переменную как final и использовать ее вместо этого. Однако он может избежать использования дополнительного объекта, объявив переменную как атрибут текущего класса (как указано и объяснено в ответе). Принудительное использование изменяемых объектов (например, использование массивов для изменения значения общей переменной) не является хорошей идеей.
Emerino

7

Когда я сталкиваюсь с этой проблемой, я просто передаю объекты во внутренний класс через конструктор. Если мне нужно передать примитивы или неизменяемые объекты (как в этом случае), нужен класс-обертка.

Изменить: На самом деле я вообще не использую анонимный класс, но правильный подкласс:

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

Вы не можете ссылаться на не финальные переменные, потому что так сказано в спецификации языка Java. Начиная с 8.1.3:
«Любая локальная переменная, формальный параметр метода или параметр обработчика исключений, используемые, но не объявленные во внутреннем классе, должны быть объявлены окончательными». Весь абзац.
Я вижу только часть вашего кода - по моему мнению, планирование изменения локальных переменных - странная идея. Локальные переменные перестают существовать, когда вы выходите из функции. Может быть, статические поля класса были бы лучше?


2

Я просто написал что - то ручкой что - то вдоль намерения авторов . Я обнаружил, что лучше всего позволить конструктору взять все объекты, а затем в вашем реализованном методе использовать эти объекты конструктора.

Тем не менее, если вы пишете универсальный интерфейсный класс, то вам нужно передать объект, а точнее список объектов. Это может быть сделано Object [] или, что еще лучше, Object ... потому что его проще вызвать.

Смотрите мой пример чуть ниже.

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

}

Пожалуйста, смотрите этот пост о Java-замыканиях, которые поддерживают это из коробки: http://mseifed.blogspot.se/2012/09/closure-implementation-for-java-5-6-and.html

Версия 1 поддерживает прохождение нефинальных замыканий с автокастингом:
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

Если вы хотите изменить значение в вызове метода в анонимном классе, это значение на самом деле является Future. Итак, если вы используете Гуава, вы можете написать

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

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

 return myvalue.get();

2

Одно решение, которое я заметил, не упомянуто (если я не пропустил его, если я действительно исправил меня), это использование переменной класса. Побежал в этот вопрос пытается запустить новый поток внутри метода: new Thread(){ Do Something }.

Звонок doSomething()со следующего будет работать. Вам не обязательно объявлять его final, просто нужно изменить область действия переменной, чтобы она не собиралась до внутреннего класса. Это если, конечно, ваш процесс не огромен и изменение области действия может привести к конфликту. Я не хотел делать мою переменную финальной, поскольку она никоим образом не была финальной / постоянной.

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

Если переменная должна быть конечной, не может быть, тогда вы можете присвоить значение переменной другой переменной и сделать THAT окончательным, чтобы вы могли использовать его вместо этого.


1

используйте ClassName.this.variableName для ссылки на не финальную переменную


1

Вы можете просто объявить переменную вне внешнего класса. После этого вы сможете редактировать переменную из внутреннего класса. Иногда я сталкиваюсь с подобными проблемами при кодировании в Android, поэтому я объявляю переменную глобальной, и она работает для меня.


Это на самом деле не отвечает на вопрос ... Это то, почему вы получаете отрицание.
Стюарт Зиглер


0

Основная проблема заключается в том, может ли переменная внутри экземпляра анонимного класса быть разрешена во время выполнения. Не обязательно делать переменную окончательной, если гарантируется, что переменная находится внутри области выполнения. Например, посмотрите две переменные _statusMessage и _statusTextView внутри метода 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

то, что сработало для меня, это просто определить переменную вне этой вашей функции.

Непосредственно перед объявлением основной функции т.е.

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

Это не сработает, вы объявляете переменную экземпляра, чтобы использовать ее, вам нужно создать экземпляр внутри основного метода. Вы должны быть более конкретны или просто добавить статический модификатор в переменную price.
Emerino

0

Объявите переменную как статическую и ссылайтесь на нее в необходимом методе, используя className.variable


Non-static parameter cannot be referenced from a static context
Стивен

Локальные переменные @Shweta и параметры метода не могут быть объявлены как «статические», более того, речь идет о том, как это было реализовано, чтобы классы внутри методов (локальные анонимные классы) могли продолжать доступ к локальным переменным и параметрам метода даже после метода вернул, т.е. он делает их «окончательные» копии и использует их в качестве переменных экземпляра.
Sactiw

0

Просто другое объяснение. Рассмотрим этот пример ниже

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

Здесь вывод будет

m1 завершен

Нить т работает

Нить т работает

Нить т работает

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

Теперь метод m1 () завершается, и мы присваиваем ссылочную переменную o пустому значению. Теперь Outer Class Object подходит для GC, но Inner Class Object все еще существует, у которого есть (Has-A) связь с запущенным объектом Thread. Без существующего объекта класса Outer нет шансов на существующий метод m1 (), а без существующего метода m1 () нет шансов на существование его локальной переменной, но если Inner Class Object использует локальную переменную метода m1 (), то все говорит само за себя ,

Чтобы решить эту проблему, мы должны создать копию локальной переменной, а затем скопировать ее затем в кучу с объектом класса Inner, что java делает только для конечной переменной, потому что они на самом деле не являются переменными, они как константы (все происходит только во время компиляции не во время выполнения).


-1

Чтобы решить вышеуказанную проблему, разные языки принимают разные решения.

для Java решение - то, что мы видим в этой статье.

для C # решение - разрешить побочные эффекты, и захват по ссылке - единственный вариант.

для C ++ 11 решение состоит в том, чтобы позволить программисту принять решение. Они могут выбрать захват по значению или по ссылке. При захвате по значению никаких побочных эффектов не произойдет, поскольку указанная переменная на самом деле отличается. При захвате по ссылке могут возникнуть побочные эффекты, но программист должен это понимать.


-2

Потому что сбивает с толку, если переменная не является окончательной, так как изменения в ней не будут приняты в анонимном классе.

Просто сделайте переменные 'price' и 'lastPrice' final.

-- Редактировать

Упс, и вам также нужно не назначать им, очевидно, в вашей функции. Вам понадобятся новые локальные переменные. В любом случае, я подозреваю, что кто-то уже дал вам лучший ответ.


2
это не просто сбивает с толку - это совершенно неверно, поэтому компилятор не позволяет этого.
Чии

Но тогда как мне изменить значения, когда мне нужно?
Анкур

Не только потому, что это сбивает с толку; это потому, что Java не поддерживает замыкания. Смотрите мой ответ ниже. @Ankur: Вы можете сделать переменные-члены переменными объекта анонимного класса вместо локальных переменных в main ().
Джеспер

Он модифицирует их, поэтому они не могут быть окончательными.
Робин

Если бы цена и lastPrice были окончательными, назначения для них не компилировались бы.
Грег Мэттс
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.