Чому доступні лише остаточні змінні в анонімному класі?


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

1
Я не думаю, що анонімні класи Java надають такий вид лямбда-закриття, якого ви очікували, але хтось, будь ласка, виправте мене, якщо я помиляюся ...
user541686,

4
Чого ви намагаєтесь досягти? Обробник клацань може бути виконаний, коли "f" закінчено.
Іван Дубров

@Lambert, якщо ви хочете використовувати метод onClick, він повинен бути остаточним @Ivan, як може метод f () поводитись як метод onClick () повертати int при натисканні

2
Це те, що я маю на увазі - він не підтримує повне закриття, оскільки не дозволяє отримати доступ до не остаточних змінних.
користувач541686

4
Зауважте: що стосується Java 8, ваша змінна повинна бути фактично остаточною
Пітер Лорі

Відповіді:


489

Як зазначається в коментарях, дещо з цього стає неважливим у Java 8, де це finalможе бути неявним. В анонімному внутрішньому класі або лямбда-виразі можна використовувати лише ефективно остаточну змінну.


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

Коли ви створюєте екземпляр анонімного внутрішнього класу, будь-які змінні, які використовуються в цьому класі, копіюють свої значення через автогенерований конструктор. Це дозволяє уникнути компілятора, який повинен автогенерувати різні додаткові типи, щоб утримувати логічний стан "локальних змінних", як, наприклад, компілятор C # ... (Коли C # захоплює змінну в анонімну функцію, вона дійсно фіксує змінну - Закриття може оновлювати змінну таким чином, як це бачиться основним елементом методу, і навпаки.)

Оскільки значення було скопійовано в екземпляр анонімного внутрішнього класу, буде дивно, якби змінна могла бути модифікована рештою методу - у вас може виникнути код, який, здається, працює із застарілою змінною ( адже це фактично те, що буде ... ви працювали з копією, зробленою в інший час). Так само, якщо ви могли вносити зміни в анонімний внутрішній клас, розробники можуть очікувати, що ці зміни будуть помітні в тілі методу, що додає.

Здійснення змінної остаточної видаляє всі ці можливості - оскільки значення взагалі неможливо змінити, вам не потрібно турбуватися про те, чи будуть видимі такі зміни. Єдиний спосіб дозволити методу та анонімному внутрішньому класу побачити зміни один одного - це використовувати змінений тип деякого опису. Це може бути сам клас, що додається, масив, тип змінної обгортки ... все подібне. В основному це трохи схоже на спілкування між одним методом та іншим: зміни, внесені до параметрів одного методу, його виклик не бачить, але зміни, внесені до об'єктів, на які посилаються параметри, бачуться.

Якщо вас зацікавило більш детальне порівняння між закриттями Java та C #, у мене є стаття, яка детальніше розглядає її. У цій відповіді я хотів зосередитись на стороні Java :)


4
@Ivan: Як і C #, в основному. Хоча він має досить високу складність, якщо ви хочете такого ж функціоналу, як і C #, де змінні різних областей можуть бути "примірниками" різну кількість разів.
Джон Скіт


11
Це було справедливо для Java 7, майте на увазі, що з Java 8 було введено закриття, і тепер дійсно можна отримати доступ до незакінченого поля класу з його внутрішнього класу.
Матіас Бадер

22
@MathiasBader: Дійсно? Я думав, що це все ще по суті той самий механізм, компілятор зараз досить розумний, щоб зробити висновок final(але він все ще повинен бути ефективно остаточним).
Тіло

3
@Mathias Bader: ви завжди можете отримати доступ до не остаточних полів , які не слід плутати з локальними змінними, які повинні були бути остаточними і все ж повинні бути ефективно остаточними, тому Java 8 не змінює семантику.
Холгер

41

Існує хитрість, яка дозволяє анонімному класу оновлювати дані у зовнішній області.

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

// ...

2
Дякую за вашу відповідь. Я знаю все це, і моє рішення краще, ніж це. моє запитання "чому тільки остаточний"?

6
Відповідь тоді така - як вони реалізуються :)
Іван Дубров

