Почему только конечные переменные доступны в анонимном классе?
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; } }); }
Как я могу вернуть
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 } }); }
16 ответов
Как отмечается в комментариях, часть этого становится неактуальной в Java 8, где final
может быть неявным. Однако в анонимном внутреннем классе или лямбда-выражении можно использовать только эффективно окончательную переменную.
В основном это связано с тем, как Java управляет замыканиями.
Когда вы создаете экземпляр анонимного внутреннего класса, любые переменные, которые используются в этом классе, копируются в свои значения через автоматически сгенерированный конструктор. Это избавляет компилятор от необходимости автоматически генерировать различные дополнительные типы для хранения логического состояния "локальных переменных", как, например, это делает компилятор C#... (Когда C# захватывает переменную в анонимной функции, он действительно захватывает переменную - Закрытие может обновить переменную таким образом, который виден основной частью метода, и наоборот.)
Поскольку значение было скопировано в экземпляр анонимного внутреннего класса, выглядело бы странно, если бы переменная могла быть изменена остальной частью метода - у вас мог бы быть код, который, казалось, работал с устаревшей переменной (потому что именно так и происходит... вы работаете с копией, сделанной в другое время). Аналогично, если бы вы могли вносить изменения в анонимный внутренний класс, разработчики могли бы ожидать, что эти изменения будут видны в теле включающего метода.
Если сделать окончательную переменную, все эти возможности будут удалены - поскольку значение не может быть изменено вообще, вам не нужно беспокоиться о том, будут ли такие изменения видны. Единственный способ позволить методу и анонимному внутреннему классу видеть изменения друг друга - это использовать изменяемый тип некоторого описания. Это может быть сам класс включения, массив, изменяемый тип-обертка... что-нибудь в этом роде. По сути, это похоже на обмен данными между одним методом и другим: изменения, внесенные в параметры одного метода, не видны вызывающей стороне, но видны изменения, внесенные в объекты, на которые ссылаются параметры.
Если вас интересует более подробное сравнение между замыканиями в Java и C#, у меня есть статья, которая углубляется в это. Я хотел сосредоточиться на стороне Java в этом ответе:)
Существует хитрость, которая позволяет анонимному классу обновлять данные во внешней области.
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]);
// ...
Анонимный класс - это внутренний класс, и к внутренним классам применяется строгое правило (JLS 8.1.3):
Любая локальная переменная, формальный параметр метода или параметр обработчика исключений, используемые, но не объявленные во внутреннем классе, должны быть объявлены как окончательные. Любая локальная переменная, используемая, но не объявленная во внутреннем классе, должна быть обязательно назначена перед телом внутреннего класса.
Я еще не нашел причины или объяснения для jls или jvms, но мы знаем, что компилятор создает отдельный файл класса для каждого внутреннего класса, и он должен убедиться, что методы объявлены в этом файле класса (на уровне байтового кода) по крайней мере, иметь доступ к значениям локальных переменных.
(У Джона есть полный ответ - я держу этот ответ невостребованным, потому что кто-то может заинтересоваться правилом JLS)
Вы можете создать переменную уровня класса, чтобы получить возвращаемое значение. Я имею в виду
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 и использовать его там, где хотите.
Ответ вашего почему:
Локальный экземпляр внутреннего класса привязан к классу Main и может обращаться к последним локальным переменным содержащего его метода. Когда экземпляр использует последний локальный элемент своего содержащего метода, переменная сохраняет значение, которое оно содержало во время создания экземпляра, даже если переменная вышла из области видимости (это фактически грубая, ограниченная версия замыканий Java).
Поскольку локальный внутренний класс не является ни членом класса, ни пакета, он не объявляется с уровнем доступа. (Однако, имейте в виду, что его собственные члены имеют уровни доступа, как в обычном классе.)
Чтобы понять причину этого ограничения, рассмотрите следующую программу:
public class Program {
interface Interface {
public void printInteger();
}
static Interface interfaceInstance = null;
static void initialize(int val) {
class Class implements interface {
@Override
public void printInteger() {
System.out.println(val);
}
}
interfaceInstance = new Class();
}
public static void main(String[] args) {
initialize(12345);
interfaceInstance.printInteger();
}
}
InterfaceInstance остается в памяти после возврата из метода initialize, а параметр val - нет. JVM не может получить доступ к локальной переменной за пределами своей области, поэтому Java заставляет последующий вызов printInteger работать, копируя значение val в неявное поле с тем же именем в interfaceInstance. Говорят, что interfaceInstance зафиксировал значение локального параметра. Если параметр не был окончательным (или фактически конечным), его значение могло бы измениться, не синхронизироваться с зафиксированным значением, что может привести к неинтуитивному поведению.
Ну, в 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 и дать ей новое значение. final
означает только это: значение является неизменным и окончательным.
И поскольку он окончательный, Java может безопасно скопировать его в локальные анонимные классы. Вы не получаете некоторую ссылку на int (тем более что у вас не может быть ссылок на примитивы типа int в Java, только ссылки на объекты).
Он просто копирует значение a в неявный int, называемый a в вашем анонимном классе.
Причина, по которой доступ был ограничен только локальными конечными переменными, заключается в том, что если бы все локальные переменные были сделаны доступными, то сначала их нужно было бы скопировать в отдельный раздел, где внутренние классы могли бы иметь к ним доступ и поддерживать несколько копий. изменяемые локальные переменные могут привести к противоречивым данным. Принимая во внимание, что конечные переменные являются неизменяемыми, и, следовательно, любое количество копий к ним не окажет влияния на согласованность данных.
Когда в теле метода определен анонимный внутренний класс, все переменные, объявленные как final в области действия этого метода, доступны из внутреннего класса. Для скалярных значений, после того, как оно было назначено, значение конечной переменной не может измениться. Для значений объекта ссылка не может быть изменена. Это позволяет компилятору Java "захватывать" значение переменной во время выполнения и сохранять копию в виде поля во внутреннем классе. После завершения внешнего метода и удаления его стекового фрейма исходная переменная исчезает, но личная копия внутреннего класса сохраняется в собственной памяти класса.
Методы внутри анонимного внутреннего класса могут вызываться задолго после того, как поток, породивший его, завершился. В вашем примере внутренний класс будет вызываться в потоке диспетчеризации событий, а не в том же потоке, в котором он был создан. Следовательно, область действия переменных будет другой. Поэтому для защиты таких проблем области назначения переменных вы должны объявить их окончательными.
private void f(Button b, final int a[]) {
b.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
a[0] = a[0] * 5;
}
});
}
Конечная переменная Java внутри внутреннего класса
внутренний класс может использовать только
- ссылка из внешнего класса
- final локальные переменные вне области видимости, которые являются ссылочным типом (например,
Object
...) - значение (примитив) (например,
int
...) тип может быть заключен в окончательный ссылочный тип.IntelliJ IDEA
может помочь вам преобразовать его в один массив элементов
Когда non static nested
(inner class
) [About] генерируется компилятором - новый класс -<OuterClass>$<InnerClass>.class
создается, и связанные параметры передаются в конструктор [Локальная переменная в стеке]. Это похоже на закрытие
final переменная - это переменная, которую нельзя переназначить. конечную ссылочную переменную все еще можно изменить, изменив состояние
Возможно, это было бы странно, потому что как программист вы могли бы сделать вот так
//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;
}
Попробуйте этот код,
Создайте Array List, поместите в него значение и верните его:
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;
}
Поскольку у Джона есть подробности ответа, другой возможный ответ будет состоять в том, что JVM не хочет обрабатывать запись в записи, которая закончила его активацию.
Рассмотрим вариант использования, в котором ваши лямбды вместо того, чтобы их применять, хранятся в каком-то месте и запускаются позже.
Я помню, что в Smalltalk вы бы подняли нелегальный магазин, когда вы сделаете такую модификацию.
Анонимный класс Java очень похож на закрытие Javascript, но Java реализует это по-другому. (проверьте ответ Андерсена)
Так что, чтобы не запутать разработчика Java со странным поведением, которое может произойти для тех, кто исходит из фона 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
value копируется в скрытые свойства объекта внутреннего класса. (здесь есть две целочисленные переменные, одна в локальном методе, другая в скрытых свойствах внутреннего класса)
Таким образом, любые изменения после создания внутреннего объекта (например, строка 1) не повлияют на внутренний объект. Таким образом, это приведет к путанице между двумя разными результатами и поведением (между Java и Javascript).
Я считаю, что именно поэтому Java решила сделать его окончательным, чтобы данные были "согласованными" от начала до конца.
public class Closure {
public void publicMethod() {
Integer localVariable1 = 10;
Map<String, Integer> map = new HashMap<String, Integer>() {
{
put("a", localVariable1);
}
};
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println(localVariable1);
}
});
List<String> list = Arrays.asList("A", "B", "C");
Collections.sort(list, new Comparator<String>() {
public int compare(String p1, String p2) {
return p1.compareTo(p2);
}
});
}
}
- После компиляции приведенное выше приведет к созданию 4 классов (3 AIC и 1 родительский класс).
- Если бы во время выполнения Java позволила бы обновить localVariable1 или список, это сделало бы скомпилированные классы недействительными, а карта содержала бы устаревшие данные.
- Чтобы избежать такого непоследовательного поведения, Java заставляет его быть окончательным или фактически окончательным.
Может быть, этот трюк дает вам идею
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);