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

  • 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;
    
            }
        });
    }
    
  • Как я могу вернуть 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 8, где final может быть неявным. Только эффективная конечная переменная может использоваться в анонимном внутреннем классе или лямбда-выражении.


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

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

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

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

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

Ответ 2

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

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

// ...

Ответ 3

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

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

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

(У Джона есть полный ответ - я сохраняю это один, потому что его может заинтересовать правило JLS)

Ответ 4

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

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

Ответ вашего вопроса:

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

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

Ответ 5

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

public class Test
{
 public final int a = 3;

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

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

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

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

Ответ 9

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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

Ответ 10

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

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

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

Ответ 11

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

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

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

Ответ 12

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

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 остается в памяти после возврата из метода initialize, а параметр val - нет. JVM не может получить доступ к локальной переменной за пределами своей области, поэтому Java заставляет последующий вызов printInteger работать, копируя значение val в неявное поле с тем же именем в interfaceInstance. Говорят, что interfaceInstance зафиксировал значение локального параметра. Если параметр не является окончательным (или фактически конечным), его значение может измениться, не синхронизироваться с зафиксированным значением, что может привести к неинтуитивному поведению.

Ответ 13

Возможно, этот трюк дает идею

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