1
Дякую. Я вже використовував трюк вище. Я не був впевнений, чи це гарна ідея. Якщо Java цього не дозволяє, це може бути вагомою причиною. Ваша відповідь пояснює, що мій List.forEachкод у безпеці.
RuntimeException

Прочитайте stackoverflow.com/q/12830611/2073130, щоб добре обговорити обґрунтування "чому лише остаточного".
lcn

існує кілька обхідних шляхів. Шахта є: final int resf = res; Спочатку я використовував підхід масиву, але вважаю, що він має занадто громіздкий синтаксис. AtomicReference, можливо, трохи повільніше (виділяє об’єкт).
zakmck

17

Анонімний клас - це внутрішній клас, і чітке правило застосовується до внутрішніх класів (JLS 8.1.3) :

Будь-який локальний змінний, параметр формального методу або параметр обробника винятків, який використовується, але не оголошений у внутрішньому класі, повинен бути оголошений остаточним . Будь-яка локальна змінна, використана, але не оголошена у внутрішньому класі, повинна бути визначена перед тілом внутрішнього класу .

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

Джона є повна відповідь - я залишаю це невизначеним, оскільки когось може зацікавити правило JLS)


11

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

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

Оскільки локальний внутрішній клас не є членом класу чи пакету, він не оголошується з рівнем доступу. (Будьте зрозумілі, однак, що його члени мають рівні доступу, як у звичайному класі.)


Я згадував, що "не зберігаючи його як приватного члена"

6

Ну а в 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означає просто це: значення є незмінним і остаточним.

А оскільки вона остаточна, Java може сміливо копіювати її в локальні анонімні класи. Ви не отримуєте посилання на int (тим більше, що ви не можете мати посилання на примітиви, такі як int на Java, лише посилання на об’єкти ).

Він просто копіює значення значення в неявний int, який називається у вашому анонімному класі.


3
Я пов'язую "змінну рівня класу" із static. Можливо, це зрозуміліше, якщо ви замість цього використовуєте "змінну екземпляра".
eljenso

1
добре, я використовував рівень класу, тому що методика буде працювати як зі змінними екземпляра, так і статичними.
Zach L

ми вже знаємо, що фінал доступний, але ми хочемо знати, чому? ви можете, будь ласка, додати ще одне пояснення, чому сторона?
Саурабх Оза

6

Причина, по якій доступ був обмежений лише локальними кінцевими змінними, полягає в тому, що якщо всі локальні змінні будуть доступні, то їх спочатку потрібно буде скопіювати в окремий розділ, де внутрішні класи можуть мати доступ до них і зберігати кілька копій локальні змінні, що змінюються, можуть призвести до суперечливих даних. Тоді як кінцеві змінні є незмінними, а отже, будь-яка кількість копій до них не матиме жодного впливу на узгодженість даних.


Це не так, як це реалізовано на мовах, як C #, які підтримують цю функцію. Насправді компілятор змінює змінну з локальної змінної на змінну екземпляра, або створює додаткову структуру даних для цих змінних, яка може випереджати область зовнішнього класу. Однак немає "декількох копій локальних змінних"
Mike76

Mike76 Я не дивився на реалізацію C #, але Scala робить друге, що ви згадали, я думаю: Якщо Intперерозподіляється всередині закриття, змініть цю змінну на екземпляр IntRef(по суті мутабельної Integerобгортки). Кожен змінний доступ потім переписується відповідно.
Adowrath

3

Щоб зрозуміти обґрунтування цього обмеження, врахуйте наступну програму:

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 залишається в пам'яті після ініціалізації метод повертає, але параметр Вел не робить. JVM не може отримати доступ до локальної змінної за її межами, тому Java здійснює наступний виклик роботи printInteger , копіюючи значення val в неявне поле з тим самим іменем в interfaceInstance . InterfaceInstance , як кажуть, захопили значення локального параметра. Якщо параметр не був остаточним (або фактично остаточним), його значення може змінитися, перетворившись на синхронізацію із захопленим значенням, що може спричинити неінтуїтивну поведінку.


2

