Все ли окончательные переменные захвачены анонимными классами?

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

В этом коде:

public class Outer {

    // other code

    private void method1() {
        final SomeObject obj1 = new SomeObject(...);
        final SomeObject obj2 = new SomeObject(...);
        someManager.registerCallback(new SomeCallbackClass() {
            @Override
            public void onEvent() {
                 System.out.println(obj1.getName());
            }
        });
    }
}

Предположим, что registerCallback где-то сохраняет свой параметр, так что объект анонимного подкласса будет жить некоторое время. Очевидно, что этот объект должен поддерживать ссылку на obj1 чтобы onEvent работал, если он вызывается.

Но, учитывая, что объект не использует obj2, он все еще поддерживает ссылку на obj2, так что obj2 не может быть собран мусором, пока объект живет? У меня сложилось впечатление, что все видимые final (или фактически конечные) локальные переменные и параметры были захвачены и, таким образом, не могут быть скопированы, пока объект был жив, но я не могу найти ничего, что говорит так или иначе. Другой.

Это зависит от реализации?

Есть ли раздел в JLS, который отвечает на это? Я не смог найти ответ там.

Ответы

Ответ 1

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

Единственный особенно важный раздел языковой спецификации, который я могу найти, это JLS Sec 8.1.3:

Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные во внутреннем классе, должны быть либо объявлены как final, либо быть фактически окончательными (§4.12.4), либо при попытке использования возникает ошибка времени компиляции.)

(Анонимные классы являются внутренними классами)

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

Я думаю, что из этого следует сделать вывод, что реализации не должны захватывать переменные, на которые нет ссылок во внутреннем классе; но это не говорит, что они не могут.

Ответ 2

Только obj1 захвачен.

Логично, что анонимный класс реализован как обычный класс примерно так:

class Anonymous1 extends SomeCallbackClass {
    private final Outer _outer;
    private final SomeObject obj1;
    Anonymous1(Outer _outer, SomeObject obj1) {
        this._outer = _outer;
        this.obj1 = obj1;
    }
    @Override
    public void onEvent() {
         System.out.println(this.obj1.getName());
    }
});

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

Использование этого становится:

someManager.registerCallback(new Anonymous1(this, obj1));

Как видите, ссылочное значение obj1 копируется (передача по значению).

Технически нет причин для того, чтобы obj1 был финальным, независимо от того, объявлен ли он final или фактически финальным (Java 8+), за исключением того, что если это не так, и вы измените значение, копия не изменится, что приведет к ошибкам, поскольку вы ожидали значение для изменения, учитывая, что копирование является скрытым действием. Чтобы избежать путаницы программистов, они решили, что obj1 должен быть окончательным, поэтому вы никогда не запутаетесь в этом поведении.

Ответ 3

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

public class test {
    private static Object holder;

    private void method1() {
        final Object obj1 = new Object();
        final Object obj2 = new Object();
        holder = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println(obj1);
            }
        };
    }
}

И привел следующий байт-код для method1

 private method1()V
   L0
    LINENUMBER 8 L0
    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 9 L1
    NEW java/lang/Object
    DUP
    INVOKESPECIAL java/lang/Object.<init> ()V
    ASTORE 2
   L2
    LINENUMBER 10 L2
    NEW test$1
    DUP
    ALOAD 0
    ALOAD 1
    INVOKESPECIAL test$1.<init> (Ltest;Ljava/lang/Object;)V
    PUTSTATIC test.holder : Ljava/lang/Object;

Что значит:

  • L0 - сохранить первый финал с idx 1 (ASTORE 1)
  • L1 - сохранить второй финал с idx 2 (тот, который не используется в классе anon) (ASTORE 2)
  • L2 - создать новый тест $ 1 с аргументами (ALOAD 0) this и obj1 (ALOAD 1)

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

Ответ 4

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

Единственное, что делает final, это то, что вы не можете переопределить значение, оно не защищает объект от сборщика мусора.