Почему только конечные переменные доступны в анонимном классе?


355
  1. aможет быть только окончательным здесь. Почему? Как я могу передать aв onClick()метод , не держа его в качестве частного члена?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
    
  2. Как я могу вернуть, 5 * aкогда он нажал? Я имею в виду,

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

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

4
Чего ты пытаешься достичь? Обработчик щелчка может быть выполнен, когда "f" закончен.
Иван Дубров

@Lambert, если вы хотите использовать метод в методе onClick, он должен быть окончательным. @Ivan Как метод f () может вести себя как метод onClick (), возвращая int при щелчке

2
Это то, что я имею в виду - он не поддерживает полное закрытие, потому что не допускает доступ к неконечным переменным.
user541686

4
Примечание.
Начиная

Ответы:


489

Как отмечено в комментариях, часть этого становится неактуальной в Java 8, где это finalможет быть неявным. Однако в анонимном внутреннем классе или лямбда-выражении можно использовать только эффективно окончательную переменную.


В основном это связано с тем, как Java управляет замыканиями .

Когда вы создаете экземпляр анонимного внутреннего класса, любые переменные, которые используются в этом классе, копируются в свои значения через автоматически сгенерированный конструктор. Это избавляет компилятор от необходимости автоматически генерировать различные дополнительные типы для хранения логического состояния «локальных переменных», как, например, это делает компилятор C # ... (Когда C # захватывает переменную в анонимной функции, он действительно захватывает переменную - Закрытие может обновить переменную таким образом, который виден основной частью метода, и наоборот.)

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

Если сделать окончательную переменную, все эти возможности будут удалены - поскольку значение не может быть изменено вообще, вам не нужно беспокоиться о том, будут ли такие изменения видны. Единственный способ позволить методу и анонимному внутреннему классу видеть изменения друг друга - это использовать изменяемый тип некоторого описания. Это может быть сам класс включения, массив, изменяемый тип-обертка ... что-нибудь в этом роде. По сути, это немного похоже на обмен данными между одним методом и другим: изменения, внесенные в параметры одного метода, не отображаются его вызывающей стороной, но видны изменения, внесенные в объекты, на которые ссылаются параметры.

Если вас интересует более подробное сравнение между замыканиями Java и C #, у меня есть статья, которая углубляется в это. Я хотел сосредоточиться на стороне Java в этом ответе :)


4
@Ivan: Как и C #, в основном. Тем не менее, он имеет достаточную степень сложности, если вам нужна такая же функциональность, как в C #, где переменные из разных областей могут быть "созданы" разное количество раз.
Джон Скит


11
Это все было верно для Java 7, имейте в виду, что с Java 8 были введены замыкания, и теперь действительно возможно получить доступ к неконечному полю класса из его внутреннего класса.
Матиас Бадер

22
@MathiasBader: Действительно? Я думал, что это все тот же механизм, компилятор теперь достаточно умен, чтобы сделать вывод final(но он все еще должен быть эффективно окончательным).
Тило

3
@Mathias Bader: вы всегда можете получить доступ к неконечным полям , которые не следует путать с локальными переменными, которые должны быть финальными и все же должны быть эффективно финальными, поэтому Java 8 не меняет семантику.
Хольгер

41

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

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

Однако этот прием не очень хорош из-за проблем с синхронизацией. Если обработчик вызывается позже, вам необходимо: 1) синхронизировать доступ к res, если обработчик был вызван из другого потока; 2) необходимо иметь какой-либо флаг или указатель того, что res был обновлен.

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

// ...

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

// ...

2
спасибо за Ваш ответ. Я знаю все это, и мое решение лучше, чем это. мой вопрос "почему только окончательный"?

6
Ответ таков: как они реализованы :)
Иван Дубров,

1
Спасибо. Я использовал трюк выше самостоятельно. Я не был уверен, если это хорошая идея. Если Java не позволяет этого, то на это может быть веская причина. Ваш ответ уточняет, что мой List.forEachкод безопасен.
RuntimeException

Прочитайте stackoverflow.com/q/12830611/2073130 для хорошего обсуждения обоснования "почему только финал".
января

Есть несколько обходных путей. У меня есть: final int resf = res; Первоначально я использовал массивный подход, но я считаю, что он имеет слишком громоздкий синтаксис. AtomicReference может быть немного медленнее (выделяет объект).
zakmck

17

Анонимный класс - это внутренний класс, и к внутренним классам применяется строгое правило (JLS 8.1.3) :

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

Я еще не нашел причину или объяснение для jls или jvms, но мы знаем, что компилятор создает отдельный файл класса для каждого внутреннего класса, и он должен убедиться, что методы объявлены в этом файле класса ( на уровне байтового кода) по крайней мере, иметь доступ к значениям локальных переменных.

Джона есть полный ответ - я держу этот ответ без восстановления, потому что кто-то может заинтересоваться правилом JLS)


11

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

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

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

Ответ вашего почему:

Локальный экземпляр внутреннего класса привязан к классу Main и может обращаться к последним локальным переменным содержащего его метода. Когда экземпляр использует последний локальный элемент содержащего его метода, переменная сохраняет значение, которое оно содержало во время создания экземпляра, даже если переменная вышла из области видимости (это, по сути, грубая ограниченная версия замыканий Java).

