Все ли окончательные переменные захвачены анонимными классами?
Я думал, что знаю ответ на этот вопрос, но я не могу найти никакого подтверждения после часа или около того поиска.
В этом коде:
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, это то, что вы не можете переопределить значение, оно не защищает объект от сборщика мусора.