Выражение лямбда и захват переменных

Пожалуйста, объясните мне, как лямбда-выражение может использовать и изменять переменные экземпляра своего включающего класса, но может использовать только локальные переменные своей охватывающей области. (Разве это окончательный или эффективный окончательный вариант?)

Мой основной вопрос заключается в том, как переменные экземпляра класса могут быть модифицированы из лямбды, а локальные переменные - нет, в контексте области видимости.

Ответ 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 получит новое значение "для всех вас" после возврата этой лямбды.

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

Ответ 2

Вы можете обратиться к этой статье - https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood объяснил при составлении лямбда-выражений. Как объяснено лямбда-выражения/блоки кода, скомпилированные в анонимный класс, эти анонимные классы скомпилированы с форматом имени (<<Enclosing Class name>>$<<1(Number)>>), поэтому предположим, что если разрешены не окончательные локальные переменные, то компилятор не может отследить его от этой локальной переменной упоминается как анонимный класс. Файлы '.class' создаются/компилируются в вышеупомянутом формате отдельно, как обычные классы Java.

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