Поскольку локальный внутренний класс не является ни членом класса, ни пакета, он не объявлен с уровнем доступа. (Однако, имейте в виду, что его собственные члены имеют уровни доступа, как в обычном классе.)


Я упомянул, что "не оставляя его в качестве частного члена"

6

Ну, в Java переменная может быть конечной не только как параметр, но и как поле уровня класса, например

public class Test
{
 public final int a = 3;

или как локальная переменная, как

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

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

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

Вы не можете иметь переменную как final и дать ей новое значение. finalозначает только это: значение является неизменным и окончательным.

И поскольку он окончательный, Java может безопасно скопировать его в локальные анонимные классы. Вы не получаете некоторую ссылку на int (тем более что у вас не может быть ссылок на примитивы типа int в Java, только ссылки на объекты ).

Он просто копирует значение a в неявный int, называемый a в вашем анонимном классе.


3
Я ассоциирую «переменную уровня класса» с static. Может быть, это будет более понятно, если вместо этого использовать «переменную экземпляра».
eljenso

1
ну, я использовал уровень класса, потому что техника будет работать как с экземплярами, так и со статическими переменными.
Зак Л

мы уже знаем, что финал доступен, но мы хотим знать, почему? Не могли бы вы добавить еще несколько объяснений, почему сторона?
Саурабх Оза

6

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


Это не так, как это реализовано в таких языках, как C #, которые поддерживают эту функцию. Фактически, компилятор изменяет переменную с локальной переменной на переменную экземпляра или создает дополнительную структуру данных для этих переменных, которая может превзойти область действия внешнего класса. Однако «множественных копий локальных переменных» не существует
Mike76

Mike76 Я не смотрел на реализацию C #, но Scala делает второе, что вы упомянули, я думаю: если объект Intпереназначается внутри замыкания, измените эту переменную на экземпляр IntRef(по сути, изменяемый Integerупаковщик). Каждый доступ к переменной затем переписывается соответственно.
Adowrath

3

Чтобы понять причину этого ограничения, рассмотрите следующую программу:

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

InterfaceInstance остается в памяти после инициализации метод возвращает, но параметр Вэл не делает. JVM не может получить доступ к локальной переменной за пределами своей области, поэтому Java заставляет последующий вызов printInteger работать, копируя значение val в неявное поле с тем же именем в interfaceInstance . InterfaceInstance , как говорят, захватили значение локального параметра. Если параметр не был окончательным (или фактически окончательным), его значение могло бы измениться, не синхронизироваться с зафиксированным значением, что может привести к неинтуитивному поведению.


2

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


2

Когда в теле метода определен анонимный внутренний класс, все переменные, объявленные как final в области действия этого метода, доступны из внутреннего класса. Для скалярных значений, после того, как оно было назначено, значение конечной переменной не может измениться. Для значений объекта ссылка не может быть изменена. Это позволяет компилятору Java «захватывать» значение переменной во время выполнения и сохранять копию как поле во внутреннем классе. После завершения внешнего метода и удаления его стекового фрейма исходная переменная исчезает, но личная копия внутреннего класса сохраняется в собственной памяти класса.

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


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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

0

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

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

Я помню, что в Smalltalk вы бы подняли нелегальный магазин, когда вы сделаете такую ​​модификацию.


0

Попробуйте этот код,

Создайте Array List, поместите в него значение и верните его:

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

ОП спрашивает, почему что-то требуется. Следовательно, вы должны указать, как ваш код решает эту проблему
NitinSingh

0

Java анонимный класс очень похож на закрытие Javascript, но Java реализует это по-другому. (проверьте ответ Андерсена)

Поэтому, чтобы не путать Java-разработчика со странным поведением, которое может возникнуть у тех, кто пришел из Javascript. Я думаю, именно поэтому они заставляют нас использовать final, это не ограничение JVM.

Давайте посмотрим на пример Javascript ниже:

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

В Javascript counterзначение будет 100, потому что есть только одна counterпеременная от начала до конца.

Но в Java, если его нет final, он будет распечатан 0, поскольку во время создания внутреннего объекта 0значение копируется в скрытые свойства объекта внутреннего класса. (здесь есть две целочисленные переменные, одна в локальном методе, другая в скрытых свойствах внутреннего класса)

Поэтому любые изменения после создания внутреннего объекта (например, строка 1) не будут влиять на внутренний объект. Таким образом, это приведет к путанице между двумя различными результатами и поведением (между Java и Javascript).

Я считаю, что именно поэтому Java решает сделать его окончательным, поэтому данные «согласованы» от начала до конца.


0

Конечная переменная Java внутри внутреннего класса

внутренний класс может использовать только

  1. ссылка из внешнего класса
  2. конечные локальные переменные из области видимости, которые являются ссылочным типом (например, Object...)
  3. intТип value (примитив) (например ...) может быть заключен в окончательный ссылочный тип. IntelliJ IDEAможет помочь вам преобразовать его в один массив элементов

Когда a non static nested( inner class) [About] генерируется компилятором - создается новый класс - <OuterClass>$<InnerClass>.classи связанные параметры передаются в конструктор [Local variable on stack] . Это похоже на закрытие

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

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

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

-2

Может быть, этот трюк дает вам идею

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);
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.