Почему в анонимном классе доступны только конечные переменные? - PullRequest
333 голосов
/ 19 января 2011
  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 
            }
        });
    }
    

Ответы [ 13 ]

466 голосов
/ 19 января 2011

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


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

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

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

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

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

42 голосов
/ 19 января 2011

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

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

// ...
17 голосов
/ 19 января 2011

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

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

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

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

10 голосов
/ 19 января 2011

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

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

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

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

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

6 голосов
/ 01 сентября 2013

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

6 голосов
/ 19 января 2011

Ну, в 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, просто ссылки на Objects ).

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

2 голосов
/ 10 ноября 2018

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

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 захватило значение локального параметра.Если параметр не является окончательным (или фактически окончательным), его значение может измениться, не синхронизироваться с зафиксированным значением, что может привести к неинтуитивному поведению.

2 голосов
/ 17 февраля 2015

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

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

2 голосов
/ 19 января 2011

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

1 голос
/ 28 августа 2011
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}
...