Как можно использовать неконечные поля в классе анонимного класса, если их значение может измениться?

Я уже задавал этот вопрос, но не получил адекватного ответа.

Как можно использовать неконечные поля в классе анонимного класса, если их значение может меняться?

class Foo{
    private int i;
    void bar(){
        i = 10
        Runnable runnable = new Runnable (){
            public void run (){
                System.out.println(i); //works fine
            }//end method run
        }//end Runnable
    }//end method bar
}//end class Foo 

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

До:

public class Access1 {
  public void f() {
    final int i = 3;
    Runnable runnable = new Runnable() {
        public void run() {
            System.out.println(i);
        }//end method run
    };//end anonymous class
  }//end method f
}//end class Access1

После:

public class Access1 {
    public Access1() {}//end constructor

    public void f() {
        Access1$1 access1$1 = new Access1$1(this);
    }//end method f
}//end class Access1

И

class Access1$1 implements Runnable {
    Access1$1(Access1 access1) {
        this$0 = access1;
    }//end constructor

    public void run() {
        System.out.println(3);
    }//end method run
    private final Access1 this$0;
}//end class Access1$1

Тогда как компилятор может встроить значение не конечного поля?


person kzidane    schedule 12.10.2013    source источник
comment
Компилятор не может встраивать поля, которые не являются константами времени компиляции. Вы уверены, что декомпилировали правильный код?   -  person Joni    schedule 12.10.2013
comment
@Joni Я не встраиваю значение поля выше. Я просто сравниваю встроенное значение локальной переменной и то, как анонимный класс использует нефинальное значение поля (чего я еще не знаю).   -  person kzidane    schedule 12.10.2013


Ответы (1)


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

Внутренний класс имеет доступ к содержащему экземпляру и ко всем членам этого экземпляра, final или нет. Нет необходимости в том, чтобы они были окончательными, потому что на них ссылаются (в вашем случае) Foo.this. Таким образом, при доступе к вашему члену i внутренний класс действительно обращается к Foo.this.i, просто Foo.this (например, this) может подразумеваться, если без него ссылка недвусмысленна.

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

Давайте посмотрим, как это делается:

public class InnerEx {
    public static final void main(String[] args) {
        new InnerEx().test("hi");
    }

    private void test(String arg) {
        final String localVar = arg;

        Runnable r = new Runnable() {
            public void run() {
                System.out.println(localVar);
            }
        };
        r.run();
    }
}

При компиляции получаем InnerEx.class и InnerEx$1.class. Если мы декомпилируем InnerEx$1.class, мы увидим это:

class InnerEx$1 implements java.lang.Runnable {
  final java.lang.String val$localVar;

  final InnerEx this$0;

  InnerEx$1(InnerEx, java.lang.String);
    Code:
       0: aload_0       
       1: aload_1       
       2: putfield      #1                  // Field this$0:LInnerEx;
       5: aload_0       
       6: aload_2       
       7: putfield      #2                  // Field val$localVar:Ljava/lang/String;
      10: aload_0       
      11: invokespecial #3                  // Method java/lang/Object."<init>":()V
      14: return        

  public void run();
    Code:
       0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0       
       4: getfield      #2                  // Field val$localVar:Ljava/lang/String;
       7: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      10: return        
}

Обратите внимание на член экземпляра с именем val$localVar, который является членом экземпляра, созданным для замены локальной переменной в вызове InnerEx#test.

person T.J. Crowder    schedule 12.10.2013
comment
Итак, в моем случае анонимный класс, который я создал внутри класса «Foo», может ссылаться на частное поле «i» без каких-либо ограничений, используя код Foo.this.i? Учитывая, что анонимный класс отделен в другом файле, а ссылка на него используется внутри окружающего класса (как в случае с кодом, который я написал в своем вопросе), не должны ли частные члены закрывающего класса НЕ быть доступными для анонимного класс (который стал отдельным файлом и имеет ссылку на его объемлющий класс)? Извините, я знаю, что это немного сбивает с толку :D - person kzidane; 12.10.2013
comment
@KareemMesbah: Итак... анонимный класс, который я создал внутри класса 'Foo', может ссылаться на приватное поле 'i' без каких-либо ограничений, используя код Foo.this.i? Да, но Foo.this. часть этого не является обязательной. i и Foo.this.i - это одно и то же (вам нужна только часть Foo.this., если i сама по себе неоднозначна для компилятора). Учитывая, что анонимный класс создается в другом файле... Нет, это не так, анонимный класс создается в файле Foo.java, прямо в вашем методе bar. Он реализует интерфейс, определенный в другом месте, но класс определен внутри Foo. - person T.J. Crowder; 12.10.2013
comment
Не могли бы вы проверить раздел: окончательные локальные переменные на goo.gl/xHqksx? это то, что я имел в виду под анонимным классом, разделенным в другом файле. Разве это не правда? - person kzidane; 12.10.2013
comment
@KareemMesbah: я не перехожу по какой-то случайной слепой ссылке за пределами сайта. Если вы имеете в виду раздел в JLS или одно из руководств по Java, обратитесь к нему напрямую. Также рекомендуется цитировать то, что вы имеете в виду. Но опять же: нет, анонимный класс создается в вашем Foo.java, внутри вашего bar метода, где вы говорите = new Runnable() { ... };. - person T.J. Crowder; 12.10.2013
comment
Я имею в виду часть моего вопроса от слова До — классы Access1 и Access1$1. Разве это не то, что мы получаем после декомпиляции с помощью JAD? - person kzidane; 12.10.2013
comment
@KareemMesbah: Опять же, существует большая разница между членом экземпляра объекта (i в вашем Foo) и локальной переменной в вызове метода (i, локальная переменная в методе foo Access1). - person T.J. Crowder; 12.10.2013
comment
Извините, но я снова знаю, что есть разница. Я просто пытаюсь понять. Таким образом, ваш ответ другими словами должен быть таким: потому что мы можем ссылаться на поле, используя ссылку на охватывающий класс (неявно или явно), но мы не можем получить доступ к локальной переменной, используя ссылку охватывающего класса, и поэтому компилятор требует локального переменные должны быть окончательными, чтобы встроить их значения, используя ссылку на объемлющий класс для доступа к его полям. Я правильно понимаю? - person kzidane; 12.10.2013
comment
@KareemMesbah: По существу. Компилятор не встраивает значения конечных локальных переменных; вместо этого (и это гораздо более удивительно) он создает члены-экземпляры в анонимном классе. Я добавил к ответу, чтобы показать это. - person T.J. Crowder; 12.10.2013
comment
Разве нельзя просто создать члены экземпляра в анонимном классе и инициализировать их значениями локальных переменных, не требуя, чтобы локальные переменные были окончательными? - person kzidane; 12.10.2013
comment
@KareemMesbah: Это могло быть разработано таким образом, но тогда люди действительно запутались бы, потому что у нас есть две независимые вещи: локальная переменная и член внутреннего класса с (фактически) одним и тем же именем. Изменение одного не изменит другого. Представьте себе возникающие ошибки, особенно. если вы думаете о взаимодействии нескольких внутренних классов... Итак, они выбрали final, поскольку он стирает весь этот класс проблем. - person T.J. Crowder; 12.10.2013
comment
@KareemMesbah: Не за что — я многому научился, отвечая на этот вопрос, что всегда весело! - person T.J. Crowder; 12.10.2013