Методи всередині анономічного внутрішнього класу можуть бути застосовані добре після припинення потоку, що породив його. У вашому прикладі внутрішній клас буде викликаний у потоці відправлення події, а не в тому ж потоці, що і той, який його створив. Отже, сфера застосування змінних буде різною. Таким чином, щоб захистити такі проблеми сфери застосування присвоєння, ви повинні оголосити їх остаточними.


2

Коли анонімний внутрішній клас визначений в тілі методу, всі змінні, оголошені остаточними в межах цього методу, доступні з внутрішнього класу. Для скалярних значень, після їх призначення, значення кінцевої змінної не може змінюватися. Для об'єктних значень посилання не може змінюватися. Це дозволяє компілятору Java "захоплювати" значення змінної під час виконання та зберігати копію як поле у ​​внутрішньому класі. Після того, як зовнішній метод припиняється і його стек-кадр був видалений, початкова змінна відсутня, але приватна копія внутрішнього класу зберігається у власній пам'яті класу.

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


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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

0

Оскільки у Джона є детальні відомості про реалізацію, іншою можливою відповіддю буде те, що JVM не хоче обробляти записи, які закінчили його активацію.

Розглянемо випадок використання, коли ваші лямбдаси замість того, щоб застосувати, зберігаються в якомусь місці та запускаються пізніше.

Я пам’ятаю, що в Smalltalk ви отримаєте незаконний магазин, коли ви зробите таку модифікацію.


0

Спробуйте цей код,

Створіть список масиву і введіть значення всередину цього і поверніть його:

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

ОП просить причини, чому щось потрібно. Отже, вам слід вказати, як адресується ваш код
NitinSingh

0

Java анонімний клас дуже схожий на закриття Javascript, але Java реалізує це по-іншому. (перевірити відповідь Андерсена)

Отже, щоб не плутати Java Developer із дивною поведінкою, яка може траплятися для тих, хто надходить з Javascript. Я думаю, саме тому вони змушують нас використовувати final, це не обмеження JVM.

Давайте розглянемо приклад Javascript нижче:

var add = (function () {
  var counter = 0;

  var func = function () {
    console.log("counter now = " + counter);
    counter += 1; 
  };

  counter = 100; // line 1, this one need to be final in Java

  return func;

})();


add(); // this will print out 100 in Javascript but 0 in Java

У Javascript counterзначення буде 100, оскільки є лише однеcounter змінна від початку до кінця.

Але в Java, якщо такої немає final, вона буде роздрукована 0, оскільки, створюючи внутрішній об'єкт, 0значення копіюється на приховані властивості об'єкта внутрішнього класу. (тут є дві цілі змінні, одна в локальному методі, інша - у прихованих властивостях внутрішнього класу)

Отже, будь-які зміни після створення внутрішнього об'єкта (наприклад, рядок 1), це не вплине на внутрішній об'єкт. Таким чином, це заплутається між двома різними результатами та поведінкою (між Java та Javascript).

Я вважаю, що тому Java вирішують змусити його бути остаточним, тому дані "узгоджуються" від початку до кінця.


0

Кінцева змінна Java всередині внутрішнього класу

внутрішній клас може використовувати тільки

  1. посилання від зовнішнього класу
  2. остаточні локальні змінні, що не входять у сферу застосування, які є еталонним типом (наприклад, Object...)
  3. intтип значення (примітивний) (наприклад ...) може бути завершений кінцевим типом посилання. IntelliJ IDEAможе допомогти вам приховати його до одного масиву елементів

Коли компілятор створює non static nested( inner class) [About] - створюється новий клас <OuterClass>$<InnerClass>.class, а пов'язані параметри передаються в конструктор [Локальна змінна в стеці] . Це схоже на закриття

остаточна змінна - це змінна, яку неможливо перепризначити. остаточну довідкову змінну все ж можна змінити, змінивши стан

Можливо, це було б дивно, тому що як програміст ви могли зробити так

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

            myClass.something(); //<- what is the address ?
            int b = a; //<- 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
        }
    });

    myClass = new MyClass(); //address 2
    int a = 10;
}

-2

Можливо, ця хитрість дає ідею

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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.