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

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

12 ответов


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


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

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

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

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

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


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

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

// ...

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

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

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

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


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

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 и использовать его там, где хотите.

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

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

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


Ну, в 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 в вашем анонимном классе.


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


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


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

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


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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

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

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

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


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

Создайте список массивов и поместите значение внутри него и верните его:

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

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

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