Ответ 1
Сначала мы можем взглянуть на JLS, в котором говорится следующее:
Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные в лямбда-выражении, должны быть либо объявлены окончательными, либо фактически окончательными (§4.12.4), либо при попытке использования возникает ошибка времени компиляции.
Любая локальная переменная, используемая, но не объявленная в лямбда-теле, должна быть обязательно назначена (§16 (Определенное назначение)) перед лямбда-телом, иначе произойдет ошибка времени компиляции.
Аналогичные правила использования переменных применяются в теле внутреннего класса (§8.1.3). Ограничение на эффективные конечные переменные запрещает доступ к динамически изменяющимся локальным переменным, чей захват может привести к проблемам параллелизма. По сравнению с последним ограничением это уменьшает нагрузку на программистов.
Ограничение на эффективные конечные переменные включает стандартные переменные цикла, но не расширенные переменные цикла, которые рассматриваются как отдельные для каждой итерации цикла (§14.14.2).
Чтобы лучше это понять, взгляните на этот пример класса:
public class LambdaTest {
public static void main(String[] args) {
LambdaTest test = new LambdaTest();
test.returnConsumer().accept("Hello");
test.returnConsumerWithInstanceVariable().accept("Hello");
test.returnConsumerWithLocalFinalVariable().accept("Hello");
}
String string = " world!";
Consumer<String> returnConsumer() {
return ((s) -> {System.out.println(s);});
}
Consumer<String> returnConsumerWithInstanceVariable() {
return ((s) -> {System.out.println(s + string);});
}
Consumer<String> returnConsumerWithLocalFinalVariable() {
final String foo = " you there!";
return ((s) -> {System.out.println(s + foo);});
}
}
Вывод main - это
Hello
Hello world!
Hello you there!
Это потому, что возвращение лямбды здесь во многом аналогично созданию нового анонимного класса с new Consumer<String>() {...}
. Ваша лямбда - экземпляр Consumer<String>
имеет ссылку на класс, в котором он был создан. Вы можете переписать returnConsumerWithInstanceVariable()
, чтобы использовать System.out.println(s + LambdaTest.this.string)
, это будет точно так же. Вот почему вам разрешен доступ (и изменение) переменных экземпляра.
Если у вас есть (эффективно) конечная локальная переменная в вашем методе, вы можете получить к ней доступ, потому что она копируется в ваш лямбда-экземпляр.
Но, однако, если это не окончательно, что, по вашему мнению, должно произойти в следующем контексте:
Consumer<String> returnConsumerBad() {
String foo = " you there!";
Consumer<String> results = ((s) -> {System.out.println(s + foo);});
foo = " to all of you!";
return results;
}
Должно ли значение копироваться в ваш экземпляр, но не обновляться при обновлении локальной переменной? Это, вероятно, вызовет путаницу, так как я думаю, что многие программисты ожидали, что foo
получит новое значение "для всех вас" после возврата этой лямбды.
Если бы у вас было примитивное значение, оно лежало бы в стеке. Таким образом, вы не можете просто ссылаться на локальную переменную, потому что она может исчезнуть после достижения конца вашего метода.