Почему локальные переменные должны быть окончательными при доступе из анонимных внутренних классов? - PullRequest
3 голосов
/ 20 января 2012

Мы все знаем, что вы не можете делать такие вещи:

int a = 7;
new Runnable() {
     public void run() {
         System.out.println(a);
     }
}.run();
...

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

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

int[] a = {7};
new Runnable() {
    public void run() {
        System.out.println(a[0]);
    }
}.run();
...

Тогда мы находимся в положении, когда можно безопасно получить доступ из анонимного внутреннего класса идействительно измените это, если мы хотим.Конечно, это может быть сделано только тогда, когда мы действительно изменим a.Насколько я мог видеть, это было бы относительно просто вставить, сработало бы для всех типов и позволило бы изменить a из любого контекста.Конечно, вышеприведенное предложение можно изменить, чтобы использовать синтетические классы-обертки для нескольких значений, или другой подход, который немного более эффективен, но идея та же.Я предполагаю, что есть небольшой удар по производительности, но я сомневаюсь, что он был бы чрезмерным, особенно с потенциалом для дополнительной оптимизации под капотом.Помимо, возможно, определенных рефлексивных вызовов, которые полагаются на то, что искусственные поля ломаются определенным образом, я не вижу много недостатков, но я никогда не слышал, чтобы это серьезно предлагалось!Есть ли причина, почему?

Ответы [ 3 ]

2 голосов
/ 20 января 2012

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

int a = 7;
Runnable r = new Runnable() {
    public void run() {
        a = 5;
    }
};
r.run();
System.out.println(a);

Вы можете ожидать, что напечатает 5 (что на самом деле в C #). Но поскольку была взята только копия , на самом деле было бы напечатано 7 ... если бы это было разрешено, без больших изменений.

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

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

Обратите внимание, что в подходе захвата переменной есть существенные сложности, по крайней мере, с использованием правил C #. Например:

List<Runnable> runnables = new ArrayList<Runnable>();
int outer = 0;
for (int i = 0; i < 10; i++) {
    int inner = 0;
    runnables.add(new Runnable() {
        public void run() {
            outer++;
            inner++;
        }
    });
}

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

1 голос
/ 22 января 2012

Другая проблема, с которой вы столкнулись (и которая может возникнуть в groovy, у которой нет этого ограничения), заключается в том, что не финальные переменные могут изменяться (в противном случае у вас не возникло бы проблем с получением финальных переменных)Если поток всегда видит a = 1 или может иногда видеть a = 2.

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

Кстати: альтернатива, для которой в моей IDE есть автоматическое исправление, - это использовать вместо этого массив из одного.* Хотя это и ужасно, но вряд ли вы случайно напишите анонимный класс, в котором значение может измениться.

0 голосов
/ 20 января 2012

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

...