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


  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 305

12 ответов:

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


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

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

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

создание переменной 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):

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

Я еще не нашел причину или объяснение на 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 и использовать его там, где вы хотите.

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

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

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

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

public class Test
{
 public final int a = 3;

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

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

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

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

когда анонимный внутренний класс определен в теле метода, все переменные, объявленные final в области этого метода, доступны из внутреннего класса. Для скалярных значений, как только он был назначен, значение конечной переменной не может измениться. Для значений объекта, ссылка не может измениться. Это позволяет компилятору 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;

        }
    });
}

Как Йон имеет ли детали реализации ответ другой возможный ответ будет заключаться в том, что 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;
}

